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.