PageSourceSearch

https://global-plastics-tool.org/js/report_sparklines.js

js global-plastics-tool.org collected 2026-09-26 01:14:22 UTC 13,459 bytes, 402 lines download raw bytes

1/**
2 * Logic for the sparklines appearing below the lage timeseries chart on the details tab.
3 *
4 * @license BSD, see LICENSE.md
5 */
6
7import {
8    ALL_ATTRS,
9    CONSUMPTION_ATTRS,
10    DISPLAY_STAGES,
11    DISPLAY_TYPES,
12    EOL_ATTRS,
13    HISTORY_START_YEAR,
14    MAX_YEAR,
15    PRODUCTION_ATTRS,
16    START_YEAR,
17    STANDARD_ATTR_NAMES,
18    getGlobalColors,
19} from "const";
20import {STRINGS} from "strings";
21
22/**
23 * Presenter for the details tab timeseries sparkline.
24 */
25class SparklinePresenter {
26    /**
27     * Create a new sparkline presenter to manage a new sparkline.
28     *
29     * @param attrName The name of the attribute (like sector) being displayed by this presenter.
30     * @param color The color the line of this presenter should use.
31     * @param targetDiv The div where this presenter should render.
32     * @param onYearChange Callback to invoke if the user changes the selected year.
33     */
34    constructor(attrName, color, targetDiv, onYearChange) {
35        const self = this;
36
37        self._attrName = attrName;
38        self._color = color;
39        self._targetDiv = targetDiv;
40        self._onYearChange = onYearChange;
41        self._d3Selection = self._getD3().select("#" + targetDiv.id);
42
43        self._initElements();
44    }
45
46    /**
47     * Update the display.
48     *
49     * @param stateSet The set of state Maps having gone through the policy simulation.
50     * @param selection Structure describing the selections made by the user like year and region.
51     */
52    update(stateSet, selection) {
53        const self = this;
54
55        const boundingBox = self._targetDiv.querySelector(".sparkline")
56            .getBoundingClientRect();
57        const totalWidth = boundingBox.width;
58        const totalHeight = boundingBox.height;
59
60        const states = stateSet.getAllWithInterventions();
61
62        const showHistorical = self._getShowHistory();
63        const isPercent = selection.getDisplayType() == DISPLAY_TYPES.percent;
64        const selectedYear = selection.getYear();
65        const forwardYear = Math.min(START_YEAR, selectedYear);
66        const startYear = showHistorical ? HISTORY_START_YEAR : forwardYear;
67        const endYear = MAX_YEAR;
68        const region = selection.getRegion();
69
70        const allStates = [stateSet.getAllWithInterventions(), stateSet.getAllBusinessAsUsuals()];
71
72        const allValues = allStates.map((x) => x.values())
73            .flatMap((x) => Array.from(x));
74
75        let attrs = null;
76        if (selection.getDisplayStage() == DISPLAY_STAGES.consumption) {
77            attrs = CONSUMPTION_ATTRS;
78        } else if (selection.getDisplayStage() == DISPLAY_STAGES.production) {
79            attrs = PRODUCTION_ATTRS;
80        } else {
81            attrs = EOL_ATTRS;
82        }
83
84        const yearValues = allValues.map((state) => {
85            const out = state.get("out").get(region);
86
87            const maxPositive = attrs.map((attr) => out.get(attr))
88                .filter((x) => x > 0)
89                .reduce((a, b) => a > b ? a : b, 0);
90
91            const maxNegative = attrs.map((attr) => out.get(attr))
92                .filter((x) => x < 0)
93                .map((x) => Math.abs(x))
94                .reduce((a, b) => a > b ? a : b, 0);
95
96            return Math.max(maxPositive, maxNegative);
97        });
98        const maxValueFloat = yearValues.reduce((a, b) => a > b ? a : b);
99        const maxValue = Math.round(maxValueFloat);
100        const minValue = selection.getShowBauDelta() ? -maxValue : 0;
101
102        const horizontalScale = self._getD3().scaleLinear()
103            .domain([startYear, endYear])
104            .range([35, totalWidth - 30]);
105
106        const verticalScale = self._getD3().scaleLinear()
107            .domain([minValue, maxValue])
108            .range([totalHeight - 20, 20]);
109
110        const pathGenerator = self._getD3().line()
111            .x((x) => horizontalScale(x["year"]))
112            .y((x) => verticalScale(x["value"]));
113
114        const getData = (target) => {
115            return Array.of(...target.entries())
116                .map((raw) => {
117                    return {
118                        "year": raw[0],
119                        "value": raw[1].get("out").get(region).get(self._attrName),
120                    };
121                })
122                .filter((datum) => datum["year"] >= startYear)
123                .filter((datum) => datum["year"] <= endYear);
124        };
125        attrs;
126        const updateLabels = () => {
127            const shiftLabels = (maxValue >= 1000 || !isPercent);
128            self._d3Selection.select(".start-year-label")
129                .html(startYear);
130
131            self._d3Selection.select(".end-year-label")
132                .html(endYear);
133
134            self._d3Selection.select(".min-value-label")
135                .html(minValue)
136                .attr("x", shiftLabels ? 28 : 20);
137
138            self._d3Selection.select(".max-value-label")
139                .html(maxValue)
140                .attr("x", shiftLabels ? 28 : 20);
141
142            self._d3Selection.select(".units-value-label")
143                .html(isPercent ? "%" : "Mt")
144                .attr("x", shiftLabels ? 30 : 20);
145        };
146
147        const updateIndicator = () => {
148            const selectedYear = selection.getYear();
149            const newX = horizontalScale(selectedYear);
150
151            self._d3Selection.select(".spark-year-indicator")
152                .transition()
153                .attr("x", newX);
154
155            self._d3Selection.select(".selected-year-label")
156                .html(selectedYear)
157                .transition()
158                .attr("x", newX);
159        };
160
161        const updateLines = () => {
162            const bauData = getData(stateSet.getAllBusinessAsUsuals());
163            const projectionData = getData(stateSet.getAllWithInterventions());
164
165            self._d3Selection.select(".bau-glyph")
166                .transition()
167                .attr("d", pathGenerator(bauData));
168
169            self._d3Selection.select(".projection-glyph")
170                .transition()
171                .attr("d", pathGenerator(projectionData));
172        };
173
174        const updateHoverTargets = () => {
175            const yearWidth = horizontalScale(2050) - horizontalScale(2049) - 2;
176            const years = [];
177            for (let year = startYear; year <= endYear; year++) {
178                years.push(year);
179            }
180
181            self._d3Selection.select(".spark-hover-targets")
182                .html("");
183
184            const effectiveHeight = totalHeight - 20 - 16;
185            self._d3Selection.select(".spark-hover-targets")
186                .selectAll(".spark-hover-target")
187                .data(years)
188                .enter()
189                .append("rect")
190                .classed("spark-hover-target", true)
191                .attr("x", (year) => horizontalScale(year) - yearWidth / 2)
192                .attr("width", yearWidth < 0 ? 0 : yearWidth)
193                .attr("y", 16)
194                .attr("height", effectiveHeight < 0 ? 0 : effectiveHeight)
195                .on("click", (event, year) => self._onYearChange(year));
196        };
197
198        const updateDescription = () => {
199            const value = stateSet.getAllWithInterventions().get(selectedYear)
200                .get("out")
201                .get(region)
202                .get(self._attrName);
203            const valueRounded = Math.round(value * 10) / 10;
204            const labelContent = [
205                STRINGS.get(self._attrName) + ":",
206                valueRounded,
207                isPercent ? "%" : "Mt",
208                "in",
209                selectedYear,
210                "with interventions.",
211            ].join(" ");
212            self._d3Selection.select(".sparkline")
213                .attr("aria-label", labelContent);
214        };
215
216        updateLabels();
217        updateIndicator();
218        updateLines();
219        updateHoverTargets();
220        updateDescription();
221    }
222
223    _initElements() {
224        const self = this;
225
226        const targetSvg = self._d3Selection.select(".sparkline");
227        targetSvg.html("");
228
229        const boundingBox = self._targetDiv.querySelector(".sparkline")
230            .getBoundingClientRect();
231        const totalWidth = boundingBox.width;
232        const totalHeight = boundingBox.height < 1 ? 100 : boundingBox.height;
233
234        targetSvg.append("text")
235            .attr("x", 35)
236            .attr("y", totalHeight - 12)
237            .classed("start-year-label", true)
238            .classed("year-label", true)
239            .classed("spark-label", true);
240
241        targetSvg.append("text")
242            .attr("x", totalWidth - 30)
243            .attr("y", totalHeight - 12)
244            .classed("end-year-label", true)
245            .classed("year-label", true)
246            .classed("spark-label", true);
247
248        targetSvg.append("text")
249            .attr("x", 20)
250            .attr("y", totalHeight - 20)
251            .classed("min-value-label", true)
252            .classed("value-label", true)
253            .classed("spark-label", true);
254
255        targetSvg.append("text")
256            .attr("x", 20)
257            .attr("y", totalHeight / 2)
258            .html("Mt")
259            .classed("units-value-label", true)
260            .classed("value-label", true)
261            .classed("spark-label", true);
262
263        targetSvg.append("text")
264            .attr("x", 20)
265            .attr("y", 20)
266            .classed("max-value-label", true)
267            .classed("value-label", true)
268            .classed("spark-label", true);
269
270        targetSvg.append("text")
271            .attr("x", 35)
272            .attr("y", 14)
273            .classed("selected-year-label", true)
274            .classed("year-label", true)
275            .classed("spark-label", true);
276
277        const effectiveHeight = totalHeight - 20 - 16;
278        targetSvg.append("rect")
279            .attr("x", 35)
280            .attr("y", 16)
281            .attr("width", 1)
282            .attr("height", effectiveHeight < 0 ? 0 : effectiveHeight)
283            .classed("spark-year-indicator", true);
284
285        targetSvg.append("path")
286            .classed("sparkline-glyph", true)
287            .classed("bau-glyph", true)
288            .style("stroke", self._color);
289
290        targetSvg.append("path")
291            .classed("sparkline-glyph", true)
292            .classed("projection-glyph", true)
293            .style("stroke", self._color);
294
295        targetSvg.append("g")
296            .classed("spark-hover-targets", true);
297    }
298
299    _getShowHistory() {
300        const self = this;
301
302        // Should move this out at some point.
303        const historicalCheck = document.getElementById("history-check");
304        return historicalCheck.checked;
305    }
306
307    _getD3() {
308        const self = this;
309        // eslint-disable-next-line no-undef
310        return d3;
311    }
312}
313
314
315/**
316 * Presenter which manages a collection of individual sparkline presenters.
317 */
318class SparklinesSet {
319    /**
320     * Create a new set of sparklines each with a sparkline presenter.
321     *
322     * @param targetDiv The div where the sparklines should be rendered.
323     * @param onYearChange Callback to invoke if the year selected by the user changes.
324     */
325    constructor(targetDiv, onYearChange) {
326        const self = this;
327
328        self._targetDiv = targetDiv;
329        self._onYearChange = onYearChange;
330
331        self._d3Selection = self._getD3().select("#" + self._targetDiv.id);
332
333        self._sparklines = self._buildSparklines(onYearChange);
334    }
335
336    /**
337     * Update the display.
338     *
339     * @param stateSet The set of state Maps having gone through the policy simulation.
340     * @param selection Structure describing the selections made by the user like year and region.
341     */
342    update(stateSet, selection) {
343        const self = this;
344
345        const attrsOptions = STANDARD_ATTR_NAMES;
346        const attrs = attrsOptions.get(selection.getDisplayStage());
347
348        self._d3Selection.selectAll(".sparkline-container")
349            .style("display", (x) => {
350                return attrs.indexOf(x) == -1 ? "none" : "block";
351            });
352
353        self._sparklines.forEach((sparkline) => sparkline.update(stateSet, selection));
354    }
355
356    _buildSparklines(onYearChange) {
357        const self = this;
358
359        // Color scales
360        const colorScales = new Map();
361        const globalColors = getGlobalColors();
362        [EOL_ATTRS, CONSUMPTION_ATTRS, PRODUCTION_ATTRS].forEach((attrList) => {
363            attrList.forEach((attr, i) => {
364                const color = globalColors[i];
365                colorScales.set(attr, color);
366            });
367        });
368
369        const bound = self._d3Selection.selectAll(".sparkline-container")
370            .data(ALL_ATTRS, (x) => x);
371
372        bound.exit().remove();
373
374        const newContainers = bound.enter()
375            .append("div")
376            .classed("sparkline-container", true)
377            .attr("id", (x) => "sparkline-container-" + x);
378
379        newContainers.append("div")
380            .classed("caption", true)
381            .html((x) => STRINGS.get(x));
382
383        newContainers.append("svg")
384            .classed("sparkline", true);
385
386        return ALL_ATTRS.map((attrName) => {
387            const attrDivId = "sparkline-container-" + attrName;
388            const attrDiv = document.getElementById(attrDivId);
389            const color = colorScales.get(attrName);
390            return new SparklinePresenter(attrName, color, attrDiv, onYearChange);
391        });
392    }
393
394    _getD3() {
395        const self = this;
396        // eslint-disable-next-line no-undef
397        return d3;
398    }
399}
400
401
402export {SparklinesSet};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.