let chartArray = []; class LineChart { constructor(parent, data, lines=[null], options={}) { this.parent = d3.select(parent); this.data = data; this.lines = lines; this.options = options; this.settings = { pad: { top: 5, right: 5, bottom: 5, left: 5 }, axis: { x: { show: true }, y: { show: true } } }; chartArray.push(this); } optionUpdate(original, update) { Object.entries(update).forEach(option => { if (original[option[0]]) { if (typeof original[option[0]] === 'object' && !original[option[0]].isArray) { this.optionUpdate(original[option[0]], option[1]) } else { original[option[0]] = option[1] } } }) } create() { this.parent.selectAll('*').remove() if (this.settings.title) { this.title = this.parent.append('div').attr('class', 'ctv-chart-title').text(this.settings.title) } this.chartContainer = this.parent.append('div').attr('class', 'ctv-chart-container') this.svg = this.chartContainer.append('svg').attr('width', '100%').attr('height', '100%') if (this.settings.subText) { this.title = this.parent.append('div').attr('class', 'ctv-chart-subtext').text(this.settings.subText) } if (this.settings.source) { this.title = this.parent.append('div').attr('class', 'ctv-chart-source').html(`Source`) } this.axisLayer = this.svg.append('g').attr('class', 'axis-layer') this.yAxis = this.axisLayer.append('g').attr('class', 'y-axis') this.xAxis = this.axisLayer.append('g').attr('class', 'x-axis') this.chartLayer = this.svg.append('g').attr('class', 'chart-layer') this.lines.forEach((d, i) => { let c = this.chartLayer.append('g').attr('class', `layer-${i}`) c.append('path').attr('class', (d,i) => `chart-area area-${i}`) c.append('path').attr('class', (d,i) => `chart-line line-${i}`) }) } draw() { //console.log(this.settings) this.width = Number(this.svg.style('width').split('px')[0]); this.height = Number(this.svg.style('height').split('px')[0]); let flat = []; this.data.forEach(d => {this.lines.forEach(category => flat.push(Number(typeof d === 'object' ? d[category] : d) ))}) let max = d3.max(flat) this.xScale = d3.scaleLinear().domain([0, this.data.length-1]).range([this.settings.pad.left, this.width - this.settings.pad.right]) this.yScale = d3.scaleLinear().domain([0, max*1.2]).range([this.height - this.settings.pad.bottom, this.settings.pad.top]) this.lines.forEach((category, i) => { let line = d3.line().x((d,i) => this.xScale(i)).y(d => this.yScale(Number(typeof d === 'object' ? d[category] : d))) let area = d3.area().x((d,i) => this.xScale(i)).y0(this.yScale(0)).y1(d => this.yScale(Number(typeof d === 'object' ? d[category] : d))) let layer = this.chartLayer.select(`.layer-${i}`); if (this.settings.type === 'area') { layer.select('.chart-area').datum(this.data) .attr('d', area) } layer.select('.chart-line').datum(this.data) .attr('d', line) }) } drawAxes() { if (this.settings.axis.y.show) { this.yAxis.attr('transform', `translate(${this.width-this.settings.pad.right}, 0)`) .call(d3.axisRight(this.yScale) .tickSize(-this.width + this.settings.pad.right + this.settings.pad.left) .tickSizeOuter(0) .tickFormat(d3.format(this.settings.tickFormat ? this.settings.tickFormat : '~s')) ) } if (this.settings.axis.x.show) { this.xAxis.attr('transform', `translate(0, ${this.height-this.settings.pad.bottom})`) .call(d3.axisBottom(this.xScale) .ticks(this.width/100) .tickFormat(d => this.data[d].Year) ) } } update() { this.optionUpdate(this.settings, this.options) this.draw(); this.drawAxes(); } } class PeopleChart { constructor (parent, data) { this.parent = d3.select(parent); this.data = data; } draw() { this.parent.selectAll('*').remove() this.parent.append('div').attr('class', 'ctv-chart-title').text('Representation of visible minorities among police (2018)') this.parent.append('div').attr('class', 'ctv-chart-subtext').text('This chart compares the percentage of visible minorites present in Canadian police forces compared to the general population in the area that force represents. Select cities with the highest proportion of visible minority representation are also shown.') this.data.forEach(region => { let container = this.parent.append('div').attr('class', 'people-chart-container') let title = container.append('div').attr('class', 'people-chart-title').text(region.region) let div = container.append('div').attr('class', 'people-chart-div') let chart1 = div.append('div').attr('class', 'people-chart') chart1.append('div').attr('class', 'people-chart-subtitle').text(`Police (${region.police}%)`) let people1 = chart1.append('div').attr('class', 'people-chart-grid') for (let i = 0; i < 100; i++) { people1.append('div').attr('class', i < region.police ? 'per-vis-min' : 'per') } let chart2 = div.append('div').attr('class', 'people-chart') chart2.append('div').attr('class', 'people-chart-subtitle').text(`Average (${region.average}%)`) let people2 = chart2.append('div').attr('class', 'people-chart-grid') for (let i = 0; i < 100; i++) { people2.append('div').attr('class', i < region.average ? 'per-vis-min' : 'per') } }) } } let containerSalary = d3.select('.ctv-chart#chart-2') containerSalary.selectAll('*').remove() let salaryArray = [] class Salary { constructor(name, tests, highlight=false) { this.name = name; this.tests = tests; //this.totalTests = totalTests; this.highlight = highlight; salaryArray.push(this) } percent() { return `${100*this.tests/maxTests}%` } } new Salary('First Nations', 88394.00) new Salary('Municipals', 100962.00) new Salary('OPP', 102821.00) new Salary('RCMP', 99082.00) new Salary('RNC', 88419.00) new Salary('Sûreté du Québec', 87245.00) salaryArray.sort((a, b) => a.tests - b.tests) maxTests = d3.max(salaryArray, d => d.tests); salaryArray.forEach((salary, i) => { containerSalary.append('div').attr('class', `testing-salary ${salary.highlight ? 'testing-bold highlight' : ''}`).text(salary.name) let barContainer = containerSalary.append('div').attr('class', 'testing-bar-container') let bar = barContainer.append('div').attr('class', `testing-bar ${salary.highlight ? 'highlight' : ''}`) bar.style('width', 0).transition().duration(1200 + salary.tests/200).style('width', salary.percent()) let num = bar.append('div').attr('class', 'testing-number').text('$' + salary.tests.toLocaleString()) num.style('opacity', 0).transition().duration(1200 + salary.tests/200).style('opacity', 1) }) d3.select(containerSalary.node().parentElement).append('div').attr('class', 'ctv-chart-subtext').text('RNC = Royal Newfoundland Constabulary, OPP = Ontario Provincial Police. The average salary calculation uses the full-time equivalents counts collected in the survey, and not the actual headcount numbers.') let containerPopulation = d3.select('.ctv-chart#chart-4') containerPopulation.selectAll('*').remove() let populationArray = [] class Population { constructor(name, tests, highlight=false) { this.name = name; this.tests = tests; //this.totalTests = totalTests; this.highlight = highlight; populationArray.push(this) } percent() { return `${100*this.tests/maxTests}%` } } new Population('N.L.', 171) new Population('P.E.I.', 141) new Population('N.S.', 194) new Population('N.B.', 160) new Population('Que.', 189) new Population('Ont.', 177) new Population('Man.', 189) new Population('Sask.', 186) new Population('Alta.', 174) new Population('B.C.', 185) new Population('Y.T.', 326) new Population('N.W.T.', 416) new Population('Nvt.', 354) new Population('Average', 182, true) populationArray.sort((a, b) => a.tests - b.tests) maxTests = d3.max(populationArray, d => d.tests); populationArray.forEach((population, i) => { containerPopulation.append('div').attr('class', `testing-population ${population.highlight ? 'testing-bold highlight' : ''}`).text(population.name) let barContainer = containerPopulation.append('div').attr('class', 'testing-bar-container') let bar = barContainer.append('div').attr('class', `testing-bar ${population.highlight ? 'highlight' : ''}`) bar.style('width', 0).transition().duration(1200 + population.tests/200).style('width', population.percent()) let num = bar.append('div').attr('class', 'testing-number').text(population.tests.toLocaleString()) num.style('opacity', 0).transition().duration(1200 + population.tests/200).style('opacity', 1) }) async function createCharts() { let data1 = await d3.csv("/polopoly_fs/1.4980710!/httpFile/file.txt") let data3 = await d3.csv("/polopoly_fs/1.4980711!/httpFile/file.txt") let data5 = [ { region: 'Canada (All police)', police: 8, average: 22 }, { region: 'Canada (First Nation services)', police: 19, average: 22 }, { region: 'Canada (RCMP)', police: 11, average: 22 }, { region: 'Toronto', police: 25, average: 51 }, { region: 'Peel, Ont.', police: 20, average: 60 }, { region: 'Vancouver', police: 25, average: 48 }, ] console.log(data1, data3) let options = { pad: { bottom: 30, right: 50, left: 30, top: 5 }, axis: { x: { show: true }, y: { show: true } } }; let chart1 = new LineChart(`.ctv-chart#chart-1`, data1, ['Constant dollars'], options) chart1.settings.title = 'Police expenditures per capita (1987-2018)' chart1.settings.subText = 'Values in this chart are adjusted for inflation and shown in the equivalent of 2002 dollars.' chart1.settings.source = 'https://www150.statcan.gc.ca/n1/pub/85-002-x/2019001/article/00015/tbl/tbl01-eng.htm'; chart1.settings.type = 'area'; chart1.settings.tickFormat = '$,~s' let chart3 = new LineChart(`.ctv-chart#chart-3`, data3, ['Police officers'/*, 'Civilians'*/], options) chart3.settings.title = 'Police officers per 100,000 people (1962-2018)' chart3.settings.subText = 'Police officers represent the full-time equivalent permanent, fully-sworn police officers of all ranks.' chart3.settings.source = 'https://www150.statcan.gc.ca/n1/pub/85-002-x/2019001/article/00015-eng.htm'; chart3.settings.type = 'area'; let chart5 = new PeopleChart('.ctv-chart#chart-5', data5) chart5.draw() chartArray.forEach(chart => { chart.create() chart.update() }) window.addEventListener('resize', function() { chartArray.forEach(chart => { chart.update() }) }) } window.onload = () => createCharts();