From 6db96f1ed15fc0d582167cb0d7ac784e9e203edb Mon Sep 17 00:00:00 2001 From: Monika Furdyna Date: Fri, 4 Oct 2024 17:20:51 +0200 Subject: [PATCH] Feat: update emission intensity - auto re-size and remove built-in filters (#42) * Refactor emission intensity plot * Add event listeners * Tidy up * Style --- src/js/time_line.js | 529 ++++++++++++---------------------- src/routes/sector_view.svelte | 15 +- 2 files changed, 191 insertions(+), 353 deletions(-) diff --git a/src/js/time_line.js b/src/js/time_line.js index 2fe3cb2..e038cde 100644 --- a/src/js/time_line.js +++ b/src/js/time_line.js @@ -3,7 +3,7 @@ import * as d3 from 'd3'; import '../css/plot_styles.css'; export class time_line { - constructor(container, data, labels, opts) { + constructor(container, data) { let container_div; if (typeof container === 'string') { container_div = document.querySelector(container); @@ -12,199 +12,220 @@ export class time_line { } d3.select(container_div).attr('chart_type', 'time_line'); - d3.select(container_div).attr('chart_type_data_download', 'emissions'); //matching the names in the export/ folder + + container_div.innerHTML = ''; container_div.classList.add('time_lineChart'); container_div.classList.add('d3chart'); container_div.classList.add('emissionstrajectory_chart'); container_div.classList.add('chart_container'); - let container_div_width = parseInt(window.getComputedStyle(container_div, null).width); - - let chart_div = document.createElement('div'); - chart_div.classList.add('chart_div'); - container_div.insertBefore(chart_div, container_div.firstChild); - - this.container = d3.select(chart_div); - - opts = typeof opts === 'undefined' ? {} : opts; - this.asset_class = typeof opts.asset_class === 'undefined' ? '' : opts.asset_class; - - this.xtitle = typeof opts.xtitle === 'undefined' ? '' : opts.xtitle; - //this.ytitle = (typeof opts.ytitle === 'undefined') ? "CO2 intensity" : opts.ytitle; - let line_color = typeof opts.line_color === 'undefined' ? '#1b324f' : opts.line_color; - this.dot_color = typeof opts.dot_color === 'undefined' ? this.line_color : opts.dot_color; - let scen_line_color = - typeof opts.scen_line_color === 'undefined' ? '#00c082' : opts.scen_line_color; - //this.scenario = (typeof opts.scenario === 'undefined') ? "B2DS" : opts.scenario; - let default_class = typeof opts.default_class === 'undefined' ? '' : opts.default_class; - let default_sector = - typeof opts.default_sector === 'undefined' ? 'Aviation' : opts.default_sector; - - //set labels - labels = typeof labels === 'undefined' ? {} : labels; - - const title_what = - typeof labels.title_what === 'undefined' - ? ' : 5-year emission intensity trend of ' - : labels.title_what, - caption_alloc = - typeof labels.caption_alloc === 'undefined' ? 'Allocation method: ' : labels.caption_alloc, - caption_market = - typeof labels.caption_market === 'undefined' ? 'Equity market: ' : labels.caption_market, - scen_label = typeof labels.scen_label === 'undefined' ? 'Scenario' : labels.scen_label, - port_label = typeof labels.port_label === 'undefined' ? 'Portfolio' : labels.port_label, - hoverover_value = - typeof labels.hoverover_value === 'undefined' ? 'Value: ' : labels.hoverover_value, - footnote = - typeof labels.footnote === 'undefined' ? '* start date of the analysis' : labels.footnote; - - // asset class selector - let class_names = d3 - .map(data, (d) => d.asset_class_translation) - .keys() - .sort(); - let class_selector = document.createElement('select'); - class_selector.classList = 'time_line_class_selector inline_text_dropdown'; - class_selector.addEventListener('change', change_class); - class_names.forEach((class_name) => class_selector.add(new Option(class_name, class_name))); - class_selector.options[Math.max(class_names.indexOf(default_class), 0)].selected = 'selected'; - - // sector selector - let sector_names = d3 - .map(data, (d) => d.sector_translation) - .keys() - .sort(); - let sector_selector = document.createElement('select'); - sector_selector.classList = 'peercomparison_group_selector inline_text_dropdown'; - sector_selector.addEventListener('change', change_class); - sector_names.forEach((sector_name) => - sector_selector.add(new Option(sector_name, sector_name)) - ); - - let disabled = sector_names.map( - (sector) => data.filter((d) => d.sector_translation == sector).map((d) => d.disabled)[0] - ); - disabled.map((d, i) => (sector_selector.options[i].disabled = d)); - sector_selector.options[Math.max(sector_names.indexOf(default_sector), 0)].selected = - 'selected'; - - // create title with selectors - let titlediv = document.createElement('div'); - titlediv.style.width = container_div_width + 'px'; - titlediv.classList = 'chart_title'; - titlediv.appendChild(class_selector); - titlediv.appendChild(document.createTextNode(title_what)); - titlediv.appendChild(sector_selector); - chart_div.appendChild(titlediv); - - // allocation selector - let allocation_selector = document.createElement('select'); - allocation_selector.classList = 'time_line_allocation_selector inline_text_dropdown'; - allocation_selector.addEventListener('change', change_class); - - // market selector - let market_selector = document.createElement('select'); - market_selector.classList = 'time_line_market_selector inline_text_dropdown'; - market_selector.addEventListener('change', change_class); - - // create bottom filters - let filtersdiv = document.createElement('div'); - filtersdiv.style.width = this.ttl_width + 'px'; - filtersdiv.classList = 'chart_filters'; - filtersdiv.appendChild(document.createTextNode(caption_alloc)); - filtersdiv.appendChild(allocation_selector); - filtersdiv.appendChild(document.createTextNode('\u00A0\u00A0\u00A0\u00A0')); - filtersdiv.appendChild(document.createTextNode(caption_market)); - filtersdiv.appendChild(market_selector); - chart_div.appendChild(filtersdiv); - - this.ttl_width = 750; - this.ttl_height = 450; - this.margin = { top: 70, right: 190, bottom: 70, left: 80 }; - this.width = this.ttl_width - this.margin.left - this.margin.right; - this.height = this.ttl_height - this.margin.top - this.margin.bottom; + this.container = d3.select(container_div); + + // Declare the chart dimensions and margins. + const width = 928; + const height = 650; + const marginTop = 50; + const marginRight = 50; + const marginBottom = 200; + const marginLeft = 200; + + const svg = this.container + .append('svg') + .attr('width', width) + .attr('height', height) + .attr('viewBox', [0, 0, width, height]) + .attr('preserveAspectRatio', 'xMinYMin meet') + .attr('style', 'max-width: 100%; height: auto;'); + + let asset_class = document.querySelector('#asset_class_selector').value, + sector = document.querySelector('#sector_selector').value, + allocation_method = document.querySelector('#allocation_method_selector').value, + equity_market = document.querySelector('#equity_market_selector').value; + + //filter data + let subdata = data + .filter((d) => d.asset_class == asset_class) + .filter((d) => d.sector == sector) + .filter((d) => d.allocation_translation == allocation_method) + .filter((d) => d.equity_market == equity_market); + + if (subdata.length == 0) { + container_div.querySelector('svg').innerHTML = ''; + return; + } const parseYear = d3.timeParse('%Y'); - this.x = d3.scaleTime().range([0, this.width]); - this.y = d3.scaleLinear().range([this.height, 0]); + const x = d3 + .scaleTime() + .domain(d3.extent(subdata, (d) => parseYear(d.year))) + .range([marginLeft, width - marginRight]); + + const y = d3 + .scaleLinear() + .domain(d3.extent(subdata, (d) => d.value)) + .nice() + .range([marginTop, height - marginBottom]); + + // Declare colours + let line_color = '#1b324f', + scen_line_color = '#00c082'; + + // Declare text labels + let xtitle = '', + scen_label = 'Scenario', + port_label = 'Portfolio', + hoverover_value = 'Value: ', + footnote = '* start date of the analysis'; const line = d3 .line() - .x((d) => this.x(parseYear(d.year))) - .y((d) => this.y(d.value)); - let entries = d3.nest().key((d) => d.name); + .x((d) => x(parseYear(d.year))) + .y((d) => y(d.value)); - this.svg = this.container - .append('svg') - .attr('width', this.width + this.margin.left + this.margin.right) - .attr('height', this.height + this.margin.top + this.margin.bottom); + let entries = d3.nest().key((d) => d.name); - this.svg.append('rect').attr('width', '100%').attr('height', '100%').attr('fill', 'white'); + let linedata = entries.entries(subdata); + + // Add lines + svg + .selectAll('.line') + .data(linedata) + .enter() + .append('path') + .attr('class', 'line') + .style('fill', 'none') + .style('stroke', (d) => (d.values[0].plan == 'plan' ? line_color : scen_line_color)) + .style('stroke-width', '2px') + .attr('id', (d) => d.key) + .attr('d', (d) => line(d.values)); + + // Add dots for datapoints + svg + .selectAll('.dot') + .data(subdata) + .enter() + .append('circle') + .attr('class', 'dot') + .attr('r', 5) + .style('stroke', '#fff') + .style('fill', (d) => (d.plan == 'plan' ? line_color : scen_line_color)) + .attr('name', (d) => d.key) + .attr('x_value', (d) => d.year) + .attr('y_value', (d) => d.value) + .attr('cx', (d) => x(parseYear(d.year))) + .attr('cy', (d) => y(d.value)) + .on('mouseover', mouseover) + .on('mousemove', mousemove) + .on('mouseout', mouseout); + + const tooltip = d3 + .select(container_div) + .append('div') + .attr('class', 'd3tooltip') + .style('display', 'none'); + + // Add x axis + const num_of_years = + 1 + Math.abs(x.domain().reduce((a, b) => a.getFullYear() - b.getFullYear())); + let tick_labels = d3 + .map(subdata, (d) => d.year) + .keys() + .slice(0, Math.min(num_of_years, 5) + 1); + tick_labels[0] = '31-Dec-' + tick_labels[0] + '*'; - this.svg = this.svg + svg .append('g') - .attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')'); - - this.tooltip = this.container.append('div').attr('class', 'd3tooltip').style('display', 'none'); + .attr('class', 'axis') + .attr('transform', 'translate(0,' + (height - marginBottom) + ')') + .call( + d3 + .axisBottom(x) + .ticks(Math.min(num_of_years, 5)) + .tickFormat((d, i) => tick_labels[i]) + ); - this.svg + // Add y axis + svg .append('g') - .attr('class', 'xaxis') - .attr('transform', 'translate(0,' + this.height + ')'); - - this.svg.append('g').attr('class', 'yaxis'); + .attr('class', 'axis') + .attr('transform', 'translate(' + marginLeft + ',0)') + .call(d3.axisLeft(y).ticks(6).tickFormat(d3.format('.3s'))); - this.svg + // Add y axis title + svg .append('text') - .attr('class', 'xtitle') + .attr('class', 'axis') .attr( 'transform', - 'translate(' + this.width / 2 + ' ,' + (this.height + this.margin.top + 70) + ')' + `translate(${marginLeft / 2},${(height - marginBottom) / 2 + 15}) rotate(-90)` ) - .style('text-anchor', 'middle'); - - this.svg - .append('text') - .attr('class', 'ytitle') - .attr('transform', 'rotate(-90)') - .attr('y', 0 - this.margin.left) - .attr('x', 0 - this.height / 2) + .attr('x', 0) + .attr('y', 0) .attr('dy', '1em') - .style('text-anchor', 'middle'); + .style('text-anchor', 'middle') + .text(subdata.map((d) => d.unit_translation)[0]); - this.svg - .append('text') - .attr('class', 'ysubtitle') - .attr('transform', 'rotate(-90)') - .attr('y', 0 - this.margin.left + 15) - .attr('x', 0 - this.height / 2) - .attr('dy', '1em') - .style('text-anchor', 'middle'); - - let footnote_group = this.svg + // Add footnote + svg .append('g') .attr('class', 'footnote') .attr( 'transform', 'translate(' + - (this.width + this.margin.left) + + (width - marginRight / 2) + ',' + - (this.height + this.margin.bottom / 2) + + (height - marginTop - (5 * marginBottom) / 12) + ')' - ); + ) + .append('text') + .attr('x', 0) + .attr('y', 0) + .style('text-anchor', 'end') + .style('alignment-baseline', 'central') + .style('font-size', '1.2em') + .text(footnote); + + // Add legend + let legend_data = [port_label, subdata.map((d) => d.scenario)[0] + ' ' + scen_label]; - let legend_group = this.svg + let legend = svg .append('g') .attr('class', 'legend') - .attr('transform', 'translate(' + (this.width + 10) + ',' + (50 + this.margin.top) + ')'); - - let chart = this; - sector_selector.dispatchEvent(new Event('change')); + .attr('transform', 'translate(' + marginLeft + ',' + (height - marginBottom / 3) + ')') + .selectAll('g') + .data(legend_data); + + legend + .enter() + .append('line') + .attr('x1', (d) => (d == port_label ? 0 : 200)) + .attr('x2', (d) => (d == port_label ? 60 : 260)) + .attr('y1', 0) + .attr('y2', 0) + .style('stroke-width', 2) + .style('stroke', (d) => (d == port_label ? line_color : scen_line_color)); + + legend + .enter() + .append('circle') + .attr('r', 5) + .attr('cx', (d) => (d == port_label ? 30 : 230)) + .attr('cy', 0) + .style('stroke', '#fff') + .style('fill', (d) => (d == port_label ? line_color : scen_line_color)); + + legend + .enter() + .append('text') + .attr('x', (d) => (d == port_label ? 70 : 270)) + .attr('y', 3) + .text((d) => d) + .attr('alignment-baseline', 'middle'); function mouseover(d) { - chart.tooltip + tooltip .html( (d.plan == 'plan' ? port_label : scen_label) + '
' + @@ -219,201 +240,11 @@ export class time_line { } function mousemove() { - chart.tooltip - .style('left', d3.event.pageX + 10 + 'px') - .style('top', d3.event.pageY - 20 + 'px'); + tooltip.style('left', d3.event.pageX + 10 + 'px').style('top', d3.event.pageY - 20 + 'px'); } function mouseout() { - chart.tooltip.style('display', 'none'); - } - - function change_class() { - let selected_class = class_selector.value; - let selected_sector = sector_selector.value; - - let selected_allocation = - typeof allocation_selector.value === 'undefined' - ? 'portfolio_weight' - : allocation_selector.value; - let selected_market = - typeof market_selector.value === 'undefined' ? 'Global' : market_selector.value; - - let subdata = data.filter((d) => d.asset_class_translation == selected_class); - - // reset the allocation selector for the selected asset class - allocation_selector.length = 0; - - let allocation_names = d3.map(subdata, (d) => d.allocation_translation).keys(); - allocation_names.forEach((allocation_name) => - allocation_selector.add(new Option(allocation_name, allocation_name)) - ); - allocation_selector.options[ - Math.max(0, allocation_names.indexOf(selected_allocation)) - ].selected = 'selected'; - //resize_inline_text_dropdown(null, allocation_selector); - - subdata = subdata.filter((d) => d.allocation_translation == allocation_selector.value); - - // reset the sector selector - sector_selector.length = 0; - - let sector_names = d3 - .map(subdata, (d) => d.sector_translation) - .keys() - .sort(); - sector_names.forEach((sector_name) => - sector_selector.add(new Option(sector_name, sector_name)) - ); - let disabled = sector_names.map( - (sector) => data.filter((d) => d.sector_translation == sector).map((d) => d.disabled)[0] - ); - disabled.map((d, i) => (sector_selector.options[i].disabled = d)); - sector_selector.options[ - Math.max(sector_names.indexOf(selected_sector), disabled.indexOf(false)) - ].selected = 'selected'; - //resize_inline_text_dropdown(null, sector_selector); - - subdata = subdata.filter((d) => d.sector_translation == sector_selector.value); - - // reset the market selector for the selected asset class - market_selector.length = 0; - - let market_names = d3.map(subdata, (d) => d.equity_market_translation).keys(); - market_names.forEach((market_name) => - market_selector.add(new Option(market_name, market_name)) - ); - market_selector.options[Math.max(0, market_names.indexOf(selected_market))].selected = - 'selected'; - //resize_inline_text_dropdown(null, market_selector); - - update(); - } - - function update() { - let subset = data.filter((d) => d.asset_class_translation == class_selector.value); - subset = subset.filter((d) => d.sector_translation == sector_selector.value); - subset = subset.filter((d) => d.allocation_translation == allocation_selector.value); - subset = subset.filter((d) => d.equity_market_translation == market_selector.value); - - let linedata = entries.entries(subset); - chart.x.domain(d3.extent(subset, (d) => parseYear(d.year))); - chart.y.domain(d3.extent(subset, (d) => d.value)).nice(); - - let lines_select = chart.svg.selectAll('.line').data(linedata); - lines_select.exit().remove(); - lines_select - .enter() - .append('path') - .attr('class', 'line') - .style('fill', 'none') - .style('stroke', (d) => (d.values[0].plan == 'plan' ? line_color : scen_line_color)) - .style('stroke-width', '2px') - .attr('id', (d) => d.key) - .attr('d', (d) => line(d.values)); - lines_select - .transition() - .attr('id', (d) => d.key) - .attr('d', (d) => line(d.values)); - - let dots_select = chart.svg.selectAll('.dot').data(subset); - dots_select.exit().remove(); - dots_select - .enter() - .append('circle') - .attr('class', 'dot') - .attr('r', 5) - .style('stroke', '#fff') - .style('fill', (d) => (d.plan == 'plan' ? line_color : scen_line_color)) - .attr('name', (d) => d.key) - .attr('x_value', (d) => d.year) - .attr('y_value', (d) => d.value) - .attr('cx', (d) => chart.x(parseYear(d.year))) - .attr('cy', (d) => chart.y(d.value)) - .on('mouseover', mouseover) - .on('mousemove', mousemove) - .on('mouseout', mouseout); - dots_select - .transition() - .attr('name', (d) => d.key) - .attr('x_value', (d) => d.year) - .attr('y_value', (d) => d.value) - .attr('cx', (d) => chart.x(parseYear(d.year))) - .attr('cy', (d) => chart.y(d.value)); - - //axes - const num_of_years = - 1 + Math.abs(chart.x.domain().reduce((a, b) => a.getFullYear() - b.getFullYear())); - let tick_labels = d3 - .map(subset, (d) => d.year) - .keys() - .slice(0, Math.min(num_of_years, 5) + 1); - tick_labels[0] = '31-Dec-' + tick_labels[0] + '*'; - - chart.svg - .select('.xaxis') - .transition() - .call( - d3 - .axisBottom(chart.x) - .ticks(Math.min(num_of_years, 5)) - .tickFormat((d, i) => tick_labels[i]) - ); - chart.svg - .select('.yaxis') - .transition() - .call(d3.axisLeft(chart.y).ticks(6).tickFormat(d3.format('.3s'))); - - chart.svg.select('.xtitle').text(chart.xtitle); - chart.svg.select('.ytitle').text(sector_selector.value); - chart.svg.select('.ysubtitle').text(subset.map((d) => d.unit_translation)[0]); - - let legend_data = [port_label, subset.map((d) => d.scenario)[0] + ' ' + scen_label]; - - legend_group.selectAll('*').remove(); - - let legend = legend_group.selectAll(null).data(legend_data); - - legend - .enter() - .append('line') - .attr('x1', 0) - .attr('x2', 30) - .attr('y1', (d) => (d == port_label ? 0 : 20)) - .attr('y2', (d) => (d == port_label ? 0 : 20)) - .style('stroke-width', 2) - .style('stroke', (d) => (d == port_label ? line_color : scen_line_color)); - - legend - .enter() - .append('circle') - .attr('r', 5) - .attr('cx', 15) - .attr('cy', (d) => (d == port_label ? 0 : 20)) - .style('stroke', '#fff') - .style('fill', (d) => (d == port_label ? line_color : scen_line_color)); - - legend - .enter() - .append('text') - .attr('x', 35) - .attr('y', (d) => (d == port_label ? 0 : 20)) - .text((d) => d) - .style('font-size', '15px') - .attr('alignment-baseline', 'middle') - .style('fill', (d) => (d == port_label ? line_color : scen_line_color)); - - footnote_group - .selectAll(null) - .data([footnote]) - .enter() - .append('text') - .attr('x', 0) - .attr('y', 0) - .style('text-anchor', 'end') - .style('alignment-baseline', 'central') - .style('font-size', '0.7em') - .text((d) => d); + tooltip.style('display', 'none'); } } } diff --git a/src/routes/sector_view.svelte b/src/routes/sector_view.svelte index 05a56c8..1ec3e89 100644 --- a/src/routes/sector_view.svelte +++ b/src/routes/sector_view.svelte @@ -24,7 +24,7 @@ }); } - function fetchEmissionsData() { + function fetchEmissionIntensityPlot() { new time_line(document.querySelector('#emission-intensity-plot'), emissions_data); } @@ -39,6 +39,7 @@ const sector_selector = document.querySelector('#sector_selector'); sector_selector.addEventListener('change', function () { fetchTechmix(); + fetchEmissionIntensityPlot(); }); const asset_class_selector = document.querySelector('#asset_class_selector'); @@ -51,6 +52,7 @@ d.dispatchEvent(new Event('change')); }); fetchTechmix(); + fetchEmissionIntensityPlot(); }); const benchmark_selector = document.querySelector('#benchmark_selector'); benchmark_selector.addEventListener('change', function () { @@ -82,12 +84,17 @@ const equity_market_selector = document.querySelector('#equity_market_selector'); equity_market_selector.addEventListener('change', function () { fetchTechmix(); + fetchEmissionIntensityPlot(); + }); + const allocation_method_selector = document.querySelector('#allocation_method_selector'); + allocation_method_selector.addEventListener('change', function () { + fetchEmissionIntensityPlot(); }); } fetchTechmix(); fetchTrajectoryAlignmentData(); - fetchEmissionsData(); + fetchEmissionIntensityPlot(); addEventListeners(); }); @@ -237,8 +244,8 @@