let mapOptions; palette = ['#e8e4e1', '#f2e9d0','#ffd480', '#bb5a5a']; //// CREATE //// function createProvinceMap(parent, geoData, options={}) { // The function takes a parent element query selector, the case data, the geojson // and a 'fill', which is either total cases, recovered cases or deaths to color the map //console.log(geoData) chartOptions = { fill: 'cases', pop: 'currentPop', extent: { regions: [], includes: false }, rotate: [0,0] } for (key in options) { //console.log(typeof options[key]) if (typeof options[key] === 'object' && !Array.isArray(options[key])) { //console.log(chartOptions[key]) for (subkey in options[key]) { chartOptions[key][subkey] ? chartOptions[key][subkey] = options[key][subkey] : null } } else { chartOptions[key] ? chartOptions[key] = options[key] : null; } } //console.log(chartOptions) mapOptions = chartOptions; //console.log(sortStatus(caseData)) // Creates SVG and layer let container = d3.select(parent); container.style('height', 'auto') //let title = container.append('div').attr('class', 'map-title') //title.text(`${geoData.data.healthUnit} (${chartOptions.fill})`); function makeButton(div, text, key, value) { let b = div.append('button').text(text).attr('class', 'map-button') if (chartOptions[key] === value) { b.style('background', '#222831') b.style('color', '#f3f3f3') } else { b.style('background', '#f3f3f3') b.style('color', '#2c2c2c') } b.on('click', function() { this.parentElement.querySelectorAll('button').forEach(button => { button.style.background = '#f3f3f3'; button.style.color = '#2c2c2c'; }); this.style.background = '#222831'; this.style.color = '#f0f0f0'; chartOptions[key] = value; updateProvinceMap(parent, geoData, chartOptions) }) } let mapButtons = container.append('div').attr('class', 'map-buttons-div') makeButton(mapButtons, 'All cases', 'fill', 'cases') makeButton(mapButtons, 'active', 'fill', 'active') makeButton(mapButtons, 'recovered', 'fill', 'recovered') makeButton(mapButtons, 'deaths', 'fill', 'deaths') let popButtons = container.append('div').attr('class', 'pop-buttons-div') makeButton(popButtons, 'Total', 'pop', 'current') makeButton(popButtons, 'Per 100K', 'pop', 'currentPop') let zoomButtons = container.append('div').attr('class', 'zoom-buttons-div') zoomButtons.append('button').text('+').attr('class', 'zoom-button plus').on('click', () => { mapSVG.transition().call(zoom.scaleBy, 3/2) }) zoomButtons.append('button').text('-').attr('class', 'zoom-button minus').on('click', () => { mapSVG.transition().call(zoom.scaleBy, 2/3) }) let mapDiv = container.append('div').attr('class', 'map-div') let mapSVG = mapDiv.append('svg').attr('class', 'map-svg').attr('width', '100%').attr('height', '100%') mapSVG.append('rect').attr('fill', 'rgba(255,255,255,0)').attr('width', '100%').attr('height', '100%') .on('click', function() { this.parentNode.querySelectorAll('.health-boundary').forEach(path => { path.setAttribute('stroke-width', 0.5) }) updateProvinceTotals('#province-total', geoData, 'Ontario (total of all public health units)') chartGraph.data = geoData; updateProvinceGraph('#province-graph', chartGraph.data) updateProvinceGender('#province-gender', geoData, {metric: chartGender.metric}) }) let mapLayer = mapSVG.append('g').attr('class', 'map-layer') let legendLayer = mapSVG.append('g').attr('class', 'legend-layer') let hoverLayer = mapSVG.append('g').attr('class', 'hover-layer') // Get width and height of SVG, which is 100% of container div let width = Number(mapSVG.style('width').split('px')[0]); let height = Number(mapSVG.style('height').split('px')[0]); var zoom = d3.zoom() //.scaleExtent([0.4, 8]) //.translateExtent([[-500, -500],[width+100, height+100]]) .on('zoom', function() { mapLayer.attr('transform', d3.event.transform); }); mapSVG.call(zoom); //let filterFeatures = geoData.features.filter(prov => filterArrayInclude.includes(prov.properties.ENGNAME)) let filterArray = chartOptions.extent.regions; let filterFeatures = geoData.features.filter(prov => chartOptions.extent.includes ? filterArray.includes(prov.properties.ENGNAME) : !filterArray.includes(prov.properties.ENGNAME)) let filterData = Object.assign({}, geoData) filterData.features = filterFeatures; //console.log(filterFeatures) // D3 stuff to calculate the geography from the geojson. This fits the shapes to the size of the SVG let projection = d3.geoConicConformal().rotate(chartOptions.rotate).fitExtent([[0, 0],[width, height]], filterData); let geoGenerator = d3.geoPath().projection(projection); // Data sorting function to add data to geojson //let healthData = sortData(caseData, geoData); // For color scale, set min and max of scale to be 0 and the max number of cases in a region let minCases = 1; //let maxCases = Math.max.apply(Math, geoData.filter(region => region.healthUnit !== 'not reported').map(function(o) { return o[fill].length; })) let maxCases = d3.max(geoData.features, d => d[chartOptions.pop][chartOptions.fill]); // Color scale let colorScale = d3.scaleLinear() .domain([0, minCases, maxCases/2, maxCases]) .range(palette) // D3 pattern -- select all paths withing the map layer and add data let m = mapLayer.selectAll('path') .data(geoData.features) // For reordering layers attempt to show mouseover stroke properly (not used rn) let saveNode; // Add paths based on data, define coloring, stroke and mouse interactions m.enter() .append("path") .attr('class', 'health-boundary') .attr("d", geoGenerator) .attr("fill", d => d.data ? colorScale(d[chartOptions.pop][chartOptions.fill]) : '#e3e3e3' ).attr('fill-opacity', 1) .attr('stroke-linejoin', 'round').attr("stroke", '#ffffff').attr("stroke-width", 0.5) .on('mouseover', function(d, i, nodes) { this.setAttribute('opacity', 0.8) // saveNode = nodes[i+1]; this.parentNode.appendChild(this); this.setAttribute('stroke', '#5f4951') this.setAttribute('stroke-width', 0.7) }) .on('mouseout', function(d, i, nodes) { hoverLayer.selectAll('*').remove(); this.setAttribute('opacity', 1) // this.parentNode.insertBefore(this, saveNode); this.setAttribute('stroke', '#ffffff'); this.setAttribute('stroke-width', 0.5) }) .on('mousemove', function(d) { let t = d3.zoomTransform(mapLayer.node()); let [x, y] = d3.mouse(this) let dx = t.x; let dy = t.y; let dz = t.k; hoverLayer.call(mouseover, [d.data.healthUnit, `${d[chartOptions.pop][chartOptions.fill].toLocaleString()}`, `${chartOptions.fill}${chartOptions.pop === 'currentPop' ? ' per 100,000' : ''}`], [x*dz + dx, y*dz + dy], [width, height], [15, 32, 17]) }) //// This updates the other charts when a region is clicked .on('click', function(d, i, node) { this.parentNode.querySelectorAll('.health-boundary').forEach(path => { path.setAttribute('stroke-width', 0.5) }) this.setAttribute('stroke-width', 5) //this.setAttribute('stroke', '#f3f3f3') //this.setAttribute('stroke', 2) //title.text(`${geoData.data.healthUnit} (${chartOptions.fill})`); updateProvinceTotals('#province-total', d, d.data.healthUnit) //updateProvinceCases('#province-cases', d.data.cases, 1, 10, d.data.healthUnit) updateProvinceGraph('#province-graph', d) chartGender.data = d; updateProvinceGender('#province-gender', chartGender.data, {metric: chartGender.metric}) }) let legendArray = []; let legendRectWidth = 30; let legendRectHeight = 12; let legendDivs = 9; for (let i = 0; i < legendDivs; i++) { legendArray.push(i*maxCases/(legendDivs-1)); } let L = legendLayer.selectAll('rect').data(legendArray) let T = legendLayer.selectAll('text').data(legendArray) L.enter().append('rect') .attr('x', (d, i) => i*legendRectWidth) .attr('y', 3) .attr('width', legendRectWidth-2) .attr('height', legendRectHeight) .attr('fill', d => colorScale(d)) T.enter().append('text') .attr('x', (d, i) => i*legendRectWidth + legendRectWidth/2 - 1) .attr('y', 0) .attr('text-anchor', 'middle') .attr('font-size', 11) .attr('opacity', (d, i) => i%2 === 0 ? 1 : 0) .text(d => Math.round(d).toLocaleString()) legendLayer.attr('transform', `translate(10, ${height - legendRectHeight - 13})`) // Resize function: Re-centers map upon container resize, with debounce function to limit # of redraws/second window.addEventListener('resize', debounce(() => { resizeProvinceMap(parent, geoData) })) } //// UPDATE //// function updateProvinceMap(parent, geoData, options={}) { chartOptions = { fill: 'cases', pop: 'currentPop', extent: { regions: [], includes: false }, rotate: [0,0] } for (key in options) { //console.log(typeof options[key]) if (typeof options[key] === 'object') { for (subkey in options[key]) { chartOptions[key][subkey] ? chartOptions[key][subkey] = options[key][subkey] : null } } else { chartOptions[key] ? chartOptions[key] = options[key] : null; } } //console.log(chartOptions) // For update, select map elements instead of appending them let container = d3.select(parent); //let title = container.select('.map-title') // title.text(`${geoData.data.healthUnit} (${chartOptions.fill})`); let mapSVG = container.select('.map-svg') let mapLayer = mapSVG.select('.map-layer') let legendLayer = mapSVG.select('.legend-layer') //This is to recalculate the max /*healthData = [] geoData.features.forEach(region => { healthData.push(region.data[chartOptions.fill].length) })*/ let minCases = 0.01; let maxCases = d3.max(geoData.features, d => d[chartOptions.pop][chartOptions.fill]); // Color scale let colorScale = d3.scaleLinear() .domain([0, minCases, maxCases/2, maxCases]) .range(palette) let m = mapLayer.selectAll('path') .data(geoData.features) // We only need to change things that will be updated... we might want to toggle // between total, recovered, deaths, etc., so here we're transitioning the fill color m.transition().duration(500) .attr("fill", d => d.data ? colorScale(d[chartOptions.pop][chartOptions.fill]) : '#e3e3e3' ).attr('fill-opacity', 1) let legendArray = []; let legendDivs = 9; for (let i = 0; i < legendDivs; i++) { legendArray.push(i*maxCases/(legendDivs-1)); } let L = legendLayer.selectAll('rect').data(legendArray) let T = legendLayer.selectAll('text').data(legendArray) L.attr('fill', d => colorScale(d)) T.text(d => Math.round(d).toLocaleString()) } //// RESIZE //// function resizeProvinceMap(parent, geoData) { //console.log('resizing...') //Select map elements already created let container = d3.select(parent); let mapSVG = container.select('.map-svg') let mapLayer = mapSVG.select('.map-layer') let legendLayer = mapSVG.select('.legend-layer') // Get current width/height of SVG let width = Number(mapSVG.style('width').split('px')[0]); let height = Number(mapSVG.style('height').split('px')[0]); let filterArray = mapOptions.extent.regions; let filterFeatures = geoData.features.filter(prov => mapOptions.extent.includes ? filterArray.includes(prov.properties.ENGNAME) : !filterArray.includes(prov.properties.ENGNAME)) let filterData = Object.assign({}, geoData) filterData.features = filterFeatures; // D3 stuff to calculate the geography from the geojson. This fits the shapes to the size of the SVG let projection = d3.geoConicConformal().rotate(mapOptions.rotate).fitExtent([[0, 0],[width, height]], filterData); let geoGenerator = d3.geoPath().projection(projection); // Updates map shapes based on new projection mapLayer.selectAll('path').data(geoData.features).attr("d", geoGenerator) legendLayer.attr('transform', `translate(10, ${height - 12 - 13})`) } function sortStatus(caseData) { statusArray = []; caseData.forEach(person => { let statusObj = statusArray.find(obj => obj.status === person.Outcome1); if (statusObj) { statusObj.cases.push(person) } else { let newStatus = { status: person.Outcome1, cases: [] } newStatus.cases.push(person); statusArray.push(newStatus) } }) return statusArray } function sortAges(caseData) { ageArray = []; caseData.forEach(person => { let ageObj = ageArray.find(obj => obj.status === person['Patient-Age']); if (ageObj) { ageObj.cases.push(person) } else { let newAge = { status: person['Patient-Age'], cases: [] } newAge.cases.push(person); ageArray.push(newAge) } }) return ageArray } // Add to resize event listener to only trigger resize after 100ms of not actively resizing function debounce(func){ var timer; return function(event){ if(timer) clearTimeout(timer); timer = setTimeout(func,100,event); }; }