parsedData = {}; async function getData() { // FETCH DATA FROM LOCAL SERVER /* const res = await fetch ('http://localhost:3000/'); // GeoJSON + Dissolution MP + Turnout const res3 = await fetch('http://localhost:3002/'); // Race Number/Riding ID map const res4 = await fetch('http://localhost:3003/'); // 2019 Results const res5 = await fetch('http://localhost:3004/'); // 2105 Results */ // FETCH DATA FROM ELECTION CTVÂ鶹ӰÊÓ SERVER const res = await fetch ('https://election.ctvnews.ca/polopoly_fs/1.4648476!/httpFile/file.txt'); //Geo Data const res3 = await fetch ('https://election.ctvnews.ca/polopoly_fs/1.4648466!/httpFile/file.txt') //Race Numnbers const res4 = await fetch ('https://election.ctvnews.ca/cmlink/7.737056'); // Results const res5 = await fetch ('https://election.ctvnews.ca/polopoly_fs/1.4652493!/httpFile/file.txt') data1 = await res.json(); // GeoJSON + Dissolution MP + Turnout data3 = await res3.json(); // Race Number/Riding ID map data4 = await res4.json(); // 2019 Results data5 = await res5.json(); // 2105 Results /// MERGE DATA data1.features.forEach(riding => { data3.forEach(thing => { if (riding.properties.FED_NUM === thing.FED_NUM) { riding.RaceNbr = thing.RaceNbr; riding.results = {} } }) }) data1.features.forEach(riding => { data4.QueryResults.forEach(race => { if (riding.RaceNbr === race.RaceNbr) { riding.results['2019'] = race; } }) }) data1.features.forEach(riding => { let match = false; data5.QueryResults.forEach(race => { if (riding.results['2019'].RaceName === race.RaceName) { riding.results['2015'] = race; match = true; } }) if (match === false) { console.log('Mismatch'); } }) parsedData = data1; console.log(parsedData) /// MERGE DATA END /// DRAW MAPS (SELECTOR, FILL) let canada = parsedData.features; drawMap( '#election-map-1', canada.filter( riding => riding.properties.PROVCODE === "BC" ), {fillFunction: fillMap.winner}, [ [ ['2019', {year: 2019}], ['2015', {year: 2015}] ], [ //['winner', {fillFunction: fillMap.winner}] ] ], canada.filter( riding => riding.properties.FEDNUM === 59037 || riding.properties.FEDNUM === 59029 || riding.properties.FEDNUM === 59041 ) ) drawMap( '#election-map-2', canada.filter( riding => riding.properties.PROVCODE === "SK" || riding.properties.PROVCODE === "AB" ), {fillFunction: fillMap.winner}, [ [ ['2019', {year: 2019}], ['2015', {year: 2015}] ] ] ) drawMap( '#election-map-3', canada.filter( riding => riding.properties.PROVCODE === "ON" || riding.properties.PROVCODE === "MB" ), {fillFunction: fillMap.winner}, [ [ ['2019', {year: 2019}], ['2015', {year: 2015}] ] ] ) drawMap( '#election-map-4', canada.filter( riding => riding.properties.PROVCODE === "QC" ), {party: 'BQ', fillFunction: fillMap.winner}, [ [ ['2019', {year: 2019}], ['2015', {year: 2015}] ], [ ['winner', {fillFunction: fillMap.winner}], ['vote share', {fillFunction: fillMap.voteShare}] ] ] ) drawMap( '#election-map-5', canada.filter( riding => riding.properties.PROVCODE === "NB" || riding.properties.PROVCODE === "NS" || riding.properties.PROVCODE === "PE" || riding.properties.PROVCODE === "NL" ), {party: 'GRN', fillFunction: fillMap.winner}, [ [ ['2019', {year: 2019}], ['2015', {year: 2015}] ], [ ['winner', {fillFunction: fillMap.winner}], ['vote share', {fillFunction: fillMap.voteShare}] ] ], canada.filter( riding => riding.properties.FEDNUM === 10002 || riding.properties.FEDNUM === 13005 || riding.properties.FEDNUM === 12009 ) ) /// DRAW MAPS END } let fillMap = { plain() { return { color: '#ededed', opacity: 1 } }, winner(year) { return { color: d => {return d.results[year].PollsPercent > 20 ? partyColor(d.results[year].Rows[0].PartyName) : '#dedede'}, opacity: d => {return d.results[year].Rows[0].ElectedFlag === "E" ? 1 : 0.5} } }, runnerUp(year) { return { color: d => {return d.results[year].PollsPercent > 20 ? partyColor(d.results[year].Rows[1].PartyName) : '#dedede'}, opacity: d => {return d.results[year].Rows[0].ElectedFlag === "E" ? 1 : 0.5} } }, voteShare(year, party) { return { color: partyColor(party), opacity: d => { let opacity = 0; d.results[year].Rows.forEach(row => { if (row.PartyName === party) { opacity = 2*row.VotePercent/100; } }) return opacity; } } } } ///DRAW GEO MAP function drawMap(selector, region, customOptions, buttons, customZoom) { /// PLACE SVG let options = { fillFunction: fillMap.winner, year: 2019, party: 'LIB', riding: null } Object.keys(customOptions).forEach(key => { options[key] = customOptions[key]; }) let container = d3.select(selector) let svg = container.append('svg') .attr('class', 'svg') .call(d3.zoom().on("zoom", function () { geoMapSVG.attr("transform", d3.event.transform) geoMapSVG.style("stroke-width", Math.min(0.5, 1/(d3.event.transform.k)) + 'px') })) let geoMapSVG = svg.append('g') .attr('class', 'mapLayer') .attr('id', 'geoLayer') /// SET UP DIMENSIONS AND PROJECTION let width = Number(svg.style('width').split('px')[0]); let height = Number(svg.style('height').split('px')[0]); let pad = 8; let projection = d3.geoMercator().fitExtent([[width/pad, height/pad], [width-width/pad, height-height/pad]], {type: "FeatureCollection", features: customZoom ? customZoom : region}) let path = d3.geoPath().projection(projection); /// DRAW MAP geoMapSVG.selectAll('path') .data(region) .enter() .append("path") .attr('class', 'ridings') .attr("d", path) .style("fill", options.fillFunction(options.year, options.party).color) .attr('fill-opacity', options.fillFunction(options.year, options.party).opacity) .attr('stroke-linejoin', 'round') .attr("stroke", '#fff') .on('click', riding => { options.riding = riding; ridingResults(options.riding) }) .on('mouseover', riding => { // }) /// ADD BUTTONS OVERLAY let map = document.querySelector(selector); let overlay = make('div', map, 'overlay') function makeButton(text, input, parent) { let button = make('div', parent, 'button') button.innerText = text; button.addEventListener('click', () => { Object.keys(input).forEach(key => { options[key] = input[key]; }) geoMapSVG.selectAll('path').transition() .duration(500) .style("fill", options.fillFunction(options.year, options.party).color) .attr('fill-opacity', options.fillFunction(options.year, options.party).opacity) if (options.riding) {ridingResults(options.riding)} button.parentNode.querySelectorAll('.button').forEach(button => { button.classList.remove('selected'); }) button.classList.add('selected'); }) return button; } buttons.forEach((group) => { let buttonGroup = make('div', overlay, 'button-group'); group.forEach((button, j) => { let newButton = makeButton(button[0], button[1], buttonGroup); if (j === 0) {newButton.classList.add('selected')} }) }) /// ADD STATS OVERLAY let resultsOverlay = make('div', map, 'resultsOverlay') let statsDiv = make('div', resultsOverlay, 'statsDiv ridingStats'); let ridingName = make('div', statsDiv, 'statsName bold'); ridingName.innerText = 'Select Riding for Details' ridingName.style.background = '#cecece' let pollsDiv = make('div', statsDiv, 'pollsDiv'); let resultsDiv = make('div', statsDiv, 'resultsDiv') function ridingResults(riding) { let r = riding.results[options.year]; ridingName.innerText = `${r.RaceName}`; ridingName.style.background = r.Rows[0].ElectedFlag ==="E" ? partyColor(r.Rows[0].PartyName) : '#dedede'; ridingName.style.color = r.Rows[0].ElectedFlag ==="E" ? '#ffffff' : '#4a4a4a'; let pollColor = convertHex(partyColor(r.Rows[0].PartyName), 20); let pollBackground = `rgba(255, 255, 255, 0.6)` pollsDiv.innerText = `${r.PollsReporting} / ${r.TotalPolls} polls reporting`; pollsDiv.style.backgroundImage = `linear-gradient( 90deg, ${pollColor} 0%, ${pollColor} ${r.PollsPercent}%, ${pollBackground} ${r.PollsPercent}%, ${pollBackground} 100%)`; pollsDiv.style.backgroundImageOpacity = 0.5; let maxResults = 6; resultsDiv.innerHTML = ''; resultsChart(r, maxResults) } function resultsChart(r, maxResults) { r.Rows.forEach((party, i) => { if (i < maxResults) { let resultName = make('div', resultsDiv, 'resultName'); resultName.innerText = `${party.CandidateName} ${party.IncumbentCandidateFlag === '*' ? '(I)' : '' }`; let resultParty = make('div', resultsDiv, 'resultParty bold'); resultParty.innerText = `${party.PartyName.substring(0, 3)}` let resultBarDiv = make('div', resultsDiv, 'resultBarDiv'); let resultBar = make('div', resultBarDiv, 'resultBar'); resultBar.style.width = `${party.VotePercent}%`; resultBar.style.background = partyColor(party.PartyName); let resultVotes = make('div', resultsDiv, 'resultVotes') resultVotes.innerText = `${party.Votes}`; let resultCheck = make('div', resultsDiv, 'resultCheck'); resultCheck.innerText = party.ElectedFlag === "E" ? '✔' : ''; resultCheck.style.background = party.ElectedFlag === "E" ? 'green' : 'white'; } }) } } /// TO DO /// add circles? //// END TO DO /// HELPER FUNCTIONS //Keys for turning party shortform to full name or color const partyShortArray = ['LIB', 'CON', 'NDP', 'GRN', 'BQ', 'PPC', 'IND', 'LT']; const partyLongArray = ['Liberal', 'Conservative', 'NDP', 'Green', 'Bloc Québécois', `People's Party`, `Independent`, `Libertarian`]; const partyColorArray = ['#B91319', '#1A4782', '#F37021', '#3D9B35', '#33B2CC', '#472e8c', '#888', '#555'] const provinceShortArray = ['BC', 'AB', 'SK', 'MB', 'ON', 'QC', 'NB', 'NS', 'PE', 'NL', 'YT', 'NT', 'NU']; const provinceMidArray = ['B.C.', 'Alta.', 'Sask.', 'Man.', 'Ont.', 'Que.', 'N.B.', 'N.S.', 'P.E.I.', 'Nfld.', 'Y.T.', 'N.W.T.', 'N.T.']; const provinceFullArray = ['British Columbia', 'Alberta', 'Saskatchewan', 'Manitoba', 'Ontario', 'Quebec', 'New Brunswick', 'Nova Scotia', 'Prince Edward Island', 'Newfoundland and Labrador', 'Yukon Territory', 'Northwest Territories', 'Nunavut'] //Return full name from short name (LIB -> Liberal) function partyFullName(shortName) { let fullName = shortName; if (partyShortArray.indexOf(shortName) !== -1) { fullName = partyLongArray[partyShortArray.indexOf(shortName)]; } return fullName; } //Return color from short name function partyColor(name) { let color = '#4a4a4a'; if (partyShortArray.indexOf(name) !== -1) { color = partyColorArray[partyShortArray.indexOf(name)]; } else if (partyLongArray.indexOf(name) !== -1) { color = partyColorArray[partyLongArray.indexOf(name)]; } return color; } //Create and append element to parent with optional classes, id function make(type, parent, CLASS, ID) { let element = document.createElement(type); if (typeof CLASS !== 'undefined') { element.setAttribute('class', CLASS); } if (typeof ID !== 'undefined') { element.setAttribute('id', ID); } return parent.appendChild(element); } function convertHex(hex,opacity){ hex = hex.replace('#',''); r = parseInt(hex.substring(0,2), 16); g = parseInt(hex.substring(2,4), 16); b = parseInt(hex.substring(4,6), 16); result = 'rgba('+r+','+g+','+b+','+opacity/100+')'; return result; }