1/** 2 * This function returns a javascript object containing autotrack.js properties. 3 * 4 * These properties can be added to an element with jQuery: $(element).attr(props) 5 * 6 * See _includes/autotrack.html for parameter descriptions. 7 */ 8opensdg.autotrack = function(preset, category, action, label) { 9 var presets = {};var params = { 10 category: category, 11 action: action, 12 label: label 13 }; 14 if (presets[preset]) { 15 params = presets[preset]; 16 } 17 var obj = { 18 'data-on': 'click' 19 }; 20 if (params.category) { 21 obj['data-event-category'] = params.category; 22 } 23 if (params.action) { 24 obj['data-event-action'] = params.action; 25 } 26 if (params.label) { 27 obj['data-event-label'] = params.label; 28 } 29 30 return obj; 31}; 32/** 33 * TODO: 34 * Integrate with high-contrast switcher. 35 */ 36(function($) { 37 38 if (typeof L === 'undefined') { 39 return; 40 } 41 42 // Create the defaults once 43 var defaults = { 44 45 // Options for using tile imagery with leaflet. 46 tileURL: '[replace me]', 47 tileOptions: { 48 id: '[relace me]', 49 accessToken: '[replace me]', 50 attribution: '[replace me]', 51 }, 52 // Zoom limits. 53 minZoom: 5, 54 maxZoom: 10, 55 // Visual/choropleth considerations. 56 colorRange: chroma.brewer.BuGn, 57 noValueColor: '#66f0f0f0', 58 styleNormal: { 59 weight: 1, 60 opacity: 1, 61 color: '#888888', 62 fillOpacity: 0.7 63 }, 64 styleHighlighted: { 65 weight: 1, 66 opacity: 1, 67 color: '#111111', 68 fillOpacity: 0.7 69 }, 70 styleStatic: { 71 weight: 2, 72 opacity: 1, 73 fillOpacity: 0, 74 color: '#172d44', 75 dashArray: '5,5', 76 }, 77 }; 78 79 // Defaults for each map layer. 80 var mapLayerDefaults = { 81 min_zoom: 0, 82 max_zoom: 10, 83 subfolder: 'regions', 84 label: 'indicator.map', 85 staticBorders: false, 86 }; 87 88 function Plugin(element, options) { 89 90 this.element = element; 91 // Support colorRange map option in string format. 92 if (typeof options.mapOptions.colorRange === 'string') { 93 var colorRangeParts = options.mapOptions.colorRange.split('.'), 94 colorRange = window, 95 overrideColorRange = true; 96 for (var i = 0; i < colorRangeParts.length; i++) { 97 var colorRangePart = colorRangeParts[i]; 98 if (typeof colorRange[colorRangePart] !== 'undefined') { 99 colorRange = colorRange[colorRangePart]; 100 } 101 else { 102 overrideColorRange = false; 103 break; 104 } 105 } 106 options.mapOptions.colorRange = (overrideColorRange) ? colorRange : defaults.colorRange; 107 } 108 109 // Support multiple colorsets 110 if (Array.isArray(options.mapOptions.colorRange[0])) { 111 this.goalNumber = parseInt(options.indicatorId.slice(options.indicatorId.indexOf('_')+1,options.indicatorId.indexOf('-'))); 112 options.mapOptions.colorRange = options.mapOptions.colorRange[this.goalNumber-1]; 113 } 114 115 116 this.options = $.extend(true, {}, defaults, options.mapOptions); 117 this.mapLayers = []; 118 this.indicatorId = options.indicatorId; 119 this._precision = options.precision; 120 this.precisionItems = options.precisionItems; 121 this._decimalSeparator = options.decimalSeparator; 122 this._thousandsSeparator = options.thousandsSeparator; 123 this.currentDisaggregation = 0; 124 this.dataSchema = options.dataSchema; 125 this.viewHelpers = options.viewHelpers; 126 this.modelHelpers = options.modelHelpers; 127 this.chartTitles = options.chartTitles; 128 this.chartSubtitles = options.chartSubtitles; 129 this.proxy = options.proxy; 130 this.proxySerieses = options.proxySerieses; 131 this.startValues = options.startValues; 132 this.configObsAttributes = [{"field":"COMMENT_OBS_0","label":""},{"field":"COMMENT_OBS_1","label":""},{"field":"COMMENT_OBS_2","label":""}]; 133 this.allObservationAttributes = options.allObservationAttributes; 134 this._browserDecimalSeparator = this.viewHelpers.getBrowserDecimalSeparator(); 135 136 // Require at least one geoLayer. 137 if (!options.mapLayers || !options.mapLayers.length) { 138 console.log('Map disabled - please add "map_layers" in site configuration.'); 139 return; 140 } 141 142 // Apply geoLayer defaults. 143 for (var i = 0; i < options.mapLayers.length; i++) { 144 this.mapLayers[i] = $.extend(true, {}, mapLayerDefaults, options.mapLayers[i]); 145 } 146 147 // Sort the map layers according to zoom levels. 148 this.mapLayers.sort(function(a, b) { 149 if (a.min_zoom === b.min_zoom) { 150 return a.max_zoom - b.max_zoom; 151 } 152 return a.min_zoom - b.min_zoom; 153 }); 154 155 this._defaults = defaults; 156 this._name = 'sdgMap'; 157 158 this.init(); 159 } 160 161 Plugin.prototype = { 162 163 // Update title. 164 updateTitle: function() { 165 if (!this.modelHelpers) { 166 return; 167 } 168 var currentSeries = this.disaggregationControls.getCurrentSeries(), 169 currentUnit = this.disaggregationControls.getCurrentUnit(), 170 newTitle = null; 171 newSubtitle = null; 172 if (this.modelHelpers.GRAPH_TITLE_FROM_SERIES) { 173 newTitle = currentSeries; 174 } 175 else { 176 var currentTitle = $('#map-heading').text(); 177 var currentSubtitle = $('#map-subheading').text(); 178 newTitle = this.modelHelpers.getChartTitle(currentTitle, this.chartTitles, currentUnit, currentSeries); 179 newSubtitle = this.modelHelpers.getChartTitle(currentSubtitle, this.chartSubtitles, currentUnit, currentSeries); 180 } 181 if (newTitle) { 182 if (this.proxy === 'proxy' || this.proxySerieses.includes(currentSeries)) { 183 newTitle += ' ' + this.viewHelpers.PROXY_PILL; 184 } 185 $('#map-heading').html(newTitle); 186 } 187 if (newSubtitle) { 188 $('#map-subheading').text(newSubtitle); 189 } 190 }, 191 192 // Update footer fields. 193 updateFooterFields: function() { 194 if (!this.viewHelpers) { 195 return; 196 } 197 var currentSeries = this.disaggregationControls.getCurrentSeries(), 198 currentUnit = this.disaggregationControls.getCurrentUnit(); 199 this.viewHelpers.updateSeriesAndUnitElements(currentSeries, currentUnit); 200 this.viewHelpers.updateUnitElements(currentUnit); 201 }, 202 203 // Update precision. 204 updatePrecision: function() { 205 if (!this.modelHelpers) { 206 return; 207 } 208 var currentSeries = this.disaggregationControls.getCurrentSeries(), 209 currentUnit = this.disaggregationControls.getCurrentUnit(); 210 this._precision = this.modelHelpers.getPrecision(this.precisionItems, currentUnit, currentSeries); 211 }, 212 213 // Zoom to a feature. 214 zoomToFeature: function(layer) { 215 this.map.fitBounds(layer.getBounds()); 216 }, 217 218 // Build content for a tooltip. 219 getTooltipContent: function(feature) { 220 var tooltipContent = feature.properties.name; 221 var tooltipData = this.getData(feature.properties); 222 var plugin = this; 223 if (typeof tooltipData === 'number') { 224 tooltipContent += ': ' + this.alterData(tooltipData); 225 } 226 if (feature.properties.observation_attributes) { 227 var obsAtts = feature.properties.observation_attributes[plugin.currentDisaggregation][plugin.currentYear], 228 footnoteNumbers = []; 229 if (obsAtts) { 230 var tooltipAtts = []
231 Object.keys(obsAtts).forEach(function(field) { 232 if (obsAtts[field]) { 233 tooltipAtts.push(obsAtts[field]); 234 //var hashKey = field + '|' + obsAtts[field]; 235 //var footnoteNumber = plugin.allObservationAttributes[hashKey].footnoteNumber; 236 //footnoteNumbers.push(plugin.viewHelpers.getObservationAttributeFootnoteSymbol(footnoteNumber)); 237 } 238 }); 239 if (tooltipAtts.length > 0) { 240 tooltipContent += ' [' + tooltipAtts.join(', ') + ']'; 241 } 242 } 243 } 244 245 return tooltipContent; 246 }, 247 248 // Update a tooltip. 249 updateTooltip: function(layer) { 250 if (layer.getTooltip()) { 251 var tooltipContent = this.getTooltipContent(layer.feature); 252 layer.setTooltipContent(tooltipContent); 253 } 254 }, 255 256 // Create tooltip. 257 createTooltip: function(layer) { 258 if (!layer.getTooltip()) { 259 var tooltipContent = this.getTooltipContent(layer.feature); 260 layer.bindTooltip(tooltipContent, { 261 permanent: true, 262 }).addTo(this.map); 263 } 264 }, 265 266 // Select a feature. 267 highlightFeature: function(layer) { 268 // Abort if the layer is not on the map. 269 if (!this.map.hasLayer(layer)) { 270 return; 271 } 272 // Update the style. 273 layer.setStyle(this.options.styleHighlighted); 274 // Add a tooltip if not already there. 275 this.createTooltip(layer); 276 if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) { 277 layer.bringToFront(); 278 } 279 this.updateStaticLayers(); 280 }, 281 282 // Unselect a feature. 283 unhighlightFeature: function(layer) { 284 285 // Reset the feature's style. 286 layer.setStyle(this.options.styleNormal); 287 288 // Remove the tooltip if necessary. 289 if (layer.getTooltip()) { 290 layer.unbindTooltip(); 291 } 292 293 // Make sure other selections are still highlighted. 294 var plugin = this; 295 this.selectionLegend.selections.forEach(function(selection) { 296 plugin.highlightFeature(selection); 297 }); 298 }, 299 300 // Get all of the GeoJSON layers. 301 getAllLayers: function() { 302 return L.featureGroup(this.dynamicLayers.layers); 303 }, 304 305 // Get only the visible GeoJSON layers. 306 getVisibleLayers: function() { 307 // Unfortunately relies on an internal of the ZoomShowHide library. 308 return this.dynamicLayers._layerGroup; 309 }, 310 311 updateStaticLayers: function() { 312 // Make sure the static borders are always visible. 313 this.staticLayers._layerGroup.eachLayer(function(layer) { 314 layer.bringToFront(); 315 }); 316 }, 317 318 // Update the colors of the Features on the map. 319 updateColors: function() { 320 var plugin = this; 321 this.getAllLayers().eachLayer(function(layer) { 322 layer.setStyle(function(feature) { 323 return { 324 fillColor: plugin.getColor(feature.properties), 325 } 326 }); 327 }); 328 }, 329 330 // Update the tooltips of the selected Features on the map. 331 updateTooltips: function() { 332 var plugin = this; 333 this.selectionLegend.selections.forEach(function(selection) { 334 plugin.updateTooltip(selection); 335 }); 336 }, 337 338 // Alter data before displaying it. 339 alterData: function(value) { 340 opensdg.dataDisplayAlterations.forEach(function(callback) { 341 value = callback(value); 342 }); 343 344 if (typeof value !== 'number') { 345 if (this._precision || this._precision === 0) { 346 value = Number.parseFloat(value).toFixed(this._precision); 347 } 348 if (this._decimalSeparator) { 349 value = value.toString().replace('.', this._decimalSeparator); 350 } 351 } 352 else { 353 var localeOpts = {}; 354 if (this._precision || this._precision === 0) { 355 localeOpts.minimumFractionDigits = this._precision; 356 localeOpts.maximumFractionDigits = this._precision; 357 } 358 value = value.toLocaleString(opensdg.language, localeOpts); 359 // Still use the custom decimal separator if it is there. 360 if (this._decimalSeparator) { 361 value = value.toString().replace(this._browserDecimalSeparator, this._decimalSeparator); 362 } 363 } 364 return value; 365 }, 366 367 // Get the data from a feature's properties, according to the current year. 368 getData: function(props) { 369 var ret = false;
370 if (props.values && props.values.length && this.currentDisaggregation < props.values.length) { 371 var value = props.values[this.currentDisaggregation][this.currentYear]; 372 if (typeof value === 'number') { 373 ret = opensdg.dataRounding(value, { indicatorId: this.indicatorId }); 374 } 375 if (typeof value === 'string') { 376 //get rid of observationAttributes 377 value = value.replace(value.match(/\[(.*?)\]/)[0],'')/1; 378 ret = opensdg.dataRounding(value, { indicatorId: this.indicatorId }); 379 } 380 } 381 return ret; 382 }, 383 384 // Choose a color for a GeoJSON feature. 385 getColor: function(props) { 386 var data = this.getData(props); 387 if (data) { 388 return this.colorScale(data).hex(); 389 } 390 else { 391 return this.options.noValueColor; 392 } 393 }, 394 395 // Set (or re-set) the choropleth color scale. 396 setColorScale: function() { 397 this.colorScale = chroma.scale(this.options.colorRange) 398 .domain(this.valueRanges[this.currentDisaggregation]) 399 .classes(this.options.colorRange.length); 400 }, 401 402 // Get the (long) URL of a geojson file, given a particular subfolder. 403 getGeoJsonUrl: function(subfolder) { 404 var fileName = this.indicatorId + '.geojson'; 405 return [opensdg.remoteDataBaseUrl, 'geojson', subfolder, fileName].join('/'); 406 }, 407 408 getYearSlider: function() { 409 var plugin = this, 410 years = plugin.years[plugin.currentDisaggregation]; 411 return L.Control.yearSlider({ 412 years: years, 413 yearChangeCallback: function(e) { 414 plugin.currentYear = years[e.target._currentTimeIndex]; 415 plugin.updateColors(); 416 plugin.updateTooltips(); 417 plugin.selectionLegend.update(); 418 } 419 }); 420 }, 421 422 replaceYearSlider: function() { 423 var newSlider = this.getYearSlider(); 424 var oldSlider = this.yearSlider; 425 this.map.addControl(newSlider); 426 this.map.removeControl(oldSlider); 427 this.yearSlider = newSlider; 428 $(this.yearSlider.getContainer()).insertAfter($(this.disaggregationControls.getContainer())); 429 this.yearSlider._timeDimension.setCurrentTimeIndex(this.yearSlider._timeDimension.getCurrentTimeIndex()); 430 }, 431 432 // Initialize the map itself. 433 init: function() { 434 435 // Create the map. 436 this.map = L.map(this.element, { 437 minZoom: this.options.minZoom, 438 maxZoom: this.options.maxZoom, 439 zoomControl: false, 440 }); 441 this.map.setView([0, 0], 0); 442 this.dynamicLayers = new ZoomShowHide(); 443 this.dynamicLayers.addTo(this.map); 444 this.staticLayers = new ZoomShowHide(); 445 this.staticLayers.addTo(this.map); 446 447 // Add scale. 448 this.map.addControl(L.control.scale({position: 'bottomright'})); 449 450 // Add tile imagery. 451 if (this.options.tileURL && this.options.tileURL !== 'undefined' && this.options.tileURL != '') { 452 L.tileLayer(this.options.tileURL, this.options.tileOptions).addTo(this.map); 453 } 454 455 // Because after this point, "this" rarely works. 456 var plugin = this; 457 458 // Below we'll be figuring out the min/max values and available years. 459 var minimumValues = [], 460 maximumValues = [], 461 availableYears = []; 462 463 // At this point we need to load the GeoJSON layer/s. 464 var geoURLs = this.mapLayers.map(function(item) { 465 return $.getJSON(plugin.getGeoJsonUrl(item.subfolder)); 466 }); 467 $.when.apply($, geoURLs).done(function() { 468 469 // Apparently "arguments" can either be an array of responses, or if 470 // there was only one response, the response itself. This behavior is 471 // odd and should be investigated. In the meantime, a workaround is a 472 // blunt check to see if it is a single response. 473 var geoJsons = arguments; 474 // In a response, the second element is a string (like 'success') so 475 // check for that here to identify whether it is a response. 476 if (arguments.length > 1 && typeof arguments[1] === 'string') { 477 // If so, put it into an array, to match the behavior when there are 478 // multiple responses. 479 geoJsons = [geoJsons]; 480 } 481 482 // Do a quick loop through to see which layers actually have data. 483 for (var i = 0;
483 i < geoJsons.length; i++) { 484 var layerHasData = true; 485 if (typeof geoJsons[i][0].features === 'undefined') { 486 layerHasData = false; 487 } 488 else if (!plugin.featuresShouldDisplay(geoJsons[i][0].features)) { 489 layerHasData = false; 490 } 491 if (layerHasData === false) { 492 // If a layer has no data, we'll be skipping it. 493 plugin.mapLayers[i].skipLayer = true; 494 // We also need to alter a sibling layer's min_zoom or max_zoom. 495 var hasLayerBefore = i > 0; 496 var hasLayerAfter = i < (geoJsons.length - 1); 497 if (hasLayerBefore) { 498 plugin.mapLayers[i - 1].max_zoom = plugin.mapLayers[i].max_zoom; 499 } 500 else if (hasLayerAfter) { 501 plugin.mapLayers[i + 1].min_zoom = plugin.mapLayers[i].min_zoom; 502 } 503 } 504 else { 505 plugin.mapLayers[i].skipLayer = false; 506 } 507 } 508 509 for (var i = 0; i < geoJsons.length; i++) { 510 if (plugin.mapLayers[i].skipLayer) { 511 continue; 512 } 513 // First add the geoJson as static (non-interactive) borders. 514 if (plugin.mapLayers[i].staticBorders) { 515 var staticLayer = L.geoJson(geoJsons[i][0], { 516 style: plugin.options.styleStatic, 517 interactive: false, 518 }); 519 // Static layers should start appear when zooming past their dynamic 520 // layer, and stay visible after that. 521 staticLayer.min_zoom = plugin.mapLayers[i].max_zoom + 1; 522 staticLayer.max_zoom = plugin.options.maxZoom; 523 plugin.staticLayers.addLayer(staticLayer); 524 } 525 // Now go on to add the geoJson again as choropleth dynamic regions. 526 var geoJson = geoJsons[i][0] 527 var layer = L.geoJson(geoJson, { 528 style: plugin.options.styleNormal, 529 onEachFeature: onEachFeature, 530 }); 531 // Set the "boundaries" for when this layer should be zoomed out of. 532 layer.min_zoom = plugin.mapLayers[i].min_zoom; 533 layer.max_zoom = plugin.mapLayers[i].max_zoom; 534 // Listen for when this layer gets zoomed in or out of. 535 layer.on('remove', zoomOutHandler); 536 layer.on('add', zoomInHandler); 537 // Save the GeoJSON object for direct access (download) later. 538 layer.geoJsonObject = geoJson; 539 // Add the layer to the ZoomShowHide group. 540 plugin.dynamicLayers.addLayer(layer); 541 542 // Add a download button below the map. 543 var downloadLabel = translations.t(plugin.mapLayers[i].label) 544 var downloadButton = $('<a></a>') 545 .attr('href', plugin.getGeoJsonUrl(plugin.mapLayers[i].subfolder)) 546 .attr('download', '') 547 .attr('class', 'btn btn-primary btn-download') 548 .attr('title', translations.indicator.download_geojson_title + ' - ' + downloadLabel) 549 .attr('aria-label', translations.indicator.download_geojson_title + ' - ' + downloadLabel) 550 .text(translations.indicator.download_geojson + ' - ' + downloadLabel); 551 $(plugin.element).parent().append(downloadButton); 552 553 // Keep track of the minimums and maximums. 554 _.each(geoJson.features, function(feature) { 555 if (feature.properties.values && feature.properties.values.length > 0) { 556 for (var valueIndex = 0; valueIndex < feature.properties.values.length; valueIndex++) { 557 var validEntries = _.reject(Object.entries(feature.properties.values[valueIndex]), function(entry) { 558 return isMapValueInvalid(entry[1]); 559 }); 560 var validKeys = validEntries.map(function(entry) { 561 return entry[0]; 562 }); 563 var validValues = validEntries.map(function(entry) { 564 return entry[1]; 565 }); 566 if (availableYears.length <= valueIndex) { 567 availableYears.push([]); 568 } 569 availableYears[valueIndex] = availableYears[valueIndex].concat(validKeys); 570 if (minimumValues.length <= valueIndex) { 571 minimumValues.push([]); 572 maximumValues.push([]); 573 } 574 minimumValues[valueIndex].push(_.min(validValues)); 575 maximumValues[valueIndex].push(_.max(validValues)); 576 } 577 } 578 }); 579 } 580 581 // Calculate the ranges of values, years and colors. 582 function isMapValueInvalid(val) { 583 return _.isNaN(val) || val === ''; 584 } 585 586 plugin.valueRanges = []; 587 for (var valueIndex = 0; valueIndex < minimumValues.length; valueIndex++) { 588 minimumValues[valueIndex] = _.reject(minimumValues[valueIndex], isMapValueInvalid); 589 maximumValues[valueIndex] = _.reject(maximumValues[valueIndex], isMapValueInvalid); 590 plugin.valueRanges[valueIndex] = [_.min(minimumValues[valueIndex]), _.max(maximumValues[valueIndex])]; 591 } 592 plugin.setColorScale(); 593 594 plugin.years = availableYears.map(function(yearsForIndex) { 595 return _.uniq(yearsForIndex).sort(); 596 }); 597 //Start the map with the most recent year 598 plugin.currentYear = plugin.years[plugin.currentDisaggregation].slice(-1)[0]; 599 plugin.currentYear = plugin.years.slice(-1)[0]; 600 601 // And we can now update the colors. 602 plugin.updateColors(); 603 604 // Add zoom control. 605 plugin.zoomHome = L.Control.zoomHome({ 606 zoomInTitle: translations.indicator.map_zoom_in, 607 zoomOutTitle: translations.indicator.map_zoom_out, 608 zoomHomeTitle: translations.indicator.map_zoom_home, 609 }); 610 plugin.map.addControl(plugin.zoomHome); 611 612 // Add full-screen functionality. 613 plugin.map.addControl(new L.Control.FullscreenAccessible({ 614 title: { 615 'false': translations.indicator.map_fullscreen, 616 'true': translations.indicator.map_fullscreen_exit, 617 }, 618 })); 619 620 // Add the year slider. 621 plugin.yearSlider = plugin.getYearSlider(); 622 plugin.map.addControl(plugin.yearSlider); 623 624 // Add the selection legend. 625 plugin.selectionLegend = L.Control.selectionLegend(plugin);
626 plugin.map.addControl(plugin.selectionLegend); 627 628 // Add the disaggregation controls. 629 plugin.disaggregationControls = L.Control.disaggregationControls(plugin); 630 plugin.map.addControl(plugin.disaggregationControls); 631 if (plugin.disaggregationControls.needsMapUpdate) { 632 plugin.disaggregationControls.updateMap(); 633 } 634 else { 635 plugin.updateTitle(); 636 plugin.updatePrecision(); 637 } 638 639 // Add the search feature. 640 plugin.searchControl = new L.Control.SearchAccessible({ 641 textPlaceholder: 'Search map', 642 autoCollapseTime: 7000, 643 layer: plugin.getAllLayers(), 644 propertyName: 'name', 645 marker: false, 646 moveToLocation: function(latlng) { 647 plugin.zoomToFeature(latlng.layer); 648 if (!plugin.selectionLegend.isSelected(latlng.layer)) { 649 plugin.highlightFeature(latlng.layer); 650 plugin.selectionLegend.addSelection(latlng.layer); 651 } 652 }, 653 }); 654 plugin.map.addControl(plugin.searchControl); 655 // The search plugin messes up zoomShowHide, so we have to reset that 656 // with this hacky method. Is there a better way? 657 var zoom = plugin.map.getZoom(); 658 plugin.map.setZoom(plugin.options.maxZoom); 659 plugin.map.setZoom(zoom); 660 661 // Hide the loading image. 662 $('.map-loading-image').hide(); 663 // Make the map unfocusable. 664 $('#map').removeAttr('tabindex'); 665 666 // The list of handlers to apply to each feature on a GeoJson layer. 667 function onEachFeature(feature, layer) { 668 if (plugin.featureShouldDisplay(feature)) { 669 layer.on('click', clickHandler); 670 layer.on('mouseover', mouseoverHandler); 671 layer.on('mouseout', mouseoutHandler); 672 } 673 } 674 // Event handler for click/touch. 675 function clickHandler(e) { 676 var layer = e.target; 677 if (plugin.selectionLegend.isSelected(layer)) { 678 plugin.selectionLegend.removeSelection(layer); 679 plugin.unhighlightFeature(layer); 680 } 681 else { 682 plugin.selectionLegend.addSelection(layer); 683 plugin.highlightFeature(layer); 684 plugin.zoomToFeature(layer); 685 } 686 } 687 // Event handler for mouseover. 688 function mouseoverHandler(e) { 689 var layer = e.target; 690 if (!plugin.selectionLegend.isSelected(layer)) { 691 plugin.highlightFeature(layer); 692 } 693 } 694 // Event handler for mouseout. 695 function mouseoutHandler(e) { 696 var layer = e.target; 697 if (!plugin.selectionLegend.isSelected(layer)) { 698 plugin.unhighlightFeature(layer); 699 } 700 } 701 // Event handler for when a geoJson layer is zoomed out of. 702 function zoomOutHandler(e) { 703 var geoJsonLayer = e.target; 704 // For desktop, we have to make sure that no features remain 705 // highlighted, as they might have been highlighted on mouseover. 706 geoJsonLayer.eachLayer(function(layer) { 707 if (!plugin.selectionLegend.isSelected(layer)) { 708 plugin.unhighlightFeature(layer); 709 } 710 }); 711 plugin.updateStaticLayers(); 712 if (plugin.disaggregationControls) { 713 plugin.disaggregationControls.update(); 714 } 715 } 716 // Event handler for when a geoJson layer is zoomed into. 717 function zoomInHandler(e) { 718 plugin.updateStaticLayers(); 719 if (plugin.disaggregationControls) { 720 plugin.disaggregationControls.update(); 721 } 722 } 723 }); 724 725 // Certain things cannot be done until the map is visible. Because our 726 // map is in a tab which might not be visible, we have to postpone those 727 // things until it becomes visible. 728 if ($('#map').is(':visible')) { 729 finalMapPreparation(); 730 } 731 else { 732 $('#tab-mapview').parent().click(finalMapPreparation); 733 } 734 function finalMapPreparation() { 735 // Update the series/unit stuff in case it changed 736 // while on the chart/table. 737 plugin.updateTitle(); 738 plugin.updateFooterFields(); 739 plugin.updatePrecision(); 740 // The year slider does not seem to be correct unless we refresh it here. 741 plugin.yearSlider._timeDimension.setCurrentTimeIndex(plugin.yearSlider._timeDimension.getCurrentTimeIndex()); 742 // Delay other things to give time for browser to do stuff. 743 setTimeout(function() { 744 $('#map #loader-container').hide();
745 // Leaflet needs "invalidateSize()" if it was originally rendered in a 746 // hidden element. So we need to do that when the tab is clicked. 747 plugin.map.invalidateSize(); 748 // Also zoom in/out as needed. 749 plugin.map.fitBounds(plugin.getVisibleLayers().getBounds()); 750 // Set the home button to return to that zoom. 751 plugin.zoomHome.setHomeBounds(plugin.getVisibleLayers().getBounds()); 752 // Limit the panning to what we care about. 753 plugin.map.setMaxBounds(plugin.getVisibleLayers().getBounds()); 754 // Make sure the info pane is not too wide for the map. 755 var $legendPane = $('.selection-legend.leaflet-control'); 756 var widthPadding = 20; 757 var maxWidth = $('#map').width() - widthPadding; 758 if ($legendPane.width() > maxWidth) { 759 $legendPane.width(maxWidth); 760 } 761 // Make sure the map is not too high. 762 var heightPadding = 75; 763 var minHeight = 400; 764 var maxHeight = $(window).height() - heightPadding; 765 if (maxHeight < minHeight) { 766 maxHeight = minHeight; 767 } 768 if ($('#map').height() > maxHeight) { 769 $('#map').height(maxHeight); 770 } 771 }, 500); 772 }; 773 }, 774 775 featureShouldDisplay: function(feature) { 776 var display = true; 777 display = display && typeof feature.properties.name !== 'undefined'; 778 display = display && typeof feature.properties.geocode !== 'undefined'; 779 display = display && typeof feature.properties.values !== 'undefined'; 780 display = display && typeof feature.properties.disaggregations !== 'undefined'; 781 return display; 782 }, 783 784 featuresShouldDisplay: function(features) { 785 for (var i = 0; i < features.length; i++) { 786 if (this.featureShouldDisplay(features[i])) { 787 return true; 788 } 789 } 790 return false; 791 } 792 }; 793 794 // A really lightweight plugin wrapper around the constructor, 795 // preventing against multiple instantiations 796 $.fn['sdgMap'] = function(options) { 797 return this.each(function() { 798 if (!$.data(this, 'plugin_sdgMap')) { 799 $.data(this, 'plugin_sdgMap', new Plugin(this, options)); 800 } 801 }); 802 }; 803})(jQuery); 804// This "crops" the charts so that empty years are not displayed 805// at the beginning or end of each dataset. This ensures that the 806// chart will fill all the available space. 807Chart.register({ 808 id: 'rescaler', 809 beforeInit: function (chart, options) { 810 chart.config.data.allLabels = chart.config.data.labels.slice(0); 811 }, 812 afterDatasetsUpdate: function (chart) { 813 _.each(chart.data.datasets, function (ds) { 814 if (!ds.initialised) { 815 ds.initialised = true; 816 ds.allData = ds.data.slice(0); 817 } 818 }); 819 }, 820 afterUpdate: function (chart) { 821 822 // Ensure this only runs once. 823 if (chart.isScaleUpdate) { 824 chart.isScaleUpdate = false; 825 return; 826 } 827 828 // For each dataset, create an object showing the 829 // index of the minimum value and the index of the 830 // maximum value (not counting empty/null values). 831 var ranges = _.chain(chart.data.datasets).map('allData').map(function (data) { 832 return { 833 min: _.findIndex(data, function(val) { return val !== null }), 834 max: _.findLastIndex(data, function(val) { return val !== null }) 835 }; 836 }).value(); 837 838 // Figure out the overal minimum and maximum 839 // considering all of the datasets. 840 var dataRange = ranges.length ? { 841 min: _.chain(ranges).map('min').min().value(), 842 max: _.chain(ranges).map('max').max().value() 843 } : undefined; 844 845 if (dataRange) { 846 // "Crop" the labels according to the min/max. 847 chart.data.labels = chart.data.allLabels.slice(dataRange.min, dataRange.max + 1); 848 849 // "Crop" the data of each dataset according to the min/max. 850 chart.data.datasets.forEach(function (dataset) { 851 dataset.data = dataset.allData.slice(dataRange.min, dataRange.max + 1); 852 }); 853 854 chart.isScaleUpdate = true; 855 chart.update(); 856 } 857 } 858}); 859function getTextLinesOnCanvas(ctx, text, maxWidth) { 860 var words = text.split(" "); 861 var lines = []; 862 var currentLine = words[0]; 863 864 for (var i = 1; i < words.length; i++) { 865 var word = words[i]; 866 var width = ctx.measureText(currentLine + " " + word).width; 867 if (width < maxWidth) { 868 currentLine += " " + word; 869 } else { 870 lines.push(currentLine); 871 currentLine = word; 872 } 873 } 874 lines.push(currentLine); 875 return lines; 876} 877 878function isHighContrast(contrast) { 879 if (contrast) { 880 return contrast === 'high'; 881 } 882 else { 883 return $('body').hasClass('contrast-high'); 884 } 885} 886 887// This plugin displays a message to the user whenever a chart has no data. 888Chart.register({ 889 id: 'open-sdg-no-data-message',
890 afterDraw: function(chart) { 891 if (chart.data.datasets.length === 0) { 892 893 var ctx = chart.ctx; 894 var width = chart.width; 895 var height = chart.height; 896 897 chart.clear(); 898 899 ctx.save(); 900 ctx.textAlign = 'center'; 901 ctx.textBaseline = 'middle'; 902 ctx.font = "normal 40px 'Open Sans', Helvetica, Arial, sans-serif"; 903 ctx.fillStyle = (isHighContrast()) ? 'white' : 'black'; 904 var lines = getTextLinesOnCanvas(ctx, translations.indicator.data_not_available, width); 905 var numLines = lines.length; 906 var lineHeight = 50; 907 var xLine = width / 2; 908 var yLine = (height / 2) - ((lineHeight / 2) * numLines); 909 for (var i = 0; i < numLines; i++) { 910 ctx.fillText(lines[i], xLine, yLine); 911 yLine += lineHeight; 912 } 913 ctx.restore(); 914 915 $('#selectionsChart').addClass('chart-has-no-data'); 916 } 917 else { 918 $('#selectionsChart').removeClass('chart-has-no-data'); 919 } 920 } 921}); 922// This plugin allows users to cycle through tooltips by keyboard. 923Chart.register({ 924 id: 'open-sdg-accessible-charts', 925 afterInit: function(chart) { 926 var plugin = this; 927 plugin.chart = chart; 928 plugin.selectedIndex = -1; 929 plugin.currentDataset = 0; 930 plugin.setMeta(); 931 932 if (!$(chart.canvas).data('keyboardNavInitialized')) { 933 $(chart.canvas).data('keyboardNavInitialized', true); 934 plugin.initElements(); 935 chart.canvas.addEventListener('keydown', function(e) { 936 if (e.key === 'ArrowRight') { 937 plugin.activateNext(); 938 e.preventDefault(); 939 } 940 else if (e.key === 'ArrowLeft') { 941 plugin.activatePrev(); 942 e.preventDefault(); 943 } 944 }); 945 chart.canvas.addEventListener('focus', function() { 946 if (plugin.selectedIndex === -1) { 947 plugin.activateNext(); 948 } else { 949 plugin.activate(); 950 } 951 }); 952 } 953 }, 954 afterUpdate: function(chart) { 955 this.setMeta(); 956 }, 957 setMeta: function() { 958 this.meta = this.chart.getDatasetMeta(this.currentDataset); 959 }, 960 initElements: function() { 961 $('<span/>') 962 .addClass('sr-only') 963 .attr('id', 'chart-tooltip-status') 964 .attr('role', 'status') 965 .appendTo('#chart'); 966 if (window.innerWidth <= 768) { 967 var mobileInstructions = translations.indicator.chart + '. ' + translations.indicator.data_tabular_alternative; 968 $(this.chart.canvas).html('<span class="hide-during-image-download">' + mobileInstructions + '</span>'); 969 } 970 else { 971 var keyboardInstructions = translations.indicator.data_keyboard_navigation; 972 $('<span/>') 973 .css('display', 'none') 974 .attr('id', 'chart-keyboard') 975 .text(', ' + keyboardInstructions) 976 .appendTo('#chart'); 977 var describedBy = $('#chart canvas').attr('aria-describedby'); 978 $(this.chart.canvas) 979 .attr('role', 'application') 980 .attr('aria-describedby', 'chart-keyboard ' + describedBy) 981 .html('<span class="hide-during-image-download">Chart. ' + keyboardInstructions + '</span>') 982 } 983 }, 984 activate: function() { 985 var activeElements = []; 986 if (this.chart.config.type === 'line') { 987 // For line charts, we combined all datasets into a single tooltip. 988 var numDatasets = this.chart.data.datasets.length; 989 for (var i = 0; i < numDatasets; i++) { 990 activeElements.push({datasetIndex: i, index: this.selectedIndex}); 991 } 992 } 993 else { 994 activeElements.push({datasetIndex: this.currentDataset, index: this.selectedIndex}); 995 } 996 this.chart.tooltip.setActiveElements(activeElements); 997 this.chart.render(); 998 this.announceTooltips() 999 }, 1000 isSelectedIndexEmpty: function() { 1001 var isEmpty = true; 1002 if (this.chart.config.type === 'line') { 1003 var numDatasets = this.chart.data.datasets.length; 1004 for (var i = 0; i < numDatasets; i++) { 1005 var dataset = this.chart.data.datasets[i], 1006 value = dataset.data[this.selectedIndex]; 1007 if (typeof value !== 'undefined') { 1008 isEmpty = false;
1009 } 1010 } 1011 } 1012 else { 1013 var dataset = this.chart.data.datasets[this.currentDataset], 1014 value = dataset.data[this.selectedIndex]; 1015 if (typeof value !== 'undefined') { 1016 isEmpty = false; 1017 } 1018 } 1019 return isEmpty; 1020 }, 1021 activateNext: function() { 1022 // Abort early if no data. 1023 if (this.chart.data.datasets.length === 0) { 1024 return; 1025 } 1026 this.selectedIndex += 1; 1027 if (this.selectedIndex >= this.meta.data.length) { 1028 this.selectedIndex = 0; 1029 if (this.chart.config.type !== 'line') { 1030 this.nextDataset(); 1031 } 1032 } 1033 while (this.isSelectedIndexEmpty()) { 1034 // Skip any empty years. 1035 this.activateNext(); 1036 return; 1037 } 1038 this.activate(); 1039 }, 1040 activatePrev: function() { 1041 // Abort early if no data. 1042 if (this.chart.data.datasets.length === 0) { 1043 return; 1044 } 1045 this.selectedIndex -= 1; 1046 if (this.selectedIndex < 0) { 1047 if (this.chart.config.type !== 'line') { 1048 this.prevDataset(); 1049 } 1050 this.selectedIndex = this.meta.data.length - 1; 1051 } 1052 while (this.isSelectedIndexEmpty()) { 1053 // Skip any empty years. 1054 this.activatePrev(); 1055 return; 1056 } 1057 this.activate(); 1058 }, 1059 nextDataset: function() { 1060 var numDatasets = this.chart.data.datasets.length; 1061 this.currentDataset += 1; 1062 if (this.currentDataset >= numDatasets) { 1063 this.currentDataset = 0; 1064 } 1065 this.setMeta(); 1066 }, 1067 prevDataset: function() { 1068 var numDatasets = this.chart.data.datasets.length; 1069 this.currentDataset -= 1; 1070 if (this.currentDataset < 0) { 1071 this.currentDataset = numDatasets - 1; 1072 } 1073 this.setMeta(); 1074 }, 1075 announceTooltips: function() { 1076 var tooltips = this.chart.tooltip.getActiveElements(); 1077 if (tooltips.length > 0) { 1078 var labels = {}; 1079 for (var i = 0; i < tooltips.length; i++) { 1080 var datasetIndex = tooltips[i].datasetIndex, 1081 pointIndex = tooltips[i].index, 1082 year = this.chart.data.labels[pointIndex], 1083 dataset = this.chart.data.datasets[datasetIndex], 1084 label = dataset.label, 1085 value = dataset.data[pointIndex], 1086 observationAttributes = dataset.observationAttributes[pointIndex], 1087 helpers = this.chart.config._config.indicatorViewHelpers; 1088 1089 if (observationAttributes && observationAttributes.length > 0) { 1090 label += ', ' + observationAttributes.map(helpers.getObservationAttributeText).join(', '); 1091 } 1092 1093 if (typeof labels[year] === 'undefined') { 1094 labels[year] = []; 1095 } 1096 labels[year].push(label + ': ' + value); 1097 } 1098 var announcement = ''; 1099 Object.keys(labels).forEach(function(year) { 1100 announcement += year + ' '; 1101 labels[year].forEach(function(label) { 1102 announcement += label + ', '; 1103 }); 1104 }); 1105 var currentAnnouncement = $('#chart-tooltip-status').text(); 1106 if (currentAnnouncement != announcement) { 1107 $('#chart-tooltip-status').text(announcement); 1108 } 1109 } 1110 } 1111}); 1112function event(sender) { 1113 this._sender = sender; 1114 this._listeners = []; 1115} 1116 1117event.prototype = { 1118 attach: function (listener) { 1119 this._listeners.push(listener); 1120 }, 1121 notify: function (args) { 1122 var index; 1123 1124 for (index = 0; index < this._listeners.length; index += 1) { 1125 this._listeners[index](this._sender, args); 1126 } 1127 } 1128}; 1129var accessibilitySwitcher = function () { 1130 1131 function getActiveContrast() { 1132 return $('body').hasClass('contrast-high') ? 'high' : 'default'; 1133 } 1134 1135 function setHighContrast() { 1136 $('body') 1137 .removeClass('contrast-default') 1138 .addClass('contrast-high'); 1139 var title = translations.header.disable_high_contrast; 1140 var gaAttributes = opensdg.autotrack('switch_contrast', 'Accessibility', 'Change contrast setting', 'default'); 1141 $('[data-contrast-switch-to]') 1142 .attr('data-contrast-switch-to', 'default') 1143 .attr('title', title) 1144 .attr('aria-label', title) 1145 .attr(gaAttributes); 1146 1147 imageFix('high'); 1148 createCookie('contrast', 'high', 365); 1149 } 1150 1151 function setDefaultContrast() { 1152 $('body') 1153 .removeClass('contrast-high') 1154 .addClass('contrast-default'); 1155 var title = translations.header.enable_high_contrast; 1156 var gaAttributes = opensdg.autotrack('switch_contrast', 'Accessibility', 'Change contrast setting', 'high'); 1157 $('[data-contrast-switch-to]') 1158 .attr('data-contrast-switch-to', 'high') 1159 .attr('title', title) 1160 .attr('aria-label', title) 1161 .attr(gaAttributes); 1162 1163 imageFix('default'); 1164 createCookie('contrast', 'default', 365); 1165 1166 } 1167 1168 $('[data-contrast-switch-to]').click(function () { 1169 var newContrast = $(this).attr('data-contrast-switch-to'); 1170 var oldContrast = getActiveContrast(); 1171 if (newContrast === oldContrast) { 1172 return; 1173 } 1174 if (newContrast === 'high') { 1175 setHighContrast(); 1176 broadcastContrastChange('high', this); 1177 } 1178 else { 1179 setDefaultContrast(); 1180 broadcastContrastChange('default', this); 1181 } 1182 1183 }); 1184 1185 function createCookie(name, value, days) { 1186 if (days) { 1187 var date = new Date(); 1188 date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); 1189 var expires = "; expires=" + date.toGMTString(); 1190 } 1191 else expires = ""; 1192 document.cookie = name + "=" + value + expires + "; path=/"; 1193 } 1194 1195 function readCookie(name) { 1196 var nameEQ = name + "="; 1197 var ca = document.cookie.split(';'); 1198 for (var i = 0; i < ca.length; i++) { 1199 var c = ca[i]; 1200 while (c.charAt(0) == ' ') c = c.substring(1, c.length); 1201 if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length); 1202 } 1203 return null; 1204 } 1205 1206 function imageFix(contrast) { 1207 var doNotSwitchTheseSuffixes = ['.svg']; 1208 if (contrast == 'high') { 1209 _.each($('img:not([src*=high-contrast])'), function (image) { 1210 var src = $(image).attr('src').toLowerCase(); 1211 var switchThisImage = true; 1212 for (var i = 0; i < doNotSwitchTheseSuffixes.length; i++) { 1213 var suffix = doNotSwitchTheseSuffixes[i]; 1214 if (src.slice(0 - suffix.length) === suffix) { 1215 switchThisImage = false;
1216 } 1217 } 1218 if (switchThisImage) { 1219 $(image).attr('src', $(image).attr('src').replace('img/', 'img/high-contrast/')); 1220 } 1221 }); 1222 } else { 1223 // Remove high-contrast 1224 _.each($('img[src*=high-contrast]'), function (goalImage) { 1225 $(goalImage).attr('src', $(goalImage).attr('src').replace('high-contrast/', '')); 1226 }) 1227 } 1228 }; 1229 1230 function broadcastContrastChange(contrast, elem) { 1231 var event = new CustomEvent('contrastChange', { 1232 bubbles: true, 1233 detail: contrast 1234 }); 1235 elem.dispatchEvent(event); 1236 } 1237 1238 window.onunload = function (e) { 1239 var contrast = getActiveContrast(); 1240 createCookie('contrast', contrast, 365); 1241 } 1242 1243 var cookie = readCookie('contrast'); 1244 var contrast = cookie ? cookie : 'default'; 1245 if (contrast === 'high') { 1246 setHighContrast(); 1247 } 1248 else { 1249 setDefaultContrast(); 1250 } 1251 1252}; 1253 1254// Dynamic aria labels on navbar toggle. 1255$(document).ready(function() { 1256 $('#navbarSupportedContent').on('shown.bs.collapse', function() { 1257 $('.navbar-toggler').attr('aria-label', translations.header.hide_menu); 1258 }); 1259 $('#navbarSupportedContent').on('hidden.bs.collapse', function() { 1260 $('.navbar-toggler').attr('aria-label', translations.header.show_menu); 1261 }); 1262}); 1263opensdg.chartColors = function(indicatorId) { 1264 var colorSet = "classic"; 1265 var numberOfColors = 9; 1266 var customColorList = null; 1267 1268 this.goalNumber = parseInt(indicatorId.slice(indicatorId.indexOf('_')+1,indicatorId.indexOf('-'))); 1269 this.goalColors = [['891523', 'ef7b89', '2d070b', 'f4a7b0', 'b71c2f', 'ea4f62', '5b0e17', 'fce9eb'], 1270 ['896d1f', 'efd385', '2d240a', 'f4e2ae', 'b7922a', 'eac55d', '5b4915', 'f9f0d6'], 1271 ['2d5f21', '93c587', '0f1f0b', 'c9e2c3', '3c7f2c', '6fb25f', '1e3f16', 'a7d899'], 1272 ['760f1b', 'dc7581', '270509', 'f3d1d5', '9d1424', 'd04656', '4e0a12', 'e7a3ab'], 1273 ['b22817', 'ff7563', '330b06', 'ffd7d2', 'cc2e1a', 'ff614d', '7f1d10', 'ff9c90'], 1274 ['167187', '7cd7ed', '07252d', 'd3f1f9', '1e97b4', '51cae7', '0f4b5a', 'a8e4f3'], 1275 ['977506', 'fddb6c', '322702', 'fef3ce', 'c99c08', 'fccf3b', '644e04', 'fde79d'], 1276 ['610f27', 'c7758d', 'ecd1d9', '811434', 'b44667', '400a1a', 'd9a3b3', 'd8ced1'], 1277 ['973f16', 'fda57c', '321507', 'fee1d3', 'ca541d', 'fd8750', '652a0e', 'fec3a7'], 1278 ['840b3d', 'ea71a3', '2c0314', 'f8cfe0', 'b00f52', 'd5358b', '580729', 'f1a0c2'], 1279 ['653e0e', 'fed7a7', 'b16d19', 'fdba65', 'b14a1e', 'fd976b', '000000', 'fed2bf'], 1280 ['785b1b', 'dec181', '281e09', 'f4ead5', 'a07a24', 'd3ad56', '503d12', 'e9d6ab'], 1281 ['254b28', '8bb18e', '0c190d', 'd8e5d9', '326436', '659769', '19321b', 'b2cbb4'], 1282 ['065a82', '6cc0e8', '021e2b', 'ceeaf7', '0878ad', '3aabe0', '043c56', '9dd5ef'], 1283 ['337319', '99d97f', '112608', 'ddf2d4', '449922', '77cc55', '224c11', 'bbe5aa'], 1284 ['00293e', '99c2d7', '00486d', '4c95ba', '126b80', 'cce0eb', '5a9fb0', 'a1c8d2'], 1285 ['0a1c2a', '8ca3b4', '16377c', 'd1dae1', '11324a', '466c87', '5b73a3', '0f2656']]; 1286 this.colorSets = {'classic':['7e984f', '8d73ca', 'aaa533', 'c65b8a', '4aac8d', 'c95f44'], 1287 'sdg':['e5243b', 'dda63a', '4c9f38', 'c5192d', 'ff3a21', '26bde2', 'fcc30b', 'a21942', 'fd6925', 'dd1367','fd9d24','bf8b2e','3f7e44','0a97d9','56c02b','00689d','19486a'], 1288 'goal': this.goalColors[this.goalNumber-1], 1289 'custom': customColorList, 1290 'accessible': ['cd7a00', '339966', '9966cc', '8d4d57', 'A33600', '054ce6']}; 1291 if(Object.keys(this.colorSets).indexOf(colorSet) == -1 || (colorSet=='custom' && customColorList == null)){ 1292 return this.colorSets['accessible']; 1293 } 1294 this.numberOfColors = (numberOfColors>this.colorSets[colorSet].length || numberOfColors == null || numberOfColors == 0) ? this.colorSets[colorSet].length : numberOfColors; 1295 this.colors = this.colorSets[colorSet].slice(0,this.numberOfColors); 1296 1297 return this.colors; 1298 1299}; 1300var indicatorModel = function (options) { 1301 1302 var helpers = 1303(function() { 1304 1305 /** 1306 * Constants to be used in indicatorModel.js and helper functions. 1307 */ 1308var UNIT_COLUMN = 'Units'; 1309var SERIES_COLUMN = 'Series'; 1310var GEOCODE_COLUMN = 'GeoCode'; 1311var YEAR_COLUMN = 'Year'; 1312var VALUE_COLUMN = 'Value'; 1313// Note this headline color is overridden in indicatorView.js. 1314var HEADLINE_COLOR = '#777777'; 1315var GRAPH_TITLE_FROM_SERIES = true; 1316 1317 /** 1318 * Model helper functions with general utility. 1319 */ 1320 1321/** 1322 * @param {string} prop Property to get unique values from 1323 * @param {Array} rows 1324 */ 1325function getUniqueValuesByProperty(prop, rows) { 1326 var uniques = new Set(); 1327 rows.forEach(function(row) { 1328 if (row[prop] != null) { 1329 uniques.add(row[prop]) 1330 } 1331 }); 1332 return Array.from(uniques); 1333} 1334 1335// Use as a callback to Array.prototype.filter to get unique elements 1336function isElementUniqueInArray(element, index, arr) { 1337 return arr.indexOf(element) === index; 1338} 1339 1340/** 1341 * @param {Array} columns 1342 * @return {boolean} 1343 */ 1344function dataHasGeoCodes(columns) { 1345 return columns.includes(GEOCODE_COLUMN); 1346} 1347 1348/** 1349 * @param {Array} rows 1350 * @return {Array} Columns from first row 1351 */ 1352function getColumnsFromData(rows) { 1353 return Object.keys(rows.reduce(function(result, obj) { 1354 return Object.assign(result, obj); 1355 }, {})); 1356} 1357 1358/** 1359 * @param {Array} columns 1360 * @return {Array} Columns without non-fields 1361 */ 1362function getFieldColumnsFromData(columns) { 1363 var omitColumns = nonFieldColumns(); 1364 return columns.filter(function(col) { 1365 return !omitColumns.includes(col); 1366 }); 1367} 1368 1369/** 1370 * @return {Array} Data columns that have a special purpose 1371 * 1372 * All other data columns can be considered "field columns". 1373 */ 1374function nonFieldColumns() { 1375 var columns = [ 1376 YEAR_COLUMN, 1377 VALUE_COLUMN, 1378 UNIT_COLUMN, 1379 GEOCODE_COLUMN, 1380 'Observation status', 1381 'Unit multiplier', 1382 'Unit measure', 1383 ]; 1384 var timeSeriesAttributes = [{"field":"COMMENT_TS","label":"indicator.footnote"},{"field":"DATA_LAST_UPDATE","label":"metadata_fields.national_data_update_url"}]; 1385 if (timeSeriesAttributes && timeSeriesAttributes.length > 0) {
1386 timeSeriesAttributes.forEach(function(tsAttribute) { 1387 columns.push(tsAttribute.field); 1388 }); 1389 } 1390 var observationAttributes = [{"field":"COMMENT_OBS_0","label":""},{"field":"COMMENT_OBS_1","label":""},{"field":"COMMENT_OBS_2","label":""}]; 1391 if (observationAttributes && observationAttributes.length > 0) { 1392 observationAttributes.forEach(function(oAttribute) { 1393 columns.push(oAttribute.field); 1394 }); 1395 } 1396 columns.push(SERIES_COLUMN); 1397 return columns; 1398} 1399 1400/** 1401 * @param {Array} items Objects optionally containing 'unit' and/or 'series' 1402 * @param {String} selectedUnit 1403 * @param {String} selectedSeries 1404 * @return {object|false} The first match given the selected unit/series, or false 1405 */ 1406function getMatchByUnitSeries(items, selectedUnit, selectedSeries) { 1407 var matches = getMatchesByUnitSeries(items, selectedUnit, selectedSeries); 1408 return (matches.length > 0) ? matches[0] : false; 1409} 1410 1411/** 1412 * @param {Array} items Objects optionally containing 'unit' and/or 'series' 1413 * @param {String} selectedUnit 1414 * @param {String} selectedSeries 1415 * @return {Array} All matches given the selected unit/series, if any. 1416 */ 1417function getMatchesByUnitSeries(items, selectedUnit, selectedSeries) { 1418 if (!items || items.length === 0) { 1419 return []; 1420 } 1421 if (!selectedUnit && !selectedSeries) { 1422 return items; 1423 } 1424 // First pass to find any exact matches. 1425 var matches = items.filter(function(item) { 1426 var seriesMatch = item.series === selectedSeries, 1427 unitMatch = item.unit === selectedUnit; 1428 if (selectedUnit && selectedSeries) { 1429 return seriesMatch && unitMatch; 1430 } 1431 else if (selectedUnit) { 1432 return unitMatch; 1433 } 1434 else if (selectedSeries) { 1435 return seriesMatch; 1436 } 1437 }); 1438 // Second pass to find any partial matches with unspecified unit/series. 1439 if (matches.length === 0) { 1440 matches = items.filter(function(item) { 1441 var seriesMatch = item.series === selectedSeries && item.series && !item.unit, 1442 unitMatch = item.unit === selectedUnit && item.unit && !item.series; 1443 if (selectedUnit && selectedSeries) { 1444 return seriesMatch || unitMatch; 1445 } 1446 else if (selectedUnit) { 1447 return unitMatch; 1448 } 1449 else if (selectedSeries) { 1450 return seriesMatch; 1451 } 1452 }); 1453 } 1454 // Third pass to catch cases where nothing at all was specified. 1455 if (matches.length === 0) { 1456 matches = items.filter(function(item) { 1457 var nothingSpecified = !item.unit && !item.series; 1458 return nothingSpecified; 1459 }); 1460 } 1461 return matches; 1462} 1463 1464 /** 1465 * Model helper functions related to units. 1466 */ 1467 1468/** 1469 * @param {Array} rows 1470 * @return {boolean} 1471 */ 1472function dataHasUnits(columns) { 1473 return columns.includes(UNIT_COLUMN); 1474} 1475 1476/** 1477 * @param {Array} fieldsUsedByUnit Field names 1478 * @return {boolean} 1479 */ 1480function dataHasUnitSpecificFields(fieldsUsedByUnit) { 1481 return !_.every(_.map(fieldsUsedByUnit, 'fields'), function(fields) { 1482 return _.isEqual(_.sortBy(_.map(fieldsUsedByUnit, 'fields')[0]), _.sortBy(fields)); 1483 }); 1484} 1485 1486/** 1487 * @param {Array} units 1488 * @param {Array} rows 1489 * @return {Array} Field names 1490 */ 1491function fieldsUsedByUnit(units, rows, columns) { 1492 var fields = getFieldColumnsFromData(columns); 1493 return units.map(function(unit) { 1494 return { 1495 unit: unit, 1496 fields: fields.filter(function(field) { 1497 return fieldIsUsedInDataWithUnit(field, unit, rows); 1498 }, this), 1499 } 1500 }, this); 1501} 1502 1503/** 1504 * @param {string} field 1505 * @param {string} unit 1506 * @param {Array} rows 1507 */ 1508function fieldIsUsedInDataWithUnit(field, unit, rows) { 1509 return rows.some(function(row) { 1510 return row[field] && row[UNIT_COLUMN] === unit; 1511 }, this); 1512} 1513 1514/** 1515 * @param {Array} rows 1516 * @param {string} unit 1517 * @return {Array} Rows 1518 */ 1519function getDataByUnit(rows, unit) { 1520 return rows.filter(function(row) { 1521 return row[UNIT_COLUMN] === unit; 1522 }, this); 1523} 1524 1525/** 1526 * @param {Array} rows 1527 * @return {string} 1528 */ 1529function getFirstUnitInData(rows) { 1530 return rows.find(function(row) { 1531 return row[UNIT_COLUMN]; 1532 }, this)[UNIT_COLUMN]; 1533} 1534 1535/** 1536 * @param {Array} startValues Objects containing 'field' and 'value' 1537 * @return {string|boolean} Unit, or false if none were found 1538 */ 1539function getUnitFromStartValues(startValues) { 1540 var match = startValues.find(function(startValue) { 1541 return startValue.field === UNIT_COLUMN; 1542 }, this); 1543 return (match) ? match.value : false;
1544} 1545 1546 /** 1547 * Model helper functions related to serieses. 1548 */ 1549 1550/** 1551 * @param {Array} columns 1552 * @return {boolean} 1553 */ 1554function dataHasSerieses(columns) { 1555 return columns.includes(SERIES_COLUMN); 1556} 1557 1558/** 1559 * @param {Array} fieldsUsedBySeries Field names 1560 * @return {boolean} 1561 */ 1562function dataHasSeriesSpecificFields(fieldsUsedBySeries) { 1563 return !_.every(_.map(fieldsUsedBySeries, 'fields'), function(fields) { 1564 return _.isEqual(_.sortBy(_.map(fieldsUsedBySeries, 'fields')[0]), _.sortBy(fields)); 1565 }); 1566} 1567 1568/** 1569 * @param {Array} serieses 1570 * @param {Array} rows 1571 * @return {Array} Field names 1572 */ 1573function fieldsUsedBySeries(serieses, rows, columns) { 1574 var fields = getFieldColumnsFromData(columns); 1575 return serieses.map(function(series) { 1576 return { 1577 series: series, 1578 fields: fields.filter(function(field) { 1579 return fieldIsUsedInDataWithSeries(field, series, rows); 1580 }, this), 1581 } 1582 }, this); 1583} 1584 1585/** 1586 * @param {string} field 1587 * @param {string} series 1588 * @param {Array} rows 1589 */ 1590function fieldIsUsedInDataWithSeries(field, series, rows) { 1591 return rows.some(function(row) { 1592 return row[field] && row[SERIES_COLUMN] === series; 1593 }, this); 1594} 1595 1596/** 1597 * @param {Array} rows 1598 * @param {string} series 1599 * @return {Array} Rows 1600 */ 1601function getDataBySeries(rows, series) { 1602 return rows.filter(function(row) { 1603 return row[SERIES_COLUMN] === series; 1604 }, this); 1605} 1606 1607/** 1608 * @param {Array} rows 1609 * @return {string} 1610 */ 1611function getFirstSeriesInData(rows) { 1612 return rows.find(function(row) { 1613 return row[SERIES_COLUMN]; 1614 }, this)[SERIES_COLUMN]; 1615} 1616 1617/** 1618 * @param {Array} startValues Objects containing 'field' and 'value' 1619 * @return {string|boolean} Series, or false if none were found 1620 */ 1621function getSeriesFromStartValues(startValues) { 1622 var match = startValues.find(function(startValue) { 1623 return startValue.field === SERIES_COLUMN; 1624 }, this); 1625 return (match) ? match.value : false; 1626} 1627 1628 /** 1629 * Model helper functions related to fields and data. 1630 */ 1631 1632/** 1633 * @param {Array} rows 1634 * @param {Array} edges 1635 * @return {Array} Field item states 1636 */ 1637 1638function getInitialFieldItemStates(rows, edges, columns, dataSchema) { 1639 var fields = getFieldColumnsFromData(columns); 1640 sortFieldNames(fields, dataSchema); 1641 var initial = fields.map(function(field) { 1642 var values = getUniqueValuesByProperty(field, rows); 1643 sortFieldValueNames(field, values, dataSchema); 1644 return { 1645 field: field, 1646 hasData: true, 1647 values: values.map(function(value) { 1648 return { 1649 value: value, 1650 state: 'default', 1651 checked: false, 1652 hasData: true 1653 }; 1654 }, this), 1655 }; 1656 }, this); 1657 1658 return sortFieldItemStates(initial, edges, dataSchema); 1659} 1660 1661/** 1662 * @param {Array} fieldItemStates 1663 * @param {Array} edges 1664 * return {Array} Sorted field item states 1665 */ 1666function sortFieldItemStates(fieldItemStates, edges, dataSchema) { 1667 if (edges.length > 0) { 1668 var froms = getUniqueValuesByProperty('From', edges).sort(); 1669 var tos = getUniqueValuesByProperty('To', edges).sort(); 1670 var orderedEdges = froms.concat(tos); 1671 var fieldsNotInEdges = fieldItemStates 1672 .map(function(fis) { return fis.field; }) 1673 .filter(function(field) { return !orderedEdges.includes(field); }); 1674 var customOrder = orderedEdges.concat(fieldsNotInEdges); 1675 sortFieldNames(customOrder, dataSchema); 1676 1677 return _.sortBy(fieldItemStates, function(item) { 1678 return customOrder.indexOf(item.field); 1679 }); 1680 } 1681 return fieldItemStates; 1682} 1683 1684/** 1685 * @param {Array} fieldItemStates 1686 * @param {Array} edges 1687 * @param {Array} selectedFields Field items 1688 * @param {Object} validParentsByChild Arrays of parents keyed to children 1689 * @return {Array} Field item states 1690 */ 1691function getUpdatedFieldItemStates(fieldItemStates, edges, selectedFields, validParentsByChild) { 1692 var selectedFieldNames = getFieldNames(selectedFields); 1693 getParentFieldNames(edges).forEach(function(parentFieldName) { 1694 if (selectedFieldNames.includes(parentFieldName)) { 1695 var childFieldNames = getChildFieldNamesByParent(edges, parentFieldName); 1696 var selectedParent = selectedFields.find(function(selectedField) { 1697 return selectedField.field === parentFieldName; 1698 }, this);
1699 fieldItemStates.forEach(function(fieldItem) { 1700 if (childFieldNames.includes(fieldItem.field)) { 1701 var fieldHasData = false; 1702 fieldItem.values.forEach(function(childValue) { 1703 var valueHasData = false; 1704 selectedParent.values.forEach(function(parentValue) { 1705 if (validParentsByChild[fieldItem.field][childValue.value].includes(parentValue)) { 1706 valueHasData = true; 1707 fieldHasData = true; 1708 } 1709 }, this); 1710 childValue.hasData = valueHasData; 1711 }, this); 1712 fieldItem.hasData = fieldHasData; 1713 } 1714 }, this); 1715 } 1716 }, this); 1717 return fieldItemStates; 1718} 1719 1720/** 1721 * @param {Array} fieldItems 1722 * @return {Array} Field names 1723 */ 1724function getFieldNames(fieldItems) { 1725 return fieldItems.map(function(item) { return item.field; }); 1726} 1727 1728/** 1729 * @param {Array} edges 1730 * @return {Array} Names of parent fields 1731 */ 1732function getParentFieldNames(edges) { 1733 return edges.map(function(edge) { return edge.From; }); 1734} 1735 1736/** 1737 * @param {Array} edges 1738 * @param {string} parent 1739 * @return {Array} Children of parent 1740 */ 1741function getChildFieldNamesByParent(edges, parent) { 1742 var children = edges.filter(function(edge) { 1743 return edge.From === parent; 1744 }); 1745 return getChildFieldNames(children); 1746} 1747 1748/** 1749 * @param {Array} edges 1750 * @return {Array} Names of child fields 1751 */ 1752function getChildFieldNames(edges) { 1753 return edges.map(function(edge) { return edge.To; }); 1754} 1755 1756/** 1757 * @param {Array} fieldItemStates 1758 * @param {Array} fieldsByUnit Objects containing 'unit' and 'fields' 1759 * @param {string} selectedUnit 1760 * @param {boolean} dataHasUnitSpecificFields 1761 * @param {Array} fieldsBySeries Objects containing 'series' and 'fields' 1762 * @param {string} selectedSeries 1763 * @param {boolean} dataHasSeriesSpecificFields 1764 * @param {Array} selectedFields Field items 1765 * @param {Array} edges 1766 * @param {string} compositeBreakdownLabel Alternate label for COMPOSITE_BREAKDOWN fields 1767 * @return {Array} Field item states (with additional "label" properties) 1768 */ 1769function fieldItemStatesForView(fieldItemStates, fieldsByUnit, selectedUnit, dataHasUnitSpecificFields, fieldsBySeries, selectedSeries, dataHasSeriesSpecificFields, selectedFields, edges, compositeBreakdownLabel) { 1770 var states = fieldItemStates.map(function(item) { return item; }); 1771 if (dataHasUnitSpecificFields && dataHasSeriesSpecificFields) { 1772 states = fieldItemStatesForSeries(fieldItemStates, fieldsBySeries, selectedSeries); 1773 states = fieldItemStatesForUnit(states, fieldsByUnit, selectedUnit); 1774 } 1775 else if (dataHasSeriesSpecificFields) { 1776 states = fieldItemStatesForSeries(fieldItemStates, fieldsBySeries, selectedSeries); 1777 } 1778 else if (dataHasUnitSpecificFields) { 1779 states = fieldItemStatesForUnit(fieldItemStates, fieldsByUnit, selectedUnit); 1780 } 1781 // Set all values to checked=false because they are going to be 1782 // conditionally set to true below, if needed. 1783 states.forEach(function(fieldItem) { 1784 fieldItem.values.forEach(function(defaultFieldItemValue) { 1785 defaultFieldItemValue.checked = false; 1786 }); 1787 }); 1788 if (selectedFields && selectedFields.length > 0) { 1789 states.forEach(function(fieldItem) { 1790 var selectedField = selectedFields.find(function(selectedItem) { 1791 return selectedItem.field === fieldItem.field; 1792 }); 1793 if (selectedField) { 1794 selectedField.values.forEach(function(selectedValue) { 1795 var fieldItemValue = fieldItem.values.find(function(valueItem) { 1796 return valueItem.value === selectedValue; 1797 }); 1798 if (fieldItemValue) { 1799 fieldItemValue.checked = true; 1800 } 1801 }) 1802 } 1803 }); 1804 } 1805 sortFieldsForView(states, edges); 1806 return states.map(function(item) { 1807 item.label = item.field; 1808 if (item.field === 'COMPOSITE_BREAKDOWN' && compositeBreakdownLabel !== '') { 1809 item.label = compositeBreakdownLabel; 1810 } 1811 return item; 1812 }); 1813} 1814 1815/** 1816 * @param {Array} fieldItemStates 1817 * @param {Array} edges 1818 */ 1819function sortFieldsForView(fieldItemStates, edges) { 1820 if (edges.length > 0 && fieldItemStates.length > 0) { 1821 1822 var parents = edges.map(function(edge) { return edge.From; }); 1823 var children = edges.map(function(edge) { return edge.To; }); 1824 var topLevelParents = [];
1825 parents.forEach(function(parent) { 1826 if (!(children.includes(parent)) && !(topLevelParents.includes(parent))) { 1827 topLevelParents.push(parent); 1828 } 1829 }); 1830 1831 var topLevelParentsByChild = {}; 1832 children.forEach(function(child) { 1833 var currentParent = edges.find(function(edge) { return edge.To === child; }), 1834 currentChild = child; 1835 while (currentParent) { 1836 currentParent = edges.find(function(edge) { return edge.To === currentChild; }); 1837 if (currentParent) { 1838 currentChild = currentParent.From; 1839 topLevelParentsByChild[child] = currentParent.From; 1840 } 1841 } 1842 }); 1843 fieldItemStates.forEach(function(fieldItem) { 1844 if (topLevelParents.includes(fieldItem.field) || typeof topLevelParentsByChild[fieldItem.field] === 'undefined') { 1845 fieldItem.topLevelParent = ''; 1846 } 1847 else { 1848 fieldItem.topLevelParent = topLevelParentsByChild[fieldItem.field]; 1849 } 1850 }); 1851 1852 // As an intermediary step, create a hierarchical structure grouped 1853 // by the top-level parent. 1854 var tempHierarchy = []; 1855 var tempHierarchyHash = {}; 1856 fieldItemStates.forEach(function(fieldItem) { 1857 if (fieldItem.topLevelParent === '') { 1858 fieldItem.children = []; 1859 tempHierarchyHash[fieldItem.field] = fieldItem; 1860 tempHierarchy.push(fieldItem); 1861 } 1862 }); 1863 fieldItemStates.forEach(function(fieldItem) { 1864 if (typeof tempHierarchyHash[fieldItem.topLevelParent] === 'undefined') { 1865 return; 1866 } 1867 if (fieldItem.topLevelParent !== '') { 1868 tempHierarchyHash[fieldItem.topLevelParent].children.push(fieldItem); 1869 } 1870 }); 1871 1872 // Now we clear out the field items and add them back as a flat list. 1873 fieldItemStates.length = 0; 1874 tempHierarchy.forEach(function(fieldItem) { 1875 fieldItemStates.push(fieldItem); 1876 fieldItem.children.forEach(function(child) { 1877 fieldItemStates.push(child); 1878 }); 1879 }); 1880 } 1881} 1882 1883/** 1884 * @param {Array} fieldItemStates 1885 * @param {Array} fieldsByUnit Objects containing 'unit' and 'fields' 1886 * @param {string} selectedUnit 1887 * @return {Array} Field item states 1888 */ 1889function fieldItemStatesForUnit(fieldItemStates, fieldsByUnit, selectedUnit) { 1890 var fieldsBySelectedUnit = fieldsByUnit.filter(function(fieldByUnit) { 1891 return fieldByUnit.unit === selectedUnit; 1892 })[0]; 1893 return fieldItemStates.filter(function(fis) { 1894 return fieldsBySelectedUnit.fields.includes(fis.field); 1895 }); 1896} 1897 1898/** 1899 * @param {Array} fieldItemStates 1900 * @param {Array} fieldsBySeries Objects containing 'series' and 'fields' 1901 * @param {string} selectedSeries 1902 * @return {Array} Field item states 1903 */ 1904function fieldItemStatesForSeries(fieldItemStates, fieldsBySeries, selectedSeries) { 1905 var fieldsBySelectedSeries = fieldsBySeries.filter(function(fieldBySeries
1905) { 1906 return fieldBySeries.series === selectedSeries; 1907 })[0]; 1908 return fieldItemStates.filter(function(fis) { 1909 return fieldsBySelectedSeries.fields.includes(fis.field); 1910 }); 1911} 1912 1913/** 1914 * @param {Array} fieldItems 1915 * @return {Array} Objects representing disaggregation combinations 1916 */ 1917function getCombinationData(fieldItems) { 1918 1919 // First get a list of all the single field/value pairs. 1920 var fieldValuePairs = []; 1921 fieldItems.forEach(function(fieldItem) { 1922 fieldItem.values.forEach(function(value) { 1923 var pair = {}; 1924 pair[fieldItem.field] = value; 1925 fieldValuePairs.push(pair); 1926 }); 1927 }); 1928 1929 // Generate all possible subsets of these key/value pairs. 1930 var powerset = []; 1931 // Start off with an empty item. 1932 powerset.push([]); 1933 for (var i = 0; i < fieldValuePairs.length; i++) { 1934 for (var j = 0, len = powerset.length; j < len; j++) { 1935 var candidate = powerset[j].concat(fieldValuePairs[i]); 1936 if (!hasDuplicateField(candidate)) { 1937 powerset.push(candidate); 1938 } 1939 } 1940 } 1941 1942 function hasDuplicateField(pairs) { 1943 var fields = [], i; 1944 for (i = 0; i < pairs.length; i++) { 1945 var field = Object.keys(pairs[i])[0] 1946 if (fields.includes(field)) { 1947 return true; 1948 } 1949 else { 1950 fields.push(field); 1951 } 1952 } 1953 return false; 1954 } 1955 1956 // Remove the empty item. 1957 powerset.shift(); 1958 1959 return powerset.map(function(combinations) { 1960 // We want to merge these into a single object. 1961 var combinedSubset = {}; 1962 combinations.forEach(function(keyValue) { 1963 Object.assign(combinedSubset, keyValue); 1964 }); 1965 return combinedSubset; 1966 }); 1967} 1968 1969/** 1970 * @param {Array} startValues Objects containing 'field' and 'value' 1971 * @param {Array} selectableFieldNames 1972 * @return {Array} Field items 1973 */ 1974function selectFieldsFromStartValues(startValues, selectableFieldNames) { 1975 if (!startValues) { 1976 return []; 1977 } 1978 var allowedStartValues = startValues.filter(function(startValue) { 1979 var normalField = !nonFieldColumns().includes(startValue.field); 1980 var allowedField = selectableFieldNames.includes(startValue.field) 1981 return normalField && allowedField; 1982 }); 1983 var valuesByField = {}; 1984 allowedStartValues.forEach(function(startValue) { 1985 if (!(startValue.field in valuesByField)) { 1986 valuesByField[startValue.field] = []; 1987 } 1988 valuesByField[startValue.field].push(startValue.value); 1989 }); 1990 return Object.keys(valuesByField).map(function(field) { 1991 return { 1992 field: field, 1993 values: _.uniq(valuesByField[field]), 1994 }; 1995 }); 1996} 1997 1998/** 1999 * @param {Array} rows 2000 * @param {Array} selectableFieldNames Field names 2001 * @param {string} selectedUnit 2002 * @return {Array} Field items 2003 */ 2004function selectMinimumStartingFields(rows, selectableFieldNames, selectedUnit) { 2005 var filteredData = rows; 2006 if (selectedUnit) { 2007 filteredData = filteredData.filter(function(row) { 2008 return row[UNIT_COLUMN] === selectedUnit; 2009 }); 2010 } 2011 filteredData = filteredData.filter(function(row) { 2012 return selectableFieldNames.some(function(fieldName) { 2013 return row[fieldName]; 2014 }); 2015 }); 2016 // Sort the data by each field. We go in reverse order so that the 2017 // first field will be highest "priority" in the sort. 2018 selectableFieldNames.reverse().forEach(function(fieldName) { 2019 filteredData = _.sortBy(filteredData, fieldName); 2020 }); 2021 // But actually we want the top-priority sort to be the "size" of the 2022 // rows. In other words we want the row with the fewest number of fields. 2023 filteredData = _.sortBy(filteredData, function(row) { return Object.keys(row).length; }); 2024 2025 if (filteredData.length === 0) { 2026 return []; 2027 } 2028 2029 // Convert to an array of objects with 'field' and 'values' keys, omitting 2030 // any non-field columns. 2031 return Object.keys(filteredData[0]).filter(function(key) { 2032 return !nonFieldColumns().includes(key); 2033 }).map(function(field) { 2034 return { 2035 field: field, 2036 values: [filteredData[0][field]] 2037 }; 2038 }); 2039} 2040 2041/** 2042 * @param {Array} edges 2043 * @param {Array} fieldItemStates 2044 * @param {Array} rows 2045 * @return {Object} Arrays of parents keyed to children 2046 * 2047 * @TODO: This function can be a bottleneck in large datasets with a lot of 2048 * disaggregation values. Can this be further optimized? 2049 */ 2050function validParentsByChild(edges, fieldItemStates, rows) { 2051 var parentFields = getParentFieldNames(edges); 2052 var childFields = getChildFieldNames(edges);
2053 var validParentsByChild = {}; 2054 childFields.forEach(function(childField, fieldIndex) { 2055 var fieldItemState = fieldItemStates.find(function(fis) { 2056 return fis.field === childField; 2057 }); 2058 if (typeof fieldItemState === 'undefined') { 2059 return; 2060 } 2061 var childValues = fieldItemState.values.map(function(value) { 2062 return value.value; 2063 }); 2064 var parentField = parentFields[fieldIndex]; 2065 var childRows = rows.filter(function(row) { 2066 var childNotEmpty = row[childField]; 2067 var parentNotEmpty = row[parentField]; 2068 return childNotEmpty && parentNotEmpty; 2069 }) 2070 validParentsByChild[childField] = {}; 2071 childValues.forEach(function(childValue) { 2072 var rowsWithParentValues = childRows.filter(function(row) { 2073 return row[childField] == childValue; 2074 }); 2075 validParentsByChild[childField][childValue] = getUniqueValuesByProperty(parentField, rowsWithParentValues); 2076 }); 2077 }); 2078 return validParentsByChild; 2079} 2080 2081/** 2082 * @param {Array} selectableFields Field names 2083 * @param {Array} edges 2084 * @param {Array} selectedFields Field items 2085 * @return {Array} Field names 2086 */ 2087function getAllowedFieldsWithChildren(selectableFields, edges, selectedFields) { 2088 var allowedFields = getInitialAllowedFields(selectableFields, edges); 2089 var selectedFieldNames = getFieldNames(selectedFields); 2090 getParentFieldNames(edges).forEach(function(parentFieldName) { 2091 if (selectedFieldNames.includes(parentFieldName)) { 2092 var childFieldNames = getChildFieldNamesByParent(edges, parentFieldName); 2093 allowedFields = allowedFields.concat(childFieldNames); 2094 } 2095 }, this); 2096 return allowedFields.filter(isElementUniqueInArray); 2097} 2098 2099/** 2100 * 2101 * @param {Array} fieldNames 2102 * @param {Array} edges 2103 * @return {Array} Field names 2104 */ 2105function getInitialAllowedFields(fieldNames, edges) { 2106 var children = getChildFieldNames(edges); 2107 return fieldNames.filter(function(field) { return !children.includes(field); }); 2108} 2109 2110/** 2111 * @param {Array} selectedFields Field names 2112 * @param {Array} edges 2113 * @return {Array} Selected fields without orphans 2114 */ 2115function removeOrphanSelections(selectedFields, edges) { 2116 var selectedFieldNames = selectedFields.map(function(selectedField) { 2117 return selectedField.field; 2118 });
2119 edges.forEach(function(edge) { 2120 if (!selectedFieldNames.includes(edge.From)) { 2121 selectedFields = selectedFields.filter(function(selectedField) { 2122 return selectedField.field !== edge.From; 2123 }); 2124 } 2125 }); 2126 return selectedFields; 2127} 2128 2129/** 2130 * @param {Array} rows 2131 * @param {Array} selectedFields Field items 2132 * @return {Array} Rows 2133 */ 2134function getDataBySelectedFields(rows, selectedFields) { 2135 return rows.filter(function(row) { 2136 return selectedFields.some(function(field) { 2137 return field.values.includes(row[field.field]); 2138 }); 2139 }); 2140} 2141 2142/** 2143 * @param {Array} rows 2144 * @param {Array} selectedFields Field items 2145 * @return {Array} Rows 2146 */ 2147function hasDataBySelectedFields(rows, selectedFields) { 2148 return rows.some(function(row) { 2149 return selectedFields.some(function(field) { 2150 return field.values.includes(row[field.field]); 2151 }); 2152 }); 2153} 2154 2155/** 2156 * @param {Array} fieldNames 2157 * @param {Object} dataSchema 2158 */ 2159function sortFieldNames(fieldNames, dataSchema) { 2160 if (dataSchema && dataSchema.fields) { 2161 var schemaFieldNames = dataSchema.fields.map(function(field) { return field.name; }); 2162 // If field names have been translated, we may need to use titles. 2163 if (schemaFieldNames.length > 0 && !(fieldNames.includes(schemaFieldNames[0]))) { 2164 schemaFieldNames = dataSchema.fields.map(function(field) { return field.title; }); 2165 } 2166 fieldNames.sort(function(a, b) { 2167 return schemaFieldNames.indexOf(a) - schemaFieldNames.indexOf(b); 2168 }); 2169 } 2170 else { 2171 fieldNames.sort(); 2172 } 2173} 2174 2175/** 2176 * @param {string} fieldName 2177 * @param {Array} fieldValues 2178 * @param {Object} dataSchema 2179 */ 2180function sortFieldValueNames(fieldName, fieldValues, dataSchema) { 2181 if (dataSchema && dataSchema.fields) { 2182 var fieldSchema = dataSchema.fields.find(function(x) { return x.name == fieldName; }); 2183 // If field names have been translated, we may need to use titles. 2184 if (!fieldSchema) { 2185 fieldSchema = dataSchema.fields.find(function(x) { return x.title == fieldName; }); 2186 } 2187 if (fieldSchema && fieldSchema.constraints && fieldSchema.constraints.enum) { 2188 fieldValues.sort(function(a, b) { 2189 return fieldSchema.constraints.enum.indexOf(a) - fieldSchema.constraints.enum.indexOf(b); 2190 }); 2191 } 2192 else { 2193 fieldValues.sort(); 2194 } 2195 } 2196 else { 2197 fieldValues.sort(); 2198 } 2199} 2200 2201 /** 2202 * Model helper functions related to charts and datasets. 2203 */ 2204 2205/** 2206 * @param {string} currentTitle 2207 * @param {Array} allTitles Objects containing 'unit' and 'title' 2208 * @param {String} selectedUnit 2209 * @param {String} selectedSeries 2210 * @return {String} Updated title 2211 */ 2212function getChartTitle(currentTitle, allTitles, selectedUnit, selectedSeries) { 2213 var match = getMatchByUnitSeries(allTitles, selectedUnit, selectedSeries); 2214 return (match) ? match.title : currentTitle; 2215} 2216 2217/** 2218 * Model helper functions related to charts and datasets. 2219 */ 2220 2221/** 2222 * @param {string} currentSubTitle 2223 * @param {Array} allSubTitles Objects containing 'unit' and 'subtitle' 2224 * @param {String} selectedUnit 2225 * @param {String} selectedSeries 2226 * @return {String} Updated title 2227 */ 2228function getChartSubTitle(currentSubTitle, allSubTitles, selectedUnit, selectedSeries) { 2229 var match = getMatchByUnitSeries(allSubTitles, selectedUnit, selectedSeries); 2230 if (allSubTitles == null) { 2231 currentSubTitle; 2232 } 2233 else { 2234 return (match) ? match.title : ''; 2235 } 2236 2237} 2238 2239 2240/** 2241 * @param {string} currentType 2242 * @param {Array} allTypes Objects containing 'unit', 'series', and 'type' 2243 * @param {String} selectedUnit 2244 * @param {String} selectedSeries 2245 * @return {String} Updated type 2246 */ 2247function getChartType(currentType, allTypes, selectedUnit, selectedSeries) { 2248 if (!currentType) { 2249 currentType = 'line'; 2250 } 2251 var match = getMatchByUnitSeries(allTypes, selectedUnit, selectedSeries); 2252 return (match) ? match.type : currentType; 2253} 2254 2255/** 2256 * @param {Array} graphLimits Objects containing 'unit' and 'title' 2257 * @param {String} selectedUnit 2258 * @param {String} selectedSeries 2259 * @return {Object|false} Graph limit object, if any 2260 */ 2261function getGraphLimits(graphLimits, selectedUnit, selectedSeries
2261) { 2262 return getMatchByUnitSeries(graphLimits, selectedUnit, selectedSeries); 2263} 2264 2265/** 2266 * @param {Array} graphAnnotations Objects containing 'unit' or 'series' or more 2267 * @param {String} selectedUnit 2268 * @param {String} selectedSeries 2269 * @return {Array} Graph annotations objects, if any 2270 */ 2271function getGraphAnnotations(graphAnnotations, selectedUnit, selectedSeries, graphTargetLines, graphSeriesBreaks, graphErrorBars) { 2272 var annotations = getMatchesByUnitSeries(graphAnnotations, selectedUnit, selectedSeries); 2273 if (graphTargetLines) { 2274 annotations = annotations.concat(getGraphTargetLines(graphTargetLines, selectedUnit, selectedSeries)); 2275 } 2276 if (graphSeriesBreaks) { 2277 annotations = annotations.concat(getGraphSeriesBreaks(graphSeriesBreaks, selectedUnit, selectedSeries)); 2278 } 2279 if (graphErrorBars) { 2280 annotations = annotations.concat(getGraphErrorBars(graphErrorBars, selectedUnit, selectedSeries)); 2281 } 2282 return annotations; 2283} 2284 2285/** 2286 * @param {Array} graphTargetLines Objects containing 'unit' or 'series' or more 2287 * @param {String} selectedUnit 2288 * @param {String} selectedSeries 2289 * @return {Array} Graph annotations objects, if any 2290 */ 2291function getGraphTargetLines(graphTargetLines, selectedUnit, selectedSeries) { 2292 return getMatchesByUnitSeries(graphTargetLines, selectedUnit, selectedSeries).map(function(targetLine) { 2293 targetLine.preset = 'target_line'; 2294 targetLine.label = { content: targetLine.label_content }; 2295 return targetLine; 2296 }); 2297} 2298 2299/** 2300 * @param {Array} graphErrorBars Objects containing 'unit' or 'series' or more 2301 * @param {String} selectedUnit 2302 * @param {String} selectedSeries 2303 * @return {Array} Graph annotations objects, if any 2304 */ 2305function getGraphErrorBars(graphErrorBars, selectedUnit, selectedSeries) { 2306 return getMatchesByUnitSeries(graphErrorBars, selectedUnit, selectedSeries).map(function(errorBar) { 2307 errorBar.preset = 'error_bar'; 2308 errorBar.label = { content: errorBar.label_content }; 2309 return errorBar; 2310 }); 2311} 2312 2313/** 2314 * @param {Array} graphSeriesBreaks Objects containing 'unit' or 'series' or more 2315 * @param {String} selectedUnit 2316 * @param {String} selectedSeries 2317 * @return {Array} Graph annotations objects, if any 2318 */ 2319function getGraphSeriesBreaks(graphSeriesBreaks, selectedUnit, selectedSeries) { 2320 return getMatchesByUnitSeries(graphSeriesBreaks, selectedUnit, selectedSeries).map(function(seriesBreak) { 2321 seriesBreak.preset = 'series_break'; 2322 seriesBreak.label = { content: seriesBreak.label_content }; 2323 return seriesBreak; 2324 }); 2325} 2326 2327/** 2328 * @param {Array} headline Rows 2329 * @param {Array} rows 2330 * @param {Array} combinations Objects representing disaggregation combinations 2331 * @param {Array} years 2332 * @param {string} defaultLabel 2333 * @param {Array} colors 2334 * @param {Array} selectableFields Field names 2335 * @param {Array} colorAssignments Color/striping assignments for disaggregation combinations 2336 * @return {Array} Datasets suitable for Chart.js 2337 */ 2338function getDatasets(headline, data, combinations, years, defaultLabel, colors, selectableFields, colorAssignments, fill, fillAbove, fillBelow, showLine, spanGaps, allObservationAttributes, mixedTypes) { 2339 var datasets = [], index = 0, dataset, colorIndex, color, background, border, striped, excess, combinationKey, colorAssignment, fill, fillAbove, fillBelow, showLine, spanGaps, mixedTypes; 2340 var numColors = colors.length, 2341 maxColorAssignments = numColors * 2; 2342 prepareColorAssignments(colorAssignments, maxColorAssignments); 2343 setAllColorAssignmentsReadyForEviction(colorAssignments); 2344 2345 combinations.forEach(function(combination) { 2346 var filteredData = getDataMatchingCombination(data, combination, selectableFields); 2347 if (filteredData.length > 0) { 2348 excess = (index >= maxColorAssignments); 2349 if (excess) { 2350 // This doesn't really matter: excess datasets won't be displayed. 2351 color = getHeadlineColor(); 2352 striped = false; 2353 } 2354 else { 2355 combinationKey = JSON.stringify(combination); 2356 colorAssignment = getColorAssignmentByCombination(colorAssignments, combinationKey); 2357 if (colorAssignment !== undefined) { 2358 colorIndex = colorAssignment.colorIndex;
2359 striped = colorAssignment.striped; 2360 colorAssignment.readyForEviction = false; 2361 } 2362 else { 2363 if (colorAssignmentsAreFull(colorAssignments)) { 2364 evictColorAssignment(colorAssignments); 2365 } 2366 var openColorInfo = getOpenColorInfo(colorAssignments, colors); 2367 colorIndex = openColorInfo.colorIndex; 2368 striped = openColorInfo.striped; 2369 colorAssignment = getAvailableColorAssignment(colorAssignments); 2370 assignColor(colorAssignment, combinationKey, colorIndex, striped); 2371 } 2372 } 2373 2374 color = getColor(colorIndex, colors); 2375 background = getBackground(color, striped); 2376 border = getBorderDash(striped); 2377 dataset = makeDataset(years, filteredData, combination, defaultLabel, color, background, border, excess, fill, fillAbove, fillBelow, showLine, spanGaps, allObservationAttributes, mixedTypes); 2378 datasets.push(dataset); 2379 index++; 2380 } 2381 }, this); 2382 2383 if (headline.length > 0) { 2384 dataset = makeHeadlineDataset(years, headline, defaultLabel, fill, fillAbove, fillBelow, showLine, spanGaps, colors, allObservationAttributes, mixedTypes); 2385 datasets.unshift(dataset); 2386 } 2387 return datasets; 2388} 2389 2390/** 2391 * @param {Array} colorAssignments 2392 * @param {int} maxColorAssignments 2393 */ 2394function prepareColorAssignments(colorAssignments, maxColorAssignments) { 2395 while (colorAssignments.length < maxColorAssignments) { 2396 colorAssignments.push({ 2397 combination: null, 2398 colorIndex: null, 2399 striped: false, 2400 readyForEviction: false, 2401 }); 2402 } 2403} 2404 2405/** 2406 * @param {Array} colorAssignments 2407 */ 2408function setAllColorAssignmentsReadyForEviction(colorAssignments) { 2409 for (var i = 0; i < colorAssignments.length; i++) { 2410 colorAssignments[i].readyForEviction = true; 2411 } 2412} 2413 2414/** 2415 * @param {Array} rows 2416 * @param {Object} combination Key/value representation of a field combo 2417 * @param {Array} selectableFields Field names 2418 * @return {Array} Matching rows 2419 */ 2420function getDataMatchingCombination(data, combination, selectableFields) { 2421 return data.filter(function(row) { 2422 return selectableFields.every(function(field) { 2423 return row[field] === combination[field]; 2424 }); 2425 }); 2426} 2427 2428/** 2429 * @param {Array} colorAssignments 2430 * @param {string} combination 2431 * @return {Object|undefined} Color assignment object if found. 2432 */ 2433function getColorAssignmentByCombination(colorAssignments, combination) { 2434 return colorAssignments.find(function(assignment) { 2435 return assignment.combination === combination; 2436 }); 2437} 2438 2439/** 2440 * @param {Array} colorAssignments 2441 * @return {boolean} 2442 */ 2443function colorAssignmentsAreFull(colorAssignments) { 2444 for (var i = 0; i < colorAssignments.length; i++) { 2445 if (colorAssignments[i].combination === null) { 2446 return false; 2447 } 2448 } 2449 return true; 2450} 2451 2452/** 2453 * @param {Array} colorAssignments 2454 */ 2455function evictColorAssignment(colorAssignments) { 2456 for (var i = 0; i < colorAssignments.length; i++) { 2457 if (colorAssignments[i].readyForEviction) { 2458 colorAssignments[i].combination = null; 2459 colorAssignments[i].colorIndex = null; 2460 colorAssignments[i].striped = false; 2461 colorAssignments[i].readyForEviction = false; 2462 return; 2463 } 2464 } 2465 throw 'Could not evict color assignment'; 2466} 2467 2468/** 2469 * @param {Array} colorAssignments 2470 * @param {Array} colors 2471 * @return {Object} Object with 'colorIndex' and 'striped' properties. 2472 */ 2473function getOpenColorInfo(colorAssignments, colors) { 2474 // First look for normal colors, then striped. 2475 var stripedStates = [false, true]; 2476 for (var i = 0; i < stripedStates.length; i++) { 2477 var stripedState = stripedStates[i]; 2478 var assignedColors = colorAssignments.filter(function(colorAssignment) { 2479 return colorAssignment.striped === stripedState && colorAssignment.colorIndex !== null; 2480 }).map(function(colorAssignment) { 2481 return colorAssignment.colorIndex; 2482 }); 2483 if (assignedColors.length < colors.length) { 2484 for (var colorIndex = 0; colorIndex < colors.length; colorIndex++) { 2485 if (!(assignedColors.includes(colorIndex))) { 2486 return { 2487 colorIndex: colorIndex,
2488 striped: stripedState, 2489 } 2490 } 2491 } 2492 } 2493 } 2494 throw 'Could not find open color'; 2495} 2496 2497/** 2498 * @param {Array} colorAssignments 2499 * @return {Object|undefined} Color assignment object if found. 2500 */ 2501function getAvailableColorAssignment(colorAssignments) { 2502 return colorAssignments.find(function(assignment) { 2503 return assignment.combination === null; 2504 }); 2505} 2506 2507/** 2508 * @param {Object} colorAssignment 2509 * @param {string} combination 2510 * @param {int} colorIndex 2511 * @param {boolean} striped 2512 */ 2513function assignColor(colorAssignment, combination, colorIndex, striped) { 2514 colorAssignment.combination = combination; 2515 colorAssignment.colorIndex = colorIndex; 2516 colorAssignment.striped = striped; 2517 colorAssignment.readyForEviction = false; 2518} 2519 2520/** 2521 * @param {int} colorIndex 2522 * @param {Array} colors 2523 * @return Color from a list 2524 */ 2525function getColor(colorIndex, colors) { 2526 return '#' + colors[colorIndex]; 2527} 2528 2529/** 2530 * @param {string} color 2531 * @param {boolean} striped 2532 * @return Background color or pattern 2533 */ 2534function getBackground(color, striped) { 2535 return striped ? getStripes(color) : color; 2536} 2537 2538/** 2539 * @param {string} color 2540 * @return Canvas pattern from color 2541 */ 2542function getStripes(color) { 2543 if (window.pattern && typeof window.pattern.draw === 'function') { 2544 return window.pattern.draw('diagonal', color); 2545 } 2546 return color; 2547} 2548 2549/** 2550 * @param {boolean} striped 2551 * @return {Array|undefined} An array produces dashed lines on the chart 2552 */ 2553function getBorderDash(striped) { 2554 return striped ? [5, 5] : undefined; 2555} 2556 2557/** 2558 * @param {Array} years 2559 * @param {Array} rows 2560 * @param {Object} combination 2561 * @param {string} labelFallback 2562 * @param {string} color 2563 * @param {string} background 2564 * @param {Array} border 2565 * @param {Array} excess 2566 * @return {Object} Dataset object for Chart.js 2567 */ 2568function makeDataset(years, rows, combination, labelFallback, color, background, border, excess, fill, fillAbove, fillBelow, showLine, spanGaps, allObservationAttributes, mixedTypes) { 2569 var dataset = getBaseDataset(), 2570 prepared = prepareDataForDataset(years, rows, allObservationAttributes), 2571 data = prepared.data, 2572 obsAttributes = prepared.observationAttributes; 2573 console.log("fill, fillAbove, fillBelow:", fill, fillAbove, fillBelow, typeof fill); 2574 return Object.assign(dataset, { 2575 2576 label: getCombinationDescription(combination, labelFallback), 2577 combination: combination, 2578 type: getCombinationType(combination, labelFallback, mixedTypes), 2579 order: getCombinationType(combination, labelFallback, mixedTypes) == undefined ? 0 : 1, 2580 disaggregation: combination, 2581 borderColor: color, 2582 backgroundColor: background, 2583 pointBorderColor: color,
2584 pointBackgroundColor: background, 2585 borderDash: border, 2586 borderWidth: 2, 2587 headline: false, 2588 pointStyle: 'circle', 2589 data: data, 2590 excess: excess, 2591 fill: { 2592 target: fill, 2593 above: fillAbove, 2594 below: fillBelow 2595 }, 2596 spanGaps: spanGaps, 2597 showLine: showLine, 2598 observationAttributes: obsAttributes, 2599 }); 2600} 2601 2602/** 2603 * @return {Object} Starting point for a Chart.js dataset 2604 */ 2605function getBaseDataset() { 2606 return Object.assign({}, { 2607 pointHoverRadius: 5, 2608 pointHoverBorderWidth: 1, 2609 tension: 0, 2610 fill: false, 2611 spanGaps: true, 2612 showLine: true, 2613 maxBarThickness: 150, 2614 //type: 'x', 2615 }); 2616} 2617 2618/** 2619 * @param {Object} combination Key/value representation of a field combo 2620 * @param {string} fallback 2621 * @param {Array} mixedTypes objects containing field, value, type 2622 * @return {string} type of chart for the given combination 2623 */ 2624function getCombinationType(combination, fallback, mixedTypes) { 2625 2626 var combi = getCombinationDescription(combination, fallback); 2627 if (mixedTypes !== undefined && mixedTypes !== null){ 2628 var values = mixedTypes.map(a => a.value); 2629 if (values.indexOf(combi) != -1) { 2630 return mixedTypes.find(function(item) { 2631 return getCombinationDescription([item.value],'') === combi; 2632 }).type; 2633 } 2634 } 2635 else { 2636 return ''; 2637 } 2638 2639} 2640 2641/** 2642 * @param {Object} combination Key/value representation of a field combo 2643 * @param {string} fallback 2644 * @return {string} Human-readable description of combo 2645 */ 2646function getCombinationDescription(combination, fallback) { 2647 var keys = Object.keys(combination); 2648 if (keys.length === 0) { 2649 return fallback; 2650 } 2651 return keys.map(function(key) { 2652 return translations.t(combination[key]); 2653 }).join(', '); 2654} 2655 2656/** 2657 * @param {Array} years 2658 * @param {Array} rows 2659 * @return {Array} Prepared rows 2660 */ 2661 function prepareDataForDataset(years, rows, allObservationAttributes) { 2662 var ret = { 2663 data: [], 2664 observationAttributes: [], 2665 }; 2666 var configObsAttributes = [{"field":"COMMENT_OBS_0","label":""},{"field":"COMMENT_OBS_1","label":""},{"field":"COMMENT_OBS_2","label":""}]; 2667 if (configObsAttributes && configObsAttributes.length > 0) { 2668 configObsAttributes = configObsAttributes.map(function(obsAtt) { 2669 return obsAtt.field; 2670 }); 2671 } 2672 else { 2673 configObsAttributes = []; 2674 } 2675 years.forEach(function(year) { 2676 var found = rows.find(function (row) { 2677 return row[YEAR_COLUMN] === year; 2678 }); 2679 ret.data.push(found ? found[VALUE_COLUMN] : null); 2680 2681 var obsAttributesForRow = []; 2682 if (found) { 2683 configObsAttributes.forEach(function(field) { 2684 if (found[field]) { 2685 var hashKey = field + '|' + found[field]; 2686 obsAttributesForRow.push(allObservationAttributes[hashKey]); 2687 } 2688 }); 2689 } 2690 ret.observationAttributes.push(obsAttributesForRow); 2691 }); 2692 return ret; 2693} 2694 2695 2696 2697/** 2698 * @return {string} Hex number of headline color 2699 * 2700 * TODO: Make this dynamic to support high-contrast. 2701 */ 2702function getHeadlineColor() { 2703 return HEADLINE_COLOR; 2704} 2705 2706/** 2707 * @param {Array} years 2708 * @param {Array} rows 2709 * @param {string} label 2710 * @return {Object} Dataset object for Chart.js 2711 */ 2712function makeHeadlineDataset(years, rows, label, fill, fillAbove, fillBelow, showLine, spanGaps, colors, allObservationAttributes, mixedTypes) { 2713 var dataset = getBaseDataset(), 2714 prepared = prepareDataForDataset(years, rows, allObservationAttributes), 2715 data = prepared.data, 2716 obsAttributes = prepared.observationAttributes; 2717 return Object.assign(dataset, { 2718 label: label, 2719 // Override: no headline color 2720 borderColor: '#a9e13e',//getHeadlineColor(colors), 2721 backgroundColor: '#a9e13e',//getHeadlineColor(colors), 2722 pointBorderColor: '#a9e13e',//getHeadlineColor(colors),
2723 pointBackgroundColor: '#a9e13e',//getHeadlineColor(colors), 2724 borderWidth: 4, 2725 headline: true, 2726 pointStyle: 'circle', 2727 data: data, 2728 observationAttributes: obsAttributes, 2729 fill: { 2730 target: fill, 2731 above: fillAbove, 2732 below: fillBelow 2733 }, 2734 showLine: showLine, 2735 spanGaps: spanGaps, 2736 type: getCombinationType([], '', mixedTypes), 2737 order: getCombinationType([], '', mixedTypes) == '' ? 0 : 1, 2738 }); 2739} 2740 2741 /** 2742 * @param {Array} graphStepsize Objects containing 'unit' and 'title' 2743 * @param {String} selectedUnit 2744 * @param {String} selectedSeries 2745 */ 2746 function getGraphStepsize(graphStepsize, selectedUnit, selectedSeries) { 2747 return getMatchByUnitSeries(graphStepsize, selectedUnit, selectedSeries); 2748} 2749 2750 /** 2751 * Model helper functions related to tables. 2752 */ 2753 2754/** 2755 * @param {Array} datasets 2756 * @param {Array} years 2757 * @return {Object} Object containing 'headings' and 'data' 2758 */ 2759function tableDataFromDatasets(datasets, years) { 2760 return { 2761 headings: [YEAR_COLUMN].concat(datasets.map(function(ds) { return ds.label; })), 2762 data: years.map(function(year, index) { 2763 return [year].concat(datasets.map(function(ds) { return ds.data[index]; })); 2764 }), 2765 }; 2766} 2767 2768/** 2769 * @param {Array} datasets 2770 * @param {Array} years 2771 * @return {Object} Same as tableDataFromDatasets, except values are arrays of observation attributes 2772 */ 2773function observationAttributesTableFromDatasets(datasets, years) { 2774 return { 2775 data: years.map(function(year, index) { 2776 return [null].concat(datasets.map(function(ds) { 2777 return ds.observationAttributes[index] ? ds.observationAttributes[index] : []; 2778 })); 2779 }), 2780 }; 2781} 2782 2783/** 2784 * @param {Array} rows 2785 * @param {string} selectedUnit 2786 * @return {Object} Object containing 'title', 'headings', and 'data' 2787 */ 2788function getHeadlineTable(rows, selectedUnit) { 2789 return { 2790 title: 'Headline data', 2791 headings: selectedUnit ? [YEAR_COLUMN, UNIT_COLUMN, VALUE_COLUMN] : [YEAR_COLUMN, VALUE_COLUMN], 2792 data: rows.map(function (row) { 2793 return selectedUnit ? [row[YEAR_COLUMN], row[UNIT_COLUMN], row[VALUE_COLUMN]] : [row[YEAR_COLUMN], row[VALUE_COLUMN]]; 2794 }), 2795 }; 2796} 2797 2798 /** 2799 * Model helper functions related to data and conversion. 2800 */ 2801 2802/** 2803 * @param {Object} data Object imported from JSON file 2804 * @param {Array} dropKeys Array of keys to drop from the rows 2805 * @return {Array} Rows 2806 */ 2807function convertJsonFormatToRows(data, dropKeys) { 2808 var keys = Object.keys(data); 2809 if (keys.length === 0) { 2810 return []; 2811 } 2812 2813 if (dropKeys && dropKeys.length > 0) { 2814 keys = keys.filter(function(key) { 2815 return !(dropKeys.includes(key)); 2816 }); 2817 } 2818 2819 return data[keys[0]].map(function(item, index) { 2820 return _.zipObject(keys, keys.map(function(key) { 2821 return data[key][index]; 2822 })); 2823 }); 2824} 2825 2826/** 2827 * @param {Array} selectableFields Field names 2828 * @param {Array} rows 2829 * @return {Array} Headline rows 2830 */ 2831function getHeadline(selectableFields, rows) { 2832 return rows.filter(function (row) { 2833 return selectableFields.every(function(field) { 2834 return !row[field]; 2835 }); 2836 }).map(function (row) { 2837 // Remove null fields in each row. 2838 return _.pickBy(row, function(val) { return val !== null }); 2839 }); 2840} 2841 2842/** 2843 * @param {Array} rows 2844 * @return {Array} Prepared rows 2845 */ 2846function prepareData(rows, context) { 2847 return rows.map(function(item) { 2848 2849 if (item[VALUE_COLUMN] != 0) { 2850 // For rounding, use a function that can be set on the global opensdg 2851 // object, for easier control: opensdg.dataRounding() 2852 if (typeof opensdg.dataRounding === 'function') { 2853 item.Value = opensdg.dataRounding(item.Value, context); 2854 } 2855 } 2856 2857 // remove any undefined/null values: 2858 Object.keys(item).forEach(function(key) { 2859 if (item[key] === null || typeof item[key] === 'undefined') { 2860 delete item[key]; 2861 } 2862 }); 2863 2864 return item; 2865 }, this); 2866} 2867 2868/** 2869 * @param {Array} rows 2870 * @param {string} selectedUnit 2871 * @return {Array} Sorted rows 2872 */ 2873function sortData(rows, selectedUnit) { 2874 var column = selectedUnit ? UNIT_COLUMN : YEAR_COLUMN;
2875 return _.sortBy(rows, column); 2876} 2877 2878/** 2879 * @param {Array} precisions Objects containing 'unit' and 'title' 2880 * @param {String} selectedUnit 2881 * @param {String} selectedSeries 2882 * @return {int|false} number of decimal places, if any 2883 */ 2884function getPrecision(precisions, selectedUnit, selectedSeries) { 2885 var match = getMatchByUnitSeries(precisions, selectedUnit, selectedSeries); 2886 return (match) ? match.decimals : false; 2887} 2888 2889/** 2890 * @param {Object} data Object imported from JSON file 2891 * @return {Array} Rows 2892 */ 2893function inputData(data) { 2894 var dropKeys = []; 2895 if (opensdg.ignoredDisaggregations && opensdg.ignoredDisaggregations.length > 0) { 2896 dropKeys = opensdg.ignoredDisaggregations; 2897 } 2898 return convertJsonFormatToRows(data, dropKeys); 2899} 2900 2901/** 2902 * @param {Object} edges Object imported from JSON file 2903 * @return {Array} Rows 2904 */ 2905function inputEdges(edges) { 2906 var edgesData = convertJsonFormatToRows(edges); 2907 if (opensdg.ignoredDisaggregations && opensdg.ignoredDisaggregations.length > 0) { 2908 var ignoredDisaggregations = opensdg.ignoredDisaggregations; 2909 edgesData = edgesData.filter(function(edge) { 2910 if (ignoredDisaggregations.includes(edge.To) || ignoredDisaggregations.includes(edge.From)) { 2911 return false; 2912 } 2913 return true; 2914 }); 2915 } 2916 var configuredObservationAttributes = [{"field":"COMMENT_OBS_0","label":""},{"field":"COMMENT_OBS_1","label":""},{"field":"COMMENT_OBS_2","label":""}]; 2917 if (configuredObservationAttributes && configuredObservationAttributes.length > 0) { 2918 configuredObservationAttributesFlat = configuredObservationAttributes.map(function(att) { return att.field; }); 2919 edgesData = edgesData.filter(function(edge) { 2920 if (configuredObservationAttributesFlat.includes(edge.To) || configuredObservationAttributesFlat.includes(edge.From)) { 2921 return false; 2922 } 2923 return true; 2924 }); 2925 } 2926 return edgesData; 2927} 2928 2929/** 2930 * @param {Array} rows 2931 * @return {Array} Objects containing 'field' and 'value', to be placed in the footer. 2932 */ 2933function getTimeSeriesAttributes(rows) { 2934 if (rows.length === 0) { 2935 return []; 2936 } 2937 var timeSeriesAttributes = [], 2938 possibleAttributes = [{"field":"COMMENT_TS","label":"indicator.footnote"},{"field":"DATA_LAST_UPDATE","label":"metadata_fields.national_data_update_url"}], 2939 firstRow = rows[0], 2940 firstRowKeys = Object.keys(firstRow); 2941 possibleAttributes.forEach(function(possibleAttribute) { 2942 var field = possibleAttribute.field; 2943 if (firstRowKeys.includes(field) && firstRow[field]) { 2944 timeSeriesAttributes.push({ 2945 field: field, 2946 value: firstRow[field], 2947 }); 2948 } 2949 }); 2950 return timeSeriesAttributes; 2951} 2952 2953function getAllObservationAttributes(rows) { 2954 if (rows.length === 0) { 2955 return {}; 2956 } 2957 var obsAttributeHash = {}, 2958 footnoteNumber = 0, 2959 configObsAttributes = [{"field":"COMMENT_OBS_0","label":""},{"field":"COMMENT_OBS_1","label":""},{"field":"COMMENT_OBS_2","label":""}]; 2960 if (configObsAttributes && configObsAttributes.length > 0) { 2961 configObsAttributes = configObsAttributes.map(function(obsAtt) { 2962 return obsAtt.field; 2963 }); 2964 } 2965 else { 2966 configObsAttributes = []; 2967 } 2968 configObsAttributes.forEach(function(field) { 2969 var attributeValues = Object.keys(_.groupBy(rows, field)).filter(function(value) { 2970 return value !== 'undefined'; 2971 }); 2972 attributeValues.forEach(function(attributeValue) { 2973 var hashKey = field + '|' + attributeValue; 2974 obsAttributeHash[hashKey] = { 2975 field: field, 2976 value: attributeValue, 2977 footnoteNumber: footnoteNumber, 2978 } 2979 footnoteNumber += 1; 2980 console.log("obsAttributeHash[hashKey]: ",obsAttributeHash[hashKey]); 2981 }); 2982 }); 2983 console.log("obsAttributeHash: ",obsAttributeHash); 2984 return obsAttributeHash; 2985} 2986 2987 2988 function deprecated(name) { 2989 return function() { 2990 console.log('The ' + name + ' function has been removed. Please update any overridden files.'); 2991 } 2992 } 2993 2994 return { 2995 UNIT_COLUMN: UNIT_COLUMN, 2996 SERIES_COLUMN: SERIES_COLUMN, 2997 GEOCODE_COLUMN: GEOCODE_COLUMN, 2998 YEAR_COLUMN: YEAR_COLUMN, 2999 VALUE_COLUMN: VALUE_COLUMN, 3000 GRAPH_TITLE_FROM_SERIES: GRAPH_TITLE_FROM_SERIES, 3001 convertJsonFormatToRows: convertJsonFormatToRows, 3002 getUniqueValuesByProperty: getUniqueValuesByProperty, 3003 dataHasUnits: dataHasUnits, 3004 dataHasGeoCodes: dataHasGeoCodes, 3005 dataHasSerieses: dataHasSerieses, 3006 getFirstUnitInData: getFirstUnitInData, 3007 getFirstSeriesInData: getFirstSeriesInData, 3008 getDataByUnit: getDataByUnit, 3009 getDataBySeries: getDataBySeries, 3010 getDataBySelectedFields: getDataBySelectedFields, 3011 hasDataBySelectedFields: hasDataBySelectedFields, 3012 getUnitFromStartValues: getUnitFromStartValues, 3013 getSeriesFromStartValues: getSeriesFromStartValues, 3014 selectFieldsFromStartValues: selectFieldsFromStartValues, 3015 selectMinimumStartingFields: selectMinimumStartingFields, 3016 fieldsUsedByUnit: fieldsUsedByUnit, 3017 fieldsUsedBySeries: fieldsUsedBySeries, 3018 dataHasUnitSpecificFields: dataHasUnitSpecificFields, 3019 dataHasSeriesSpecificFields: dataHasSeriesSpecificFields, 3020 getInitialFieldItemStates: getInitialFieldItemStates, 3021 validParentsByChild: validParentsByChild, 3022 getFieldNames: getFieldNames, 3023 getInitialAllowedFields: getInitialAllowedFields, 3024 prepareData: prepareData, 3025 getHeadline: getHeadline, 3026 sortData: sortData, 3027 getHeadlineTable: getHeadlineTable, 3028 removeOrphanSelections: removeOrphanSelections, 3029 getAllowedFieldsWithChildren: getAllowedFieldsWithChildren,
3030 getUpdatedFieldItemStates: getUpdatedFieldItemStates, 3031 fieldItemStatesForView: fieldItemStatesForView, 3032 getChartTitle: getChartTitle, 3033 getChartSubTitle: getChartSubTitle, 3034 getChartType: getChartType, 3035 getCombinationData: getCombinationData, 3036 getDatasets: getDatasets, 3037 tableDataFromDatasets: tableDataFromDatasets, 3038 observationAttributesTableFromDatasets: observationAttributesTableFromDatasets, 3039 sortFieldNames: typeof sortFieldNames !== 'undefined' ? sortFieldNames : function() {}, 3040 sortFieldValueNames: typeof sortFieldValueNames !== 'undefined' ? sortFieldValueNames : function() {}, 3041 getPrecision: getPrecision, 3042 getGraphLimits: getGraphLimits, 3043 getGraphAnnotations: getGraphAnnotations, 3044 getColumnsFromData: getColumnsFromData, 3045 getGraphStepsize: getGraphStepsize, 3046 inputEdges: inputEdges, 3047 getTimeSeriesAttributes: getTimeSeriesAttributes, 3048 getAllObservationAttributes: getAllObservationAttributes, 3049 inputData: inputData, 3050 } 3051})(); 3052 3053 this.helpers = helpers; 3054 3055 // events: 3056 this.onDataComplete = new event(this); 3057 this.onFieldsComplete = new event(this); 3058 this.onUnitsComplete = new event(this); 3059 this.onUnitsSelectedChanged = new event(this); 3060 this.onSeriesesComplete = new event(this); 3061 this.onSeriesesSelectedChanged = new event(this); 3062 this.onFieldsStatusUpdated = new event(this); 3063 this.onFieldsCleared = new event(this); 3064 this.onSelectionUpdate = new event(this); 3065 3066 // general members: 3067 var that = this; 3068 console.log("options.data: ",options.data); 3069 this.data = helpers.inputData(options.data); 3070 console.log("this.data: ", this.data); 3071 this.edgesData = helpers.inputEdges(options.edgesData); 3072 this.hasHeadline = true; 3073 this.country = options.country; 3074 this.indicatorId = options.indicatorId; 3075 this.shortIndicatorId = options.shortIndicatorId; 3076 this.chartTitle = options.chartTitle, 3077 this.chartTitles = options.chartTitles; 3078 this.chartSubtitle = options.chartSubtitle; 3079 this.chartSubtitles = options.chartSubtitles; 3080 this.graphType = options.graphType; 3081 this.graphTypes = options.graphTypes; 3082 this.measurementUnit = options.measurementUnit; 3083 this.xAxisLabel = options.xAxisLabel; 3084 this.startValues = options.startValues; 3085 this.showData = options.showData; 3086 this.showInfo = options.showInfo; 3087 this.selectedFields = []; 3088 this.allowedFields = []; 3089 this.selectedUnit = undefined; 3090 this.fieldsByUnit = undefined; 3091 this.dataHasUnitSpecificFields = false; 3092 this.selectedSeries = undefined; 3093 this.fieldsBySeries = undefined; 3094 this.dataHasSeriesSpecificFields = false; 3095 this.fieldValueStatuses = []; 3096 this.validParentsByChild = {}; 3097 this.hasGeoData = false; 3098 this.showMap = options.showMap; 3099 this.graphLimits = options.graphLimits; 3100 this.stackedDisaggregation = options.stackedDisaggregation; 3101 this.fill = options.fill; 3102 this.fillAbove = options.fillAbove; 3103 this.fillBelow = options.fillBelow; 3104 this.showLine = options.showLine; 3105 this.mixedTypes = options.mixedTypes; // ? options.showLine : true; 3106 this.spanGaps = options.spanGaps; 3107 this.graphAnnotations = options.graphAnnotations; 3108 this.graphTargetLines = options.graphTargetLines; 3109 this.graphSeriesBreaks = options.graphSeriesBreaks; 3110 this.graphErrorBars = options.graphErrorBars; 3111 this.graphTargetPoints = options.graphTargetPoints; 3112 this.graphTargetLabels = options.graphTargetLabels; 3113 this.indicatorDownloads = options.indicatorDownloads; 3114 this.compositeBreakdownLabel = options.compositeBreakdownLabel; 3115 this.precision = options.precision; 3116 this.dataSchema = options.dataSchema; 3117 this.graphStepsize = options.graphStepsize; 3118 this.proxy = options.proxy; 3119 this.proxySerieses = (this.proxy === 'both') ? options.proxySeries : []; 3120 this.observationAttributes = []; 3121 3122 this.initialiseUnits = function() { 3123 if (this.hasUnits) { 3124 this.units = helpers.getUniqueValuesByProperty(helpers.UNIT_COLUMN, this.data); 3125 helpers.sortFieldValueNames(helpers.UNIT_COLUMN, this.units, this.dataSchema); 3126 this.selectedUnit = this.units[0]; 3127 this.fieldsByUnit = helpers.fieldsUsedByUnit(this.units, this.data, this.allColumns); 3128 this.dataHasUnitSpecificFields = helpers.dataHasUnitSpecificFields(this.fieldsByUnit); 3129 } 3130 } 3131 3132 this.refreshSeries = function() { 3133 if (this.hasSerieses) { 3134 if (helpers.GRAPH_TITLE_FROM_SERIES) { 3135 this.chartTitle = this.selectedSeries; 3136 this.chartSubtitle = helpers.getChartSubTitle(this.chartSubtitle, this.chartSubtitles, this.selectedUnit, this.selectedSeries
3136); 3137 } 3138 this.data = helpers.getDataBySeries(this.allData, this.selectedSeries); 3139 this.years = helpers.getUniqueValuesByProperty(helpers.YEAR_COLUMN, this.data).sort(); 3140 this.fieldsBySeries = helpers.fieldsUsedBySeries(this.serieses, this.data, this.allColumns); 3141 this.dataHasSeriesSpecificFields = helpers.dataHasSeriesSpecificFields(this.fieldsBySeries); 3142 } 3143 } 3144 3145 this.initialiseFields = function() { 3146 this.fieldItemStates = helpers.getInitialFieldItemStates(this.data, this.edgesData, this.allColumns, this.dataSchema); 3147 this.validParentsByChild = helpers.validParentsByChild(this.edgesData, this.fieldItemStates, this.data); 3148 this.selectableFields = helpers.getFieldNames(this.fieldItemStates); 3149 this.allowedFields = helpers.getInitialAllowedFields(this.selectableFields, this.edgesData); 3150 } 3151 3152 // Before continuing, we may need to filter by Series, so set up all the Series stuff. 3153 this.allData = helpers.prepareData(this.data, { indicatorId: this.indicatorId }); 3154 console.log("this.allData_2: ",this.allData); 3155 this.allColumns = helpers.getColumnsFromData(this.allData); 3156 this.hasSerieses = helpers.dataHasSerieses(this.allColumns); 3157 this.serieses = this.hasSerieses ? helpers.getUniqueValuesByProperty(helpers.SERIES_COLUMN, this.allData) : []; 3158 this.hasStartValues = Array.isArray(this.startValues) && this.startValues.length > 0; 3159 if (this.hasSerieses) { 3160 helpers.sortFieldValueNames(helpers.SERIES_COLUMN, this.serieses, this.dataSchema); 3161 this.selectedSeries = this.serieses[0]; 3162 if (this.hasStartValues) { 3163 this.selectedSeries = helpers.getSeriesFromStartValues(this.startValues) || this.selectedSeries; 3164 } 3165 this.refreshSeries(); 3166 } 3167 else { 3168 this.data = this.allData; 3169 this.years = helpers.getUniqueValuesByProperty(helpers.YEAR_COLUMN, this.data).sort(); 3170 } 3171 3172 // calculate some initial values: 3173 console.log("this.allData: ",this.allData); 3174 this.allObservationAttributes = helpers.getAllObservationAttributes(this.allData); 3175 this.hasGeoData = helpers.dataHasGeoCodes(this.allColumns); 3176 this.hasUnits = helpers.dataHasUnits(this.allColumns); 3177 this.initialiseUnits(); 3178 this.initialiseFields(); 3179 this.colors = opensdg.chartColors(this.indicatorId); 3180 this.maxDatasetCount = 2 * this.colors.length; 3181 this.colorAssignments = []; 3182 3183 this.clearSelectedFields = function() { 3184 this.selectedFields = []; 3185 this.getData(); 3186 this.onFieldsCleared.notify(); 3187 }; 3188 3189 this.updateFieldStates = function(selectedFields) { 3190 this.selectedFields = helpers.removeOrphanSelections(selectedFields, this.edgesData); 3191 this.allowedFields = helpers.getAllowedFieldsWithChildren(this.selectableFields, this.edgesData, selectedFields); 3192 this.fieldItemStates = helpers.getUpdatedFieldItemStates(this.fieldItemStates, this.edgesData, selectedFields, this.validParentsByChild); 3193 this.onSelectionUpdate.notify({ 3194 selectedFields: this.selectedFields, 3195 allowedFields: this.allowedFields 3196 }); 3197 } 3198 3199 this.updateSelectedFields = function (selectedFields) { 3200 this.updateFieldStates(selectedFields); 3201 this.getData(); 3202 }; 3203 3204 this.updateChartTitle = function() { 3205 this.chartTitle = helpers.getChartTitle(this.chartTitle, this.chartTitles, this.selectedUnit, this.selectedSeries); 3206 } 3207 3208 this.updateChartSubtitle = function() { 3209 this.chartSubtitle = helpers.getChartSubTitle(this.chartSubtitle, this.chartSubtitles, this.selectedUnit, this.selectedSeries); 3210 } 3211 3212 this.updateChartType = function() { 3213 this.graphType = helpers.getChartType(this.graphType, this.graphTypes, this.selectedUnit, this.selectedSeries); 3214 } 3215 3216 this.updateSelectedUnit = function(selectedUnit) { 3217 this.selectedUnit = selectedUnit; 3218 this.getData({ 3219 updateFields: this.dataHasUnitSpecificFields 3220 }); 3221 this.onUnitsSelectedChanged.notify(selectedUnit); 3222 }; 3223 3224 this.updateSelectedSeries = function(selectedSeries) { 3225 // Updating the Series is akin to loading a whole new indicator, so 3226 // here we re-initialise most everything on the page. 3227 this.selectedSeries = selectedSeries; 3228 this.refreshSeries(); 3229 this.clearSelectedFields(); 3230 this.initialiseUnits(); 3231 this.initialiseFields(); 3232 this.getData({ updateFields: true, changingSeries: true });
3233 this.onSeriesesSelectedChanged.notify(selectedSeries); 3234 }; 3235 3236 this.getData = function(options) { 3237 options = Object.assign({ 3238 initial: false, 3239 updateFields: false, 3240 changingSeries: false, 3241 }, options); 3242 3243 var headlineUnfiltered = helpers.getHeadline(this.selectableFields, this.data); 3244 var headline; 3245 if (this.hasUnits && !this.hasSerieses) { 3246 headline = helpers.getDataByUnit(headlineUnfiltered, this.selectedUnit); 3247 } 3248 else if (this.hasSerieses && !this.hasUnits) { 3249 headline = helpers.getDataBySeries(headlineUnfiltered, this.selectedSeries); 3250 } 3251 else if (this.hasSerieses && this.hasUnits) { 3252 headline = helpers.getDataByUnit(headlineUnfiltered, this.selectedUnit); 3253 headline = helpers.getDataBySeries(headline, this.selectedSeries); 3254 } 3255 else { 3256 headline = headlineUnfiltered; 3257 } 3258 3259 // If this is the initial load, check for special cases. 3260 var selectionUpdateNeeded = false; 3261 if (options.initial || options.changingSeries) { 3262 // Decide on a starting unit. 3263 if (this.hasUnits) { 3264 var startingUnit = this.selectedUnit; 3265 if (this.hasStartValues) { 3266 var unitInStartValues = helpers.getUnitFromStartValues(this.startValues); 3267 if (unitInStartValues && this.units.includes(unitInStartValues)) { 3268 startingUnit = unitInStartValues; 3269 } 3270 } 3271 else { 3272 // If our selected unit causes the headline to be empty, change it 3273 // to the first one available that would work. 3274 if (headlineUnfiltered.length > 0 && headline.length === 0) { 3275 startingUnit = helpers.getFirstUnitInData(headlineUnfiltered); 3276 } 3277 } 3278 // Re-query the headline if needed. 3279 if (this.selectedUnit !== startingUnit) { 3280 headline = helpers.getDataByUnit(headlineUnfiltered, startingUnit); 3281 } 3282 this.selectedUnit = startingUnit; 3283 } 3284 3285 // Decide on a starting series. 3286 if (this.hasSerieses && !options.changingSeries) { 3287 var startingSeries = this.selectedSeries; 3288 if (this.hasStartValues) { 3289 var seriesInStartValues = helpers.getSeriesFromStartValues(this.startValues); 3290 if (seriesInStartValues) { 3291 startingSeries = seriesInStartValues; 3292 } 3293 } 3294 else { 3295 // If our selected series causes the headline to be empty, change it 3296 // to the first one available that would work. 3297 if (headlineUnfiltered.length > 0 && headline.length === 0) { 3298 startingSeries = helpers.getFirstSeriesInData(headlineUnfiltered); 3299 } 3300 } 3301 // Re-query the headline if needed. 3302 if (this.selectedSeries !== startingSeries) { 3303 headline = helpers.getDataBySeries(headlineUnfiltered, startingSeries); 3304 } 3305 this.selectedSeries = startingSeries; 3306 } 3307 3308 // Decide on starting field values. 3309 var startingFields = this.selectedFields; 3310 var useMinimumStartingFields = false; 3311 if (this.hasStartValues) { 3312 startingFields = helpers.selectFieldsFromStartValues(this.startValues, this.selectableFields); 3313 // Quick test to see if this would result in zero matches, in cases where 3314 // the series is being changed and the new series would not show data. 3315 if (options.changingSeries && !helpers.hasDataBySelectedFields(this.data, startingFields)) { 3316 useMinimumStartingFields = true; 3317 startingFields = this.selectedFields; 3318 } 3319 } 3320 if (!this.hasStartValues || useMinimumStartingFields) { 3321 if (headline.length === 0) { 3322 startingFields = helpers.selectMinimumStartingFields(this.data, this.selectableFields, this.selectedUnit); 3323 } 3324 } 3325 if (startingFields.length > 0) { 3326 this.selectedFields = startingFields; 3327 selectionUpdateNeeded = true; 3328 } 3329 3330 this.onUnitsComplete.notify({ 3331 units: this.units, 3332 selectedUnit: this.selectedUnit 3333 }); 3334 3335 this.onSeriesesComplete.notify({ 3336 serieses: this.serieses, 3337 selectedSeries: this.selectedSeries, 3338 proxySerieses: this.proxySerieses, 3339 }); 3340 } 3341 3342 if (options.initial || options.updateFields) { 3343 this.onFieldsComplete.notify({ 3344 fields: helpers.fieldItemStatesForView( 3345 this.fieldItemStates, 3346 this.fieldsByUnit, 3347 this.selectedUnit, 3348 this.dataHasUnitSpecificFields, 3349 this.fieldsBySeries, 3350 this.selectedSeries, 3351 this.dataHasSeriesSpecificFields, 3352 this.selectedFields, 3353 this.edgesData, 3354 this.compositeBreakdownLabel 3355 ), 3356 allowedFields: this.allowedFields, 3357 edges: this.edgesData, 3358 hasGeoData: this.hasGeoData, 3359 startValues: this.startValues, 3360 indicatorId: this.indicatorId, 3361 showMap: this.showMap, 3362 precision: helpers.getPrecision(this.precision, this.selectedUnit, this.selectedSeries
3362), 3363 precisionItems: this.precision, 3364 dataSchema: this.dataSchema, 3365 chartTitles: this.chartTitles, 3366 chartSubtitles: this.chartSubtitles, 3367 graphStepsize: helpers.getGraphStepsize(this.graphStepsize, this.selectedUnit, this.selectedSeries), 3368 proxy: this.proxy, 3369 proxySerieses: this.proxySerieses, 3370 }); 3371 } 3372 3373 if (selectionUpdateNeeded || options.updateFields) { 3374 this.updateFieldStates(this.selectedFields); 3375 } 3376 3377 var filteredData = helpers.getDataBySelectedFields(this.data, this.selectedFields); 3378 if (this.hasUnits) { 3379 filteredData = helpers.getDataByUnit(filteredData, this.selectedUnit); 3380 } 3381 3382 var timeSeriesAttributes = []; 3383 if (filteredData.length > 0) { 3384 timeSeriesAttributes = helpers.getTimeSeriesAttributes(filteredData); 3385 } 3386 else if (headline.length > 0) { 3387 timeSeriesAttributes = helpers.getTimeSeriesAttributes(headline); 3388 } 3389 3390 filteredData = helpers.sortData(filteredData, this.selectedUnit); 3391 if (headline.length > 0) { 3392 headline = helpers.sortData(headline, this.selectedUnit); 3393 } 3394 3395 var combinations = helpers.getCombinationData(this.selectedFields, this.dataSchema); 3396 var datasets = helpers.getDatasets(headline, filteredData, combinations, this.years, translations.data.total, this.colors, this.selectableFields, this.colorAssignments, this.fill, this.fillAbove, this.fillBelow, this.showLine, this.spanGaps, this.allObservationAttributes, this.mixedTypes); 3397 var selectionsTable = helpers.tableDataFromDatasets(datasets, this.years); 3398 var observationAttributesTable = helpers.observationAttributesTableFromDatasets(datasets, this.years); 3399 3400 var datasetCountExceedsMax = false; 3401 // restrict count if it exceeds the limit: 3402 if(datasets.length > this.maxDatasetCount) { 3403 datasetCountExceedsMax = true; 3404 } 3405 3406 this.updateChartTitle(); 3407 this.updateChartSubtitle(); 3408 this.updateChartType(); 3409 3410 this.onFieldsStatusUpdated.notify({ 3411 data: this.fieldItemStates, 3412 // TODO: Why is selectionStates not used? 3413 selectionStates: [] 3414 }); 3415 3416 this.onDataComplete.notify({ 3417 datasetCountExceedsMax: datasetCountExceedsMax, 3418 datasets: datasets.filter(function(dataset) { return dataset.excess !== true }), 3419 labels: this.years, 3420 headlineTable: helpers.getHeadlineTable(headline, this.selectedUnit), 3421 selectionsTable: selectionsTable, 3422 observationAttributesTable: observationAttributesTable, 3423 indicatorId: this.indicatorId, 3424 shortIndicatorId: this.shortIndicatorId, 3425 selectedUnit: this.selectedUnit, 3426 selectedSeries: this.selectedSeries, 3427 graphLimits: helpers.getGraphLimits(this.graphLimits, this.selectedUnit, this.selectedSeries), 3428 stackedDisaggregation: this.stackedDisaggregation, 3429 graphAnnotations: helpers.getGraphAnnotations(this.graphAnnotations, this.selectedUnit, this.selectedSeries, this.graphTargetLines, this.graphSeriesBreaks, this.graphErrorBars, this.graphTargetPoints, this.graphTargetLabels), 3430 chartTitle: this.chartTitle, 3431 chartSubtitle: this.chartSubtitle, 3432 chartType: this.graphType, 3433 indicatorDownloads: this.indicatorDownloads, 3434 precision: helpers.getPrecision(this.precision, this.selectedUnit, this.selectedSeries), 3435 graphStepsize: helpers.getGraphStepsize(this.graphStepsize, this.selectedUnit, this.selectedSeries), 3436 timeSeriesAttributes: timeSeriesAttributes, 3437 allObservationAttributes: this.allObservationAttributes, 3438 isProxy: this.proxy === 'proxy' || this.proxySerieses.includes(this.selectedSeries), 3439 }); 3440 }; 3441}; 3442 3443indicatorModel.prototype = { 3444 initialise: function () { 3445 this.getData({ 3446 initial: true 3447 }); 3448 }, 3449 getData: function () { 3450 this.getData(); 3451 } 3452}; 3453var mapView = function () { 3454 3455 "use strict"; 3456 3457 this.initialise = function(indicatorId, precision, precisionItems, decimalSeparator, thousandsSeparator, dataSchema, viewHelpers, modelHelpers, chartTitles, chartSubtitles, startValues, proxy, proxySerieses, allObservationAttributes) { 3458 $('.map').show(); 3459 $('#map').sdgMap({ 3460 indicatorId: indicatorId, 3461 mapOptions: {"disaggregation_controls":true,"minZoom":5,"maxZoom":10,"tileURL":"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}
3461.png","tileOptions":{"id":"mapbox.light","accessToken":"pk.eyJ1IjoibW9ib3NzZSIsImEiOiJjazU1M2trazQwYnFwM2trYmdwNm9rOWxkIn0.u36w-RJPqoTGmivl_zED1w","attribution":"<a href=\"https://www.openstreetmap.org/copyright\">© OpenStreetMap</a> contributors |<br class=\"visible-xs\"> <a href=\"https://www.bkg.bund.de\">© GeoBasis-De / BKG 2026</a> |<br class=\"hidden-lg\"> <a href=\"https://www.destatis.de/DE/Home/_inhalt.html\">© Statistisches Bundesamt (Destatis), 2026</a>"},"colorRange":[["#FCE9EB","#F7BDC4","#F2929D","#ED6676","#E83A4F","#E5243B","#B71D2F","#891623","#5C0E18","#2E070C"],["#FCF8EB","#F7E9C2","#F2DB9A","#EDCD72","#E8BE49","#E5B735","#CEA530","#A08025","#735C1B","#453710"],["#EDF5EB","#C9E2C3","#A6CF9C","#82BC74","#5EA94C","#4C9F38","#3D7F2D","#2E5F22","#1E4016","#0F200B"],["#F9E8EA","#EEBAC0","#E28C96","#D65E6C","#CB3042","#C5192D","#9E1424","#760F1B","#4F0A12","#270509"],["#FFEBE9","#FFC4BC","#FF9D90","#FF7564","#FF4E37","#FF3A21","#CC2E1A","#992314","#66170D","#330C07"],["#E9F8FB","#BEEBF6","#93DEF0","#67D1EA","#3CC4E5","#26BDE2","#1E97B5","#177188","#0F4C5A","#08262D"],["#FFF9E7","#FEEDB6","#FEE185","#FDD554","#FCC923","#FCC30B","#CA9C09","#977507","#654E04","#322702"],["#F6E8EC","#E3BAC6","#D18CA1","#BE5E7B","#AB3055","#A21942","#821435","#610F28","#410A1A","#20050D"],["#FFF0E9","#FED2BE","#FEB492","#FE9666","#FD783B","#FD6925","#CA541E","#983F16","#652A0F","#331507"],["#FCE7F0","#F5B8D1","#EE89B3","#E75A95","#E02B76","#DD1367","#B10F52","#850B3E","#580829","#2C0415"],["#FFF5E6","#FEE2B3","#FECE80","#FEBA4D","#FDA71A","#FD9D00","#CA7E00","#985E00","#653F00","#331F00"],["#FAF5EA","#EFE0C0","#E4CC96","#D9B86C","#CEA342","#C9992D","#A17A24","#795C1B","#503D12","#281F09"],["#ECF2EC","#C5D8C7","#9FBFA2","#79A57C","#528B57","#3F7E44","#326536","#264C29","#19321B","#0D190E"],["#E7F5FB","#B6E0F4","#85CBEC","#54B6E4","#23A1DD","#0A97D9","#0879AE","#065B82","#043C57","#021E2B"],["#EEF9EA","#CCECBF","#ABE095","#89D36B","#67C640","#56C02B","#459A22","#34731A","#224D11","#112609"],["#E6F0F5","#B3D2E2","#80B4CE","#4D95BA","#1A77A7","#00689D","#00537E","#003E5E","#002A3F","#00151F"],["#E8EDF0","#BAC8D2","#8CA4B5","#5E7F97","#305A79","#19486A","#143A55","#0F2B40","#0A1D2A","#050E15"]],"noValueColor":"#f0f0f0","styleNormal":{"weight":1,"opacity":1,"color":"#888","fillOpacity":0.7},"styleHighlighted":{"weight":1,"opacity":1,"color":"#111","fillOpacity":0.7},"styleStatic":{"weight":2,"opacity":1,"fillOpacity":0,"color":"#172d44","dashArray":55}}, 3462 mapLayers: [{"serviceUrl":"https://sdgtestenvironment.github.io/dns-indicators/assets/maps/boundaries.geojson","min_zoom":4.5,"max_zoom":6.5,"staticBorders":true,"subfolder":"laender","label":"indicator.map"},{"serviceUrl":"https://sdgtestenvironment.github.io/dns-indicators/assets/maps/boundariesKrs.geojson","min_zoom":7,"max_zoom":11,"staticBorders":false,"subfolder":"kreise","label":"indicator.map"}], 3463 precision: precision, 3464 precisionItems: precisionItems, 3465 decimalSeparator: decimalSeparator, 3466 thousandsSeparator: thousandsSeparator, 3467 dataSchema: dataSchema, 3468 viewHelpers: viewHelpers, 3469 modelHelpers: modelHelpers, 3470 chartTitles: chartTitles, 3471 chartSubtitles: chartSubtitles, 3472 proxy: proxy, 3473 proxySerieses: proxySerieses, 3474 startValues: startValues, 3475 allObservationAttributes: allObservationAttributes, 3476 }); 3477 }; 3478}; 3479var indicatorView = function (model, options) { 3480 3481 "use strict"; 3482 3483 var MODEL = model, 3484 VIEW = this, 3485 OPTIONS = options; 3486 3487 var helpers = 3488(function() { 3489 3490 var HIDE_SINGLE_SERIES = false; 3491var HIDE_SINGLE_UNIT = true; 3492var PROXY_PILL = '<span aria-describedby="proxy-description" class="proxy-pill">' + translations.t("Proxy") + '</span>'; 3493 3494 /** 3495 * @param {Object} args 3496 * @return null 3497 */ 3498function initialiseFields(args) { 3499 var fieldsContainValues = args.fields.some(function (field) { 3500 return field.values.length > 0; 3501 }); 3502 if (fieldsContainValues) { 3503 var template = _.template($("#item_template").html()); 3504 3505 //if (!$('button#clear').length) { 3506 //$('<button id="clear" disabled="disabled" aria-disabled="true" class="disabled">' + translations.indicator.clear_selections + ' <i class="fa fa-remove"></i></button>').insertBefore('#fields'); 3507 //} 3508 3509 $('#fields').html(template({ 3510 fields: args.fields, 3511 allowedFields: args.allowedFields, 3512 childFields: _.uniq(args.edges.map(function (edge) { return edge.To })), 3513 edges: args.edges 3514 })); 3515 3516 $(OPTIONS.rootElement).removeClass('no-fields'); 3517 3518 } else { 3519 $(OPTIONS.rootElement).addClass('no-fields'); 3520 } 3521} 3522 3523/** 3524 * @return null 3525 */ 3526function updateWithSelectedFields() { 3527 MODEL.updateSelectedFields(_.chain(_.map($('#fields input:checked'), function (fieldValue) { 3528 return { 3529 value: $(fieldValue).val(), 3530 field: $(fieldValue).data('field') 3531 }; 3532 })).groupBy('field').map(function (value, key) { 3533 return { 3534 field: key, 3535 values: _.map(value, 'value') 3536 }; 3537 }
3537).value()); 3538} 3539 3540/** 3541 * @param {Element} fieldGroupElement 3542 * @return null 3543 */ 3544function sortFieldGroup(fieldGroupElement) { 3545 var sortLabels = function (a, b) { 3546 var aObj = { hasData: $(a).attr('data-has-data'), text: $(a).text() }; 3547 var bObj = { hasData: $(b).attr('data-has-data'), text: $(b).text() }; 3548 if (aObj.hasData == bObj.hasData) { 3549 return (aObj.text > bObj.text) ? 1 : -1; 3550 } 3551 return (aObj.hasData < bObj.hasData) ? 1 : -1; 3552 }; 3553 fieldGroupElement.find('label') 3554 .sort(sortLabels) 3555 .appendTo(fieldGroupElement.find('#indicatorData .variable-options')); 3556} 3557 3558/** 3559 * @param {Array} tsAttributeValues 3560 * Array of objects containing 'field' and 'value'. 3561 * @return null 3562 */ 3563function updateTimeSeriesAttributes(tsAttributeValues) { 3564 var timeSeriesAttributes = [{"field":"COMMENT_TS","label":"indicator.footnote"},{"field":"DATA_LAST_UPDATE","label":"metadata_fields.national_data_update_url"}]; 3565 timeSeriesAttributes.forEach(function(tsAttribute) { 3566 var field = tsAttribute.field, 3567 valueMatch = tsAttributeValues.find(function(tsAttributeValue) { 3568 return tsAttributeValue.field === field; 3569 }), 3570 value = (valueMatch) ? valueMatch.value : '', 3571 $labelElement = $('dt[data-ts-attribute="' + field + '"]'), 3572 $valueElement = $('dd[data-ts-attribute="' + field + '"]'); 3573 3574 if (!value) { 3575 $labelElement.hide(); 3576 $valueElement.hide(); 3577 } 3578 else { 3579 $labelElement.show(); 3580 $valueElement.show().text(translations.t(value)); 3581 } 3582 }); 3583} 3584 3585/** 3586 * @param {Array} obsAttributes 3587 * Array of objects containing 'field' and 'value'. 3588 * @return null 3589 */ 3590function updateObservationAttributes(obsAttributes) { 3591 var $listElement = $('.observation-attribute-list'); 3592 $listElement.empty(); 3593 if (obsAttributes.length === 0) { 3594 $listElement.hide(); 3595 return; 3596 } 3597 $listElement.show(); 3598 var listedAttributes = ["test", "test2"]; 3599 Object.values(obsAttributes).forEach(function(obsAttribute) { 3600 var label = getObservationAttributeText(obsAttribute), 3601 num = obsAttribute.footnoteNumber; 3602 3603 if (num == 0){ 3604 var $listItem = $('<dt><u>' + translations.t('+++symbols') + '</u>:</dt>'); 3605 $listElement.append($listItem); 3606 var br = '<br>' 3607 } 3608 else 3609 {var br = ''} 3610 if (listedAttributes.indexOf(label) == -1) { 3611 listedAttributes.push(label); 3612 var $listItem = $('<dd id="observation-footnote-desc-' + num + '" style="margin-bottom: 0px">' + br + label + ' = ' + translations.t('+++' + label) + '</dd>'); 3613 $listElement.append($listItem); 3614 } 3615 }); 3616} 3617 3618/** 3619 * Gets the text of an observation attribute for display to the end user. 3620 */ 3621function getObservationAttributeText(obsAttribute) { 3622 var configuredObsAttributes = [{"field":"COMMENT_OBS_0","label":""},{"field":"COMMENT_OBS_1","label":""},{"field":"COMMENT_OBS_2","label":""}]; 3623 var attributeConfig = _.find(configuredObsAttributes, function(configuredObsAttribute) { 3624 return configuredObsAttribute.field === obsAttribute.field; 3625 }); 3626 if (!attributeConfig) { 3627 return ''; 3628 } 3629 // make sure we do not get 0.000 for obsValue 3630 if (isNaN(parseInt(obsAttribute.value))) { 3631 var label = translations.t(obsAttribute.value); 3632 } 3633 else{ 3634 var label = translations.t(String(parseInt(obsAttribute.value))); 3635 } 3636 //var label = translations.t(obsAttribute.value); 3637 3638 if (attributeConfig.label) { 3639 label = translations.t(attributeConfig.label) + ' = ' + label; 3640 } 3641 return label; 3642} 3643 3644 /** 3645 * @param {Object} args 3646 * @return null 3647 */ 3648function initialiseUnits(args) { 3649 var template = _.template($('#units_template').html()), 3650 units = args.units || [], 3651 selectedUnit = args.selectedUnit || null; 3652 3653 $('#units').html(template({ 3654 units: units, 3655 selectedUnit: selectedUnit 3656 })); 3657 3658 var noUnits = (units.length < 1); 3659 if (HIDE_SINGLE_UNIT) { 3660 noUnits = (units.length < 2); 3661 } 3662 3663 if (noUnits) { 3664 $(OPTIONS.rootElement).addClass('no-units'); 3665 } 3666 else { 3667 $(OPTIONS.rootElement).removeClass('no-units'); 3668 } 3669} 3670 3671/** 3672 * @param {String} selectedUnit 3673 * @return null 3674 */ 3675 function updateUnitElements(selectedUnit) { 3676 var hasUnit = typeof selectedUnit !== 'undefined'; 3677 var fallback = MODEL.measurementUnit; 3678 if (hasUnit || fallback) { 3679 var unitToDisplay = selectedUnit || fallback; 3680 $('.data-controlled-footer-field.unit-from-data').show(); 3681 $('dd.data-controlled-footer-field.unit-from-data').text(translations.t(unitToDisplay)); 3682 } 3683 else { 3684 $('.data-controlled-footer-field.unit-from-data').hide(); 3685 } 3686} 3687 3688 /** 3689 * @param {Object} args 3690 * @return null 3691 */ 3692function initialiseSerieses(args) { 3693 var activeSeriesInput = $('#serieses').find(document.activeElement), 3694 seriesWasFocused = (activeSeriesInput.length > 0) ? true : false, 3695 focusedValue = (seriesWasFocused) ? $(activeSeriesInput).val() : null, 3696 templateElement = $('#series_template'); 3697 if (templateElement.length > 0) { 3698 var template = _.template(templateElement.html()), 3699 serieses = args.serieses || [], 3700 selectedSeries = args.selectedSeries || null, 3701 proxySerieses = args.proxySerieses || []; 3702 $('#serieses').html(template({ 3703 serieses: serieses, 3704 selectedSeries: selectedSeries, 3705 proxySerieses: proxySerieses, 3706 proxyPill: PROXY_PILL, 3707 })); 3708 3709 var noSerieses = (serieses.length < 1); 3710 if (HIDE_SINGLE_SERIES) { 3711 noSerieses = (serieses.length < 2); 3712 } 3713 3714 if (noSerieses) { 3715 $(OPTIONS.rootElement).addClass('no-serieses'); 3716 } 3717 else { 3718 $(OPTIONS.rootElement).removeClass('no-serieses'); 3719 } 3720 } 3721 // Return focus if necessary. 3722 if (seriesWasFocused) {
3723 $('#serieses :input[value="' + focusedValue + '"]').focus(); 3724 } 3725} 3726 3727 /** 3728 * @param {Object} config 3729 * @param {Object} info 3730 * @return null 3731 */ 3732function alterChartConfig(config, info) { 3733 opensdg.chartConfigAlterations.forEach(function (callback) { 3734 callback(config, info); 3735 }); 3736} 3737 3738/** 3739 * @param {String} chartTitle 3740 * @return null 3741 */ 3742function updateChartTitle(chartTitle, isProxy) { 3743 if (typeof chartTitle !== 'undefined') { 3744 if (isProxy) { 3745 chartTitle += ' ' + PROXY_PILL; 3746 } 3747 $('.chart-title').html(chartTitle); 3748 } 3749} 3750 3751/** 3752 * @param {String} chartSubtitle 3753 * @return null 3754 */ 3755function updateChartSubtitle(chartSubtitle) { 3756 if (typeof chartSubtitle !== 'undefined') { 3757 $('.chart-subtitle').text(chartSubtitle); 3758 } 3759} 3760 3761/** 3762 * @param {Array} oldDatasets 3763 * @param {Array} newDatasets 3764 * @return null 3765 */ 3766function updateIndicatorDataViewStatus(oldDatasets, newDatasets) { 3767 var status = '', 3768 hasData = newDatasets.length > 0, 3769 dataAdded = newDatasets.length > oldDatasets.length, 3770 dataRemoved = newDatasets.length < oldDatasets.length, 3771 getDatasetLabel = function (dataset) { return dataset.label; }, 3772 oldLabels = oldDatasets.map(getDatasetLabel), 3773 newLabels = newDatasets.map(getDatasetLabel); 3774 3775 if (!hasData) { 3776 status = translations.indicator.announce_data_not_available; 3777 } 3778 else if (dataAdded) { 3779 status = translations.indicator.announce_data_added; 3780 var addedLabels = [];
3781 newLabels.forEach(function (label) { 3782 if (!oldLabels.includes(label)) { 3783 addedLabels.push(label); 3784 } 3785 }); 3786 status += ' ' + addedLabels.join(', '); 3787 } 3788 else if (dataRemoved) { 3789 status = translations.indicator.announce_data_removed; 3790 var removedLabels = []; 3791 oldLabels.forEach(function (label) { 3792 if (!newLabels.includes(label)) { 3793 removedLabels.push(label); 3794 } 3795 }); 3796 status += ' ' + removedLabels.join(', '); 3797 } 3798 3799 var current = $('#indicator-data-view-status').text(); 3800 if (current != status) { 3801 $('#indicator-data-view-status').text(status); 3802 } 3803} 3804 3805/** 3806 * @param {String} contrast 3807 * @param {Object} chartInfo 3808 * @return null 3809 */ 3810function updateHeadlineColor(contrast, chartInfo, indicatorId) { 3811 var goalNumber = parseInt(indicatorId.slice(indicatorId.indexOf('_')+1,indicatorId.indexOf('-'))); 3812 if (chartInfo.data.datasets.length > 0) { 3813 var firstDataset = chartInfo.data.datasets[0]; 3814 var isHeadline = (typeof firstDataset.disaggregation === 'undefined'); 3815 if (isHeadline) { 3816 var newColor = getHeadlineColor(contrast, goalNumber); 3817 firstDataset.backgroundColor = newColor; 3818 firstDataset.borderColor = newColor;
3819 firstDataset.pointBackgroundColor = newColor; 3820 firstDataset.pointBorderColor = newColor; 3821 } 3822 } 3823} 3824 3825/** 3826 * @param {Array} unit 3827 * @return null 3828 */ 3829function updateIndicatorDataUnitStatus(unit) { 3830 var status = translations.indicator.announce_unit_switched + translations.t(unit); 3831 var current = $('#indicator-data-unit-status').text(); 3832 if (current != status) { 3833 $('#indicator-data-unit-status').text(status); 3834 } 3835} 3836 3837/** 3838 * @param {Array} series 3839 * @return null 3840 */ 3841function updateIndicatorDataSeriesStatus(series) { 3842 var status = translations.indicator.announce_series_switched + translations.t(series); 3843 var current = $('#indicator-data-series-status').text(); 3844 if (current != status) { 3845 $('#indicator-data-series-status').text(status); 3846 } 3847} 3848 3849/** 3850 * @param {String} contrast 3851 * @return {String} The headline color in hex form. 3852 */ 3853//Override: No Headline Color 3854//function getHeadlineColor(contrast) { 3855 //return isHighContrast(contrast) ? '#FFDD00' : '#b8b8b8'; 3856function getHeadlineColor(contrast, goalNumber) { 3857 3858 var headlineColors = ["#e5243b", "#dda63a", "#4c9f38", "#c5192d", "#ff3a21", "#26bde2", "#fcc30b", "#a21942", "#fd6925", "#dd1367", "#fd9d24", "#bf8b2e", "#3f7e44", "#0a97d9", "#56c02b", "#00689d", "#19486a"]; 3859 var headlineColor = headlineColors[goalNumber-1]; 3860 var htmlString = '' + headlineColor + ''; 3861 console.log("goalNumber: ", htmlString); 3862 return isHighContrast(contrast) ? '#FFDD00' : htmlString; 3863} 3864 3865/** 3866 * @param {String} contrast 3867 * @return {String} The grid color in hex form. 3868 */ 3869function getGridColor(contrast) { 3870 return isHighContrast(contrast) ? '#222' : '#ddd'; 3871}; 3872 3873/** 3874 * @param {String} contrast 3875 * @return {String} The tick color in hex form. 3876 */ 3877function getTickColor(contrast) { 3878 return isHighContrast(contrast) ? '#fff' : '#000'; 3879} 3880 3881function getChartConfig(chartInfo) { 3882 var chartType = chartInfo.chartType; 3883 if (typeof opensdg.chartTypes[chartType] === 'undefined') { 3884 console.log('Unknown chart type: ' + chartType + '. Falling back to "line".'); 3885 chartType = 'line'; 3886 } 3887 return opensdg.chartTypes[chartType](chartInfo); 3888} 3889 3890function setPlotEvents(chartInfo) { 3891 window.addEventListener('contrastChange', function (e) { 3892 var gridColor = getGridColor(e.detail); 3893 var tickColor = getTickColor(e.detail); 3894 updateHeadlineColor(e.detail, VIEW._chartInstance, chartInfo.indicatorId); 3895 updateGraphAnnotationColors(e.detail, VIEW._chartInstance); 3896 VIEW._chartInstance.options.scales.y.title.color = tickColor; 3897 VIEW._chartInstance.options.scales.x.title.color = tickColor; 3898 VIEW._chartInstance.options.scales.y.ticks.color = tickColor; 3899 VIEW._chartInstance.options.scales.x.ticks.color = tickColor; 3900 VIEW._chartInstance.options.scales.y.grid.color = function(line) { 3901 return (line.index === 0) ? tickColor : gridColor; 3902 }; 3903 VIEW._chartInstance.options.scales.x.grid.color = function(line) { 3904 return (line.index === 0) ? tickColor : 'transparent'; 3905 }; 3906 3907 VIEW._chartInstance.update(); 3908 $(VIEW._legendElement).html(generateChartLegend(VIEW._chartInstance)); 3909 }); 3910 3911 //createDownloadButton(chartInfo.selectionsTable, 'Chart', chartInfo.indicatorId, '#chartSelectionDownload', chartInfo.selectedSeries, chartInfo.selectedUnit); 3912 //createSourceButton(chartInfo.shortIndicatorId, '#chartSelectionDownload'); 3913 //createIndicatorDownloadButtons(chartInfo.indicatorDownloads, chartInfo.shortIndicatorId, '#chartSelectionDownload'); 3914 3915 $("#btnSave").click(function () { 3916 var filename = chartInfo.indicatorId + '.png', 3917 element = document.getElementById('chart-canvas'), 3918 height = element.clientHeight + 50, 3919 width = element.clientWidth + 50; 3920 var options = { 3921 // These options fix the height, width, and position. 3922 height: height, 3923 width: width, 3924 windowHeight: height, 3925 windowWidth: width, 3926 x: 0, 3927 y: 0, 3928 scrollX: 0, 3929 scrollY: 0, 3930 scale: 2, 3931 backgroundColor: isHighContrast() ? '#000000' : '#FFFFFF', 3932 // Allow a chance to alter the screenshot's HTML. 3933 onclone: function (clone) { 3934 // Add a body class so that the screenshot style can be custom. 3935 clone.body.classList.add('image-download-in-progress'); 3936 }, 3937 // Decide which elements to skip. 3938 ignoreElements: function (el) { 3939 // Keep all style, head, and link elements. 3940 var keepTags = ['STYLE', 'HEAD', 'LINK']; 3941 if (keepTags.indexOf(el.tagName) !== -1) { 3942 return false;
3943 } 3944 // Keep all elements contained by (or containing) the screenshot 3945 // target element. 3946 if (element.contains(el) || el.contains(element)) { 3947 return false; 3948 } 3949 // Leave out everything else. 3950 return true; 3951 } 3952 }; 3953 // First convert the target to a canvas. 3954 html2canvas(element, options).then(function (canvas) { 3955 // Then download that canvas as a PNG file. 3956 canvas.toBlob(function (blob) { 3957 saveAs(blob, filename); 3958 }); 3959 }); 3960 }); 3961} 3962 3963/** 3964 * @param {Object} chartInfo 3965 * @return null 3966 */ 3967function createPlot(chartInfo, helpers) { 3968 3969 var chartConfig = getChartConfig(chartInfo); 3970 chartConfig.indicatorViewHelpers = helpers; 3971 alterChartConfig(chartConfig, chartInfo); 3972 if (isHighContrast()) { 3973 updateGraphAnnotationColors('high', chartConfig); 3974 //Override: No headline color 3975 //updateHeadlineColor('high', chartConfig); 3976 updateHeadlineColor('high', chartConfig, chartInfo.indicatorId); 3977 3978 } 3979 else { 3980 //Override: No headline color 3981 //updateHeadlineColor('default', chartConfig); 3982 updateHeadlineColor('default', chartConfig, chartInfo.indicatorId); 3983 } 3984 refreshChartLineWrapping(chartConfig); 3985 3986 VIEW._chartInstance = new Chart($(OPTIONS.rootElement).find('canvas'), chartConfig); 3987 $(VIEW._legendElement).html(generateChartLegend(VIEW._chartInstance)); 3988}; 3989 3990/** 3991 * @param {Object} chartInfo 3992 * @return null 3993 */ 3994 function updatePlot(chartInfo) { 3995 // If we have changed type, we will need to destroy and recreate the chart. 3996 // So we can abort here. 3997 var updatedConfig = getChartConfig(chartInfo); 3998 if (updatedConfig.type !== VIEW._chartInstance.config.type) { 3999 VIEW._chartInstance.destroy(); 4000 createPlot(chartInfo); 4001 return; 4002 } 4003 updateIndicatorDataViewStatus(VIEW._chartInstance.data.datasets, updatedConfig.data.datasets); 4004 // Override: No headline color 4005 //updateHeadlineColor(isHighContrast() ? 'high' : 'default', updatedConfig); 4006 updateHeadlineColor(isHighContrast() ? 'high' : 'default', updatedConfig, chartInfo.indicatorId); 4007 4008 if (chartInfo.selectedUnit) { 4009 updatedConfig.options.scales.y.title.text = translations.t(chartInfo.selectedUnit); 4010 } 4011 4012 alterChartConfig(updatedConfig, chartInfo); 4013 refreshChartLineWrapping(updatedConfig); 4014 VIEW._chartInstance.config.type = updatedConfig.type; 4015 VIEW._chartInstance.data.datasets = updatedConfig.data.datasets; 4016 VIEW._chartInstance.data.labels = updatedConfig.data.labels; 4017 VIEW._chartInstance.options = updatedConfig.options; 4018 updateGraphAnnotationColors(isHighContrast() ? 'high' : 'default', updatedConfig); 4019 4020 // The following is needed in our custom "rescaler" plugin. 4021 VIEW._chartInstance.data.allLabels = VIEW._chartInstance.data.labels.slice(0); 4022 4023 VIEW._chartInstance.update(); 4024 4025 $(VIEW._legendElement).html(generateChartLegend(VIEW._chartInstance)); 4026 updateChartDownloadButton(chartInfo.selectionsTable, chartInfo.selectedSeries, chartInfo.selectedUnit); 4027}; 4028 4029/** 4030 * @param {String} contrast 4031 * @param {Object} chartInfo 4032 * @return null 4033 */ 4034function updateGraphAnnotationColors(contrast, chartInfo) { 4035 if (chartInfo.options.plugins.annotation) { 4036 chartInfo.options.plugins.annotation.annotations.forEach(function (annotation) { 4037 if (contrast === 'default') { 4038 $.extend(true, annotation, annotation.defaultContrast); 4039 } 4040 else if (contrast === 'high') { 4041 $.extend(true, annotation, annotation.highContrast); 4042 } 4043 }); 4044 } 4045} 4046 4047/** 4048 * @param {Object} chart 4049 * @return {String} The HTML of the chart legend 4050 */ 4051function generateChartLegend(chart) { 4052 var text = []; 4053 text.push('<h5 class="sr-only">' + translations.indicator.plot_legend_description + '</h5>'); 4054 text.push('<ul id="legend" class="legend-for-' + chart.config.type + '-chart">'); 4055 _.each(chart.data.datasets, function (dataset) { 4056 text.push('<li>'); 4057 //text.push('<span class="swatch' + (dataset.borderDash ? ' dashed' : '') + (dataset.headline ? ' headline' : '') + '" style="background-color: ' + dataset.borderColor + '">'); 4058 text.push('<span class="swatch' + (dataset.borderDash ? ' dashed' : '') + '" style="background-color: ' + dataset.borderColor + '">'); 4059 text.push('<span class="swatch-inner" style="background-color: ' + dataset.borderColor + '"></span>'); 4060 text.push('</span>'); 4061 text.push(translations.t(dataset.label)); 4062 text.push('</li>'); 4063 }); 4064 text.push('</ul>'); 4065 return text.join(''); 4066} 4067 4068/** 4069 * @param {Object} chartConfig 4070 */ 4071function refreshChartLineWrapping(chartConfig) { 4072 var yAxisLimit = 40, 4073 wrappedYAxis = strToArray(chartConfig.options.scales.y.title.text, yAxisLimit); 4074 chartConfig.options.scales.y.title.text = wrappedYAxis; 4075} 4076 4077/** 4078 * @param {String} str 4079 * @param {Number} limit 4080 * @returns {Array} The string divided into an array for line wrapping. 4081 */ 4082function strToArray (str, limit) { 4083 var words = str.split(' '), 4084 aux = [], 4085 concat = []; 4086 4087 for (var i = 0; i < words.length; i++) { 4088 concat.push(words[i]); 4089 var join = concat.join(' '); 4090 if (join.length > limit) { 4091 aux.push(join); 4092 concat = []; 4093 } 4094 } 4095 4096 if (concat.length) { 4097 aux.push(concat.join(' ').trim()); 4098 } 4099 4100 return aux; 4101} 4102 4103 opensdg.annotationPresets = { 4104 common: { 4105 // This "common" preset is applied to all annotations automatically. 4106 borderColor: '#949494', 4107 //drawTime: 'afterDraw', 4108 type: 'line', 4109 borderDash: [10, 5], 4110 borderWidth: 1, 4111 label: { 4112 backgroundColor: 'rgba(255,255,255,0.6)', 4113 color: 'black', 4114 }, 4115 // This "highContrast" overrides colors when in high-contrast mode. 4116 highContrast: { 4117 label: { 4118 backgroundColor: 'black', 4119 color: 'white', 4120 }, 4121 }, 4122 // This callback is used to generate a generic description for screenreaders. 4123 // This can be overridden to be a more specific string, eg: 4124 // 4125 // description: 'Chart annotation showing a 2030 target of 15%' 4126 // 4127 description: function() { 4128 var descriptionParts = [translations.indicator.chart_annotation]; 4129 if (this.label && this.label.content) { 4130 descriptionParts.push(translations.t(this.label.content) + ': ' + this.value); 4131 } 4132 else { 4133 // If there is no label, just specify whether it is a box or line.
4134 if (this.type == 'line') { 4135 descriptionParts.push(this.mode + ' line'); 4136 } 4137 if (this.type == 'box') { 4138 descriptionParts.push('box'); 4139 } 4140 } 4141 if (typeof this.value !== 'undefined') { 4142 descriptionParts.push(this.value); 4143 } 4144 return descriptionParts.join(': '); 4145 }, 4146 }, 4147 target_line: { 4148 mode: 'horizontal', 4149 borderWidth: 2, 4150 borderDash: [15, 10], 4151 borderColor: '#757575', 4152 label: { 4153 position: 'end', 4154 content: translations.indicator.annotation_2030_target, 4155 }, 4156 }, 4157 series_break: { 4158 mode: 'vertical', 4159 borderDash: [2, 2], 4160 label: { 4161 position: 'top', 4162 content: translations.indicator.annotation_series_break, 4163 }, 4164 }, 4165 error_bar: { 4166 adjustScaleRange: true, 4167 drawTime: 'afterDatasetsDraw', 4168 type: 'line', 4169 backgroundColor: '#383838', 4170 borderColor: '#383838', 4171 xScaleID: 'x', 4172 yScaleID: 'y', 4173 xMin: 2, 4174 xMax: 2, 4175 yMin: 15000, 4176 yMax: 25000, 4177 }, 4178}; 4179 4180opensdg.chartTypes = opensdg.chartTypes || {}; 4181opensdg.chartTypes.base = function(info) { 4182 4183 var gridColor = getGridColor(); 4184 var tickColor = getTickColor(); 4185 4186 var config = { 4187 type: null, 4188 data: { 4189 datasets: info.datasets, 4190 labels: info.labels, 4191 }, 4192 options: { 4193 layout: { 4194 padding: { 4195 top: 5 4196 } 4197 }, 4198 clip: false, 4199 responsive: true, 4200 maintainAspectRatio: false, 4201 spanGaps: true, 4202 fill: false, 4203 scrollX: true, 4204 scrollCollapse: true, 4205 sScrollXInner: '150%', 4206 scales: { 4207 x: { 4208 grid: { 4209 color: function(line) { 4210 return (line.index === 0) ? tickColor : 'transparent'; 4211 }, 4212 }, 4213 ticks: { 4214 color: tickColor, 4215 }, 4216 title: { 4217 display: MODEL.xAxisLabel ? true : false, 4218 text: MODEL.xAxisLabel, 4219 color: tickColor, 4220 font: { 4221 size: 14, 4222 family: "'Open Sans', Helvetica, Arial, sans-serif", 4223 }, 4224 }, 4225 }, 4226 y: { 4227 grid: { 4228 color: function(line) { 4229 return (line.index === 0) ? tickColor : gridColor; 4230 }, 4231 drawBorder: false, 4232 }, 4233 suggestedMin: 0, 4234 ticks: { 4235 color: tickColor, 4236 callback: function (value) { 4237 return alterDataDisplay(value, undefined, 'chart y-axis tick'); 4238 }, 4239 }, 4240 title: { 4241 display: MODEL.selectedUnit ? translations.t(MODEL.selectedUnit) : MODEL.measurementUnit, 4242 text: MODEL.selectedUnit ? translations.t(MODEL.selectedUnit) : MODEL.measurementUnit, 4243 color: tickColor, 4244 font: { 4245 size: 14, 4246 family: "'Open Sans', Helvetica, Arial, sans-serif", 4247 }, 4248 } 4249 } 4250 }, 4251 plugins: { 4252 scaler: {}, 4253 title: { 4254 display: false 4255 }, 4256 legend: { 4257 display: false, 4258 }, 4259 tooltip: { 4260 usePointStyle: true, 4261 backgroundColor: 'rgba(0,0,0,0.7)', 4262 callbacks: { 4263 label: function (tooltipItem) { 4264 4265 var label = translations.t(tooltipItem.dataset.label); 4266 label = label.replace('<sub>','').replace('</sub>','').replace("<u>","").replace("</u>",""); 4267 if (label.length > 45){ 4268 4269 label = label.split(' '); 4270 var line = ''; 4271 4272 for(var i=0; i<label.length; i++){ 4273 if (line.concat(label[i]).length < 45){ 4274 line = line.concat(label[i] + ' '); 4275 } 4276 else { 4277 break 4278 } 4279 } 4280 return line; 4281 } else { 4282 return label + ': ' + alterDataDisplay(tooltipItem.raw, tooltipItem.dataset, 'chart tooltip', tooltipItem); 4283 } 4284 }, 4285 afterLabel: function(tooltipItem) { 4286 4287 var label = tooltipItem.dataset.label; 4288 label = label.replace('<sub>','').replace('</sub>','').replace('<u>','').replace('</u>',''); 4289 if (label.length > 45){ 4290 label = label.split(' '); 4291 var re = []; 4292 var line = ''; 4293 for (var i=0; i<label.length; i++){ 4294 if (line.concat(label[i]).length < 45){ 4295 line = line.concat(label[i] + ' '); 4296 } else { 4297 re.push(line); 4298 line = ''; 4299 line = line.concat(label[i] + ' '); 4300 } 4301 }; 4302 re.push(line.slice(0, -1) + ': ' + alterDataDisplay(tooltipItem.raw, tooltipItem.dataset, 'chart tooltip', tooltipItem)); 4303 re.shift(); 4304 } 4305 return re; 4306 }, 4307 afterBody: function () { 4308 var unit = MODEL.selectedUnit ? translations.t(MODEL.selectedUnit) : MODEL.measurementUnit; 4309 if (typeof unit !== 'undefined' && unit !== '') { 4310 return '\n' + translations.indicator.unit + ': ' + unit; 4311 } 4312 }, 4313 }, 4314 }, 4315 }, 4316 } 4317 }; 4318 4319 if (info.graphLimits && Object.keys(info.graphLimits).length > 0) { 4320 var overrides = { 4321 options: { 4322 scales: { 4323 y: { 4324 min: info.graphLimits.minimum, 4325 max: info.graphLimits.maximum, 4326 } 4327 } 4328 } 4329 } 4330 // Add these overrides onto the normal config. 4331 _.merge(config, overrides); 4332 } 4333 else { 4334 // Otherwise remove any min/max that may be there. 4335 try { 4336 delete config.options.scales.y.min; 4337 delete config.options.scales.y.max; 4338 } 4339 catch (e) { } 4340 } 4341 4342 if (info.graphStepsize && Object.keys(info.graphStepsize).length > 0) { 4343 var overrides = { 4344 options: { 4345 scales: { 4346 y: { 4347 ticks: { 4348 stepSize: info.graphStepsize.step, 4349 } 4350 } 4351 } 4352 } 4353 } 4354 // Add these overrides onto the normal config. 4355 $.extend(true, config, overrides); 4356 } 4357 4358 if (info.graphAnnotations && info.graphAnnotations.length > 0) { 4359 // Apply some helpers/fixes to the annotations. 4360 var annotations = info.graphAnnotations.map(function(annotationOverrides) { 4361 var annotation = {}; 4362 // Apply the "common" preset. 4363 if (opensdg.annotationPresets.common) { 4364 $.extend(true, annotation, opensdg.annotationPresets.common); 4365 } 4366 // Apply any specified presets. 4367 if (annotationOverrides.preset) { 4368 var preset = annotationOverrides.preset; 4369 if (opensdg.annotationPresets[preset]) { 4370 $.extend(true, annotation, opensdg.annotationPresets[preset]); 4371 } 4372 } 4373 // Now add any more annotation config. 4374 $.extend(true, annotation, annotationOverrides); 4375 // Default to horizontal lines. 4376 if (!annotation.mode && annotation.type === 'line' && annotation.preset !== 'error_bar') { 4377 annotation.mode = 'horizontal'; 4378 } 4379 // Provide the obscure scaleID properties on user's behalf. 4380 if (!annotation.scaleID && annotation.type === 'line' && annotation.preset !== 'error_bar' && annotation.preset !== 'target_point' && annotation.preset !== 'target_label') { 4381 if (annotation.mode === 'horizontal') { 4382 annotation.scaleID = 'y'; 4383 } 4384 if (annotation.mode === 'vertical') { 4385 annotation.scaleID = 'x'; 4386 } 4387 } 4388 if (!annotation.xScaleID && (annotation.type === 'box' || annotation.type === 'point')) { 4389 annotation.xScaleID = 'x'; 4390 } 4391 if (!annotation.yScaleID && (annotation.type === 'box' || annotation.type === 'point')) { 4392 annotation.yScaleID = 'y'; 4393 } 4394 // Provide the "enabled" label property on the user's behalf. 4395 if (annotation.label && annotation.label.content) { 4396 annotation.label.enabled = true; 4397 } 4398 // Translate any label content. 4399 if (annotation.label && annotation.label.content) { 4400 annotation.label.content = translations.t(annotation.label.content); 4401 } 4402 // Fix some keys where there was once a discrepancy between 4403 // Open SDG and Chart.js. Eg, we mistakenly used "fontColor" 4404 // instead of "color". 4405 if (annotation.label && annotation.label.fontColor) { 4406 annotation.label.color = annotation.label.fontColor; 4407 } 4408 if (annotation.highContrast && annotation.highContrast.label) { 4409 if (annotation.highContrast.label.fontColor) { 4410 annotation.highContrast.label.color = annotation.highContrast.label.fontColor; 4411 } 4412 } 4413 // We also used the wrong values for label position. 4414 if (annotation.label && 4415 annotation.label.position && 4416 (annotation.label.position == 'top' || 4417 annotation.label.position == 'left')) { 4418 // It should be 'start' instead of 'top' or 'left'. 4419 annotation.label.position = 'start'; 4420 } 4421 if (annotation.label && 4422 annotation.label.position && 4423 (annotation.label.position == 'bottom' || 4424 annotation.label.position == 'right')) { 4425 // It should be 'start' instead of 'top' or 'left'. 4426 annotation.label.position = 'end'; 4427 } 4428 // Save some original values for later used when contrast mode is switched. 4429 if (typeof annotation.defaultContrast === 'undefined') { 4430 annotation.defaultContrast = {}; 4431 if (annotation.borderColor) { 4432 annotation.defaultContrast.borderColor = annotation.borderColor; 4433 } 4434 if (annotation.backgroundColor) { 4435 annotation.defaultContrast.backgroundColor = annotation.backgroundColor; 4436 } 4437 if (annotation.label) { 4438 annotation.defaultContrast.label = {}; 4439 if (annotation.label.color) { 4440 annotation.defaultContrast.label.color = annotation.label.color; 4441 } 4442 if (annotation.label.backgroundColor) { 4443 annotation.defaultContrast.label.backgroundColor = annotation.label.backgroundColor; 4444 } 4445 if (annotation.label.borderWidth) { 4446 annotation.defaultContrast.label.borderWidth = annotation.label.borderWidth; 4447 } 4448 if (annotation.label.borderColor) { 4449 annotation.defaultContrast.label.borderColor = annotation.label.borderColor; 4450 } 4451 } 4452 } 4453 return annotation; 4454 }); 4455 if (annotations.length > 0) { 4456 var overrides = { 4457 options: { 4458 plugins: { 4459 annotation: { 4460 drawTime: 'afterDatasetsDraw', 4461 annotations: annotations 4462 } 4463 } 4464 } 4465 }; 4466 // Add these overrides onto the normal config. 4467 _.merge(config, overrides); 4468 4469 // Update the chart annotations element. 4470 var descriptions = annotations.map(function(annotation) { 4471 var description = ''; 4472 if (annotation.description) { 4473 if (typeof annotation.description === 'function') { 4474 description = annotation.description.call(annotation); 4475 } 4476 else { 4477 description = translations.t(annotation.description); 4478 } 4479 } 4480 return description; 4481 }).filter(function(description) { return description != ''; }); 4482
4483 var currentDescription = $('#chart-annotations').text(); 4484 var newDescription = descriptions.join('. '); 4485 if (currentDescription != newDescription) { 4486 $('#chart-annotations').text(newDescription); 4487 } 4488 } 4489 } 4490 else if (config.options && config.options.annotation) { 4491 delete config.options.annotation; 4492 } 4493 4494 return config; 4495} 4496 4497 opensdg.chartTypes.line = function(info) { 4498 var config = opensdg.chartTypes.base(info); 4499 var overrides = { 4500 type: 'line', 4501 options: { 4502 plugins: { 4503 tooltip: { 4504 mode: 'index', 4505 intersect: false, 4506 }, 4507 }, 4508 }, 4509 plugins: [{ 4510 beforeDatasetsDraw: function(chart) { 4511 if (chart.tooltip._active && chart.tooltip._active.length) { 4512 var activePoint = chart.tooltip._active[0], 4513 ctx = chart.ctx, 4514 x = activePoint.element.x, 4515 topY = chart.scales.y.top, 4516 bottomY = chart.scales.y.bottom; 4517 4518 // draw line 4519 ctx.save(); 4520 ctx.beginPath(); 4521 ctx.moveTo(x, topY); 4522 ctx.lineTo(x, bottomY); 4523 ctx.lineWidth = 2; 4524 ctx.strokeStyle = '#757575'; 4525 ctx.stroke(); 4526 ctx.restore(); 4527 } 4528 } 4529 }], 4530 }; 4531 4532 4533 if (info.graphStepsize && Object.keys(info.graphStepsize).length > 0) { 4534 overrides.options = { 4535 scales: { 4536 yAxes: [{ 4537 ticks: { 4538 stepSize: info.graphStepsize.step, 4539 } 4540 }] 4541 } 4542 }; 4543 } 4544 4545 // Add these overrides onto the normal config, and return it. 4546 _.merge(config, overrides); 4547 return config; 4548} 4549 4550 opensdg.chartTypes.bar = function (info) { 4551 var config = opensdg.chartTypes.base(info); 4552 var overrides = { 4553 type: 'bar', 4554 borderWidth: 0, 4555 }; 4556 if (info.stackedDisaggregation) { 4557 console.log('Stacked', info.stackedDisaggregation, typeof info.stackedDisaggregation); 4558 overrides.options = { 4559 scales: { 4560 x: { stacked: true }, 4561 y: { stacked: true }, 4562 } 4563 }; 4564 // If we have stackedDisaggregation, we need to group datasets into stacks. 4565 config.data.datasets.forEach(function (dataset) { 4566 var disaggregation = $.extend({}, dataset.disaggregation); 4567 // We're going to "stringify" each combination of disaggregations in order 4568 // to place them in their own "stacks". To place "stacked" disaggregations 4569 // into the same stack, we set them as "samestack" before stringifying. 4570 // Note that the string "samestack" is completely arbitrary. 4571 if (typeof disaggregation[info.stackedDisaggregation] !== 'undefined') { 4572 disaggregation[info.stackedDisaggregation] = 'samestack'; 4573 } 4574 // Use the disaggregation as a unique id for each stack. 4575 dataset.stack = JSON.stringify(disaggregation); 4576 }); 4577 } 4578 4579 if (info.graphStepsize && Object.keys(info.graphStepsize).length > 0) { 4580 overrides.options = { 4581 scales: { 4582 yAxes: [{ 4583 ticks: { 4584 stepSize: info.graphStepsize.step, 4585 } 4586 }] 4587 } 4588 }; 4589 } 4590 4591 // Manually set the borderWidths to 0 to avoid a weird border effect on the bars. 4592 // exception for line datasets in a mixed chart 4593 config.data.datasets.forEach(function(dataset) { 4594 if (dataset.type == 'line') { 4595 dataset.borderWidth = 2; 4596 } 4597 else { 4598 dataset.borderWidth = 0; 4599 } 4600 //dataset.borderWidth = 0; 4601 }); 4602 config.data.datasets.forEach(function(dataset) { 4603 if (dataset.type == 'line') { 4604 dataset.order = 0; 4605 } 4606 else { 4607 dataset.order = 1; 4608 } 4609 }); 4610 // Add these overrides onto the normal config, and return it. 4611 _.merge(config, overrides); 4612 return config; 4613} 4614 4615 opensdg.convertBinaryValue = function (value) { 4616 if (typeof value === 'string') { 4617 value = parseInt(value, 10); 4618 } 4619 if (value === 1) { 4620 return translations.indicator.affirmative; 4621 } 4622 else if (value === -1) { 4623 return translations.indicator.negative; 4624 } 4625 return ''; 4626} 4627 4628opensdg.chartTypes.binary = function (info) { 4629 var config = opensdg.chartTypes.base(info); 4630 var overrides = { 4631 // Force the "bar" type instead of the "binary" type which Chart.js 4632 // does not recognize. 4633 type: 'bar', 4634 // Assign some callbacks to convert 1/-1 to Yes/No. 4635 options: { 4636 plugins: { 4637 tooltip: { 4638 callbacks: { 4639 label: function (tooltipItem) { 4640 var label = tooltipItem.dataset.label || ''; 4641 label += ': ' + opensdg.convertBinaryValue(tooltipItem.formattedValue); 4642 return label; 4643 }, 4644 }, 4645 }, 4646 }, 4647 scales: { 4648 y: { 4649 // Set the min/max to -1/1 so that the bars will start from the 4650 // middle and either go up (for 1) or down (for -1). 4651 min: -1, 4652 max: 1, 4653 ticks: { 4654 callback: opensdg.convertBinaryValue, 4655 }, 4656 }, 4657 }, 4658 } 4659 } 4660 4661 // Tweak the data so that 0 is treated as -1. This is done so that the bar appears 4662 // to be pointed down. 4663 config.data.datasets = config.data.datasets.map(function(dataset) { 4664 dataset.data = dataset.data.map(function(value) { 4665 if (value === 0) { 4666 return -1; 4667 } 4668 return value; 4669 }); 4670 return dataset; 4671 }); 4672 4673 // Manually set the borderWidths to 0 to avoid a weird border effect on the bars.
4674 config.data.datasets.forEach(function(dataset) { 4675 dataset.borderWidth = 0; 4676 }); 4677 4678 // Add these overrides onto the normal config. 4679 _.merge(config, overrides); 4680 return config; 4681}; 4682 4683 /** 4684 * @param {Object} config 4685 * @param {Object} info 4686 * @return null 4687 */ 4688function alterTableConfig(config, info) { 4689 opensdg.tableConfigAlterations.forEach(function (callback) { 4690 callback(config, info); 4691 }); 4692} 4693 4694/** 4695 * @param {Object} tableData 4696 * @return {String} 4697 */ 4698function toCsv(tableData, selectedSeries, selectedUnit) { 4699 var lines = [], 4700 dataHeadings = _.map(tableData.headings, function (heading) { return '"' + translations.t(heading) + '"'; }), 4701 metaHeadings = []; 4702 4703 if (selectedSeries) { 4704 metaHeadings.push(translations.indicator.series); 4705 } 4706 if (selectedUnit) { 4707 metaHeadings.push(translations.indicator.unit); 4708 } 4709 var allHeadings = dataHeadings.concat(metaHeadings); 4710 4711 lines.push(allHeadings.join(',')); 4712 4713 _.each(tableData.data, function (dataValues) { 4714 var line = []; 4715 4716 _.each(dataHeadings, function (heading, index) { 4717 line.push(dataValues[index]); 4718 }); 4719 if (selectedSeries) { 4720 line.push(JSON.stringify(translations.t(selectedSeries))); 4721 } 4722 if (selectedUnit) { 4723 line.push(JSON.stringify(translations.t(selectedUnit))); 4724 } 4725 4726 lines.push(line.join(',')); 4727 }); 4728 4729 return lines.join('\n'); 4730} 4731 4732/** 4733 * @param {Element} el 4734 * @param {Object} info 4735 * @return null 4736 */ 4737function initialiseDataTable(el, info) { 4738 var nonYearColumns = []; 4739 for (var i = 1; i < info.table.headings.length; i++) { 4740 nonYearColumns.push(i); 4741 } 4742 4743 var datatables_options = OPTIONS.datatables_options || { 4744 paging: false, 4745 bInfo: false, 4746 bAutoWidth: false, 4747 searching: false, 4748 responsive: false, 4749 order: [[0, 'asc']], 4750 columnDefs: [ 4751 { 4752 targets: nonYearColumns, 4753 createdCell: function (td, cellData, rowData, row, col) { 4754 var additionalInfo = Object.assign({}, info); 4755 additionalInfo.row = row; 4756 additionalInfo.col = col; 4757 if (info.chartType === 'binary') { 4758 var cellDataInt = Number(cellData); 4759 if (cellDataInt === 1) { 4760 cellData = translations.indicator.affirmative; 4761 } 4762 else if (cellDataInt === 0 || cellDataInt === -1) { 4763 cellData = translations.indicator.negative; 4764 } 4765 } 4766 $(td).text(alterDataDisplay(cellData, rowData, 'table cell', additionalInfo)); 4767 }, 4768 }, 4769 ], 4770 }, table = $(el).find('table'); 4771 4772 datatables_options.aaSorting = []; 4773 4774 alterTableConfig(datatables_options, info); 4775 table.DataTable(datatables_options); 4776 table.removeAttr('role'); 4777 table.find('thead th').removeAttr('rowspan').removeAttr('colspan').removeAttr('aria-label'); 4778 setDataTableWidth(table); 4779}; 4780 4781/** 4782 * @param {Object} chartInfo 4783 * @return null 4784 */ 4785function createSelectionsTable(chartInfo) { 4786 createTable(chartInfo.selectionsTable, chartInfo.indicatorId, '#selectionsTable', chartInfo.isProxy, chartInfo.observationAttributesTable, chartInfo.chartType); 4787 $('#tableSelectionDownload').empty(); 4788 createTableTargetLines(chartInfo.graphAnnotations); 4789 createDownloadButton(chartInfo.selectionsTable, 'Table', chartInfo.indicatorId, '#tableSelectionDownload', chartInfo.selectedSeries, chartInfo.selectedUnit); 4790 createSourceButton(chartInfo.shortIndicatorId, '#tableSelectionDownload'); 4791 createIndicatorDownloadButtons(chartInfo.indicatorDownloads, chartInfo.shortIndicatorId, '#tableSelectionDownload'); 4792}; 4793 4794/** 4795 * @param {Array} graphAnnotations 4796 * @return null 4797 */ 4798function createTableTargetLines(graphAnnotations) { 4799 // var targetLines = graphAnnotations.filter(function (a) { return a.preset === 'target_line'; }); 4800 // var $targetLines = $('#tableTargetLines'); 4801 // $targetLines.empty();
4802 // targetLines.forEach(function (targetLine) { 4803 // var targetLineLabel = targetLine.label.content; 4804 // if (!targetLineLabel) { 4805 // targetLineLabel = opensdg.annotationPresets.target_line.label.content; 4806 // } 4807 // $targetLines.append('<dt>' + targetLineLabel + '</dt><dd>' + alterDataDisplay(targetLine.value, targetLine, 'target line') + '</dd>'); 4808 // }); 4809 // if (targetLines.length === 0) { 4810 // $targetLines.hide(); 4811 // } 4812 // else { 4813 // $targetLines.show(); 4814 // } 4815} 4816 4817/** 4818 * @param {Object} table 4819 * @return bool 4820 */ 4821function tableHasData(table) { 4822 for (var i = 0; i < table.data.length; i++) { 4823 if (table.data[i].length > 1) { 4824 return true; 4825 } 4826 } 4827 return false; 4828} 4829 4830/** 4831 * @param {Object} table 4832 * @param {String} indicatorId 4833 * @param {Element} el 4834 * @param {bool} isProxy 4835 * @param {Object} observationAttributesTable 4836 * @param {String} chartType 4837 * @return null 4838 */ 4839function createTable(table, indicatorId, el, isProxy, observationAttributesTable, chartType) { 4840 4841 var table_class = OPTIONS.table_class || 'table table-hover'; 4842 4843 // clear: 4844 $(el).html(''); 4845 4846 if (table && tableHasData(table)) { 4847 var currentTable = $('<table />').attr({ 4848 'class': table_class, 4849 'width': '100%' 4850 }); 4851 var tableTitle = MODEL.chartTitle; 4852 if (isProxy) { 4853 tableTitle += ' ' + PROXY_PILL; 4854 } 4855 if (MODEL.chartSubtitle) { 4856 currentTable.append('<caption>' + tableTitle + '<br><small>' + MODEL.chartSubtitle + '</small></caption>'); 4857 } else { 4858 currentTable.append('<caption>' + tableTitle + '<br></caption>'); 4859 } 4860 var table_head = '<thead><tr>'; 4861 4862 var getHeading = function (heading, index) { 4863 var arrows = '<span class="sort"><i class="fa fa-sort"></i><i class="fa fa-sort-down"></i><i class="fa fa-sort-up"></i></span>'; 4864 var button = '<span tabindex="0" role="button" aria-describedby="column-sort-info">' + translations.t(heading) + '</span>'; 4865 return button + arrows; 4866 }; 4867
4868 table.headings.forEach(function (heading, index) { 4869 table_head += '<th' + (!index ? '' : ' class="table-value"') + ' scope="col">' + getHeading(heading, index) + '</th>'; 4870 }); 4871 4872 table_head += '</tr></thead>'; 4873 currentTable.append(table_head); 4874 currentTable.append('<tbody></tbody>'); 4875 var row = -1; 4876 4877 table.data.forEach(function (data) { 4878 row += 1; 4879 var col = -1; 4880 var row_html = '<tr>'; 4881 var obsValue = ''; 4882 4883 //(observationAttributesTable.data[row][1][0] !== undefined ? obsValue = observationAttributesTable.data[row][1][0].value : obsValue = '.'); 4884 table.headings.forEach(function (heading, index) { 4885 col += 1; 4886 // For accessibility set the Year column to a "row" scope th. 4887 // No observation values for years 4888 if (col == 0) { 4889 obsValue = '' 4890 } 4891 // case: no obs attributes defined --> '' if we have a value and '.' if not 4892 else if (observationAttributesTable.data[row][col].length == 0) { 4893 (data[index] !== null && data[index] !== undefined ? obsValue = '' : obsValue = '.') 4894 } 4895 // case: Obs values are defined --> adding up all obs values separated by comma 4896 else { 4897 obsValue = ''; 4898 for (var i = 0; i < observationAttributesTable.data[row][col].length; i++) { 4899 obsValue += (i == 0 ? observationAttributesTable.data[row][col][i].value : (', ' + observationAttributesTable.data[row][col][i].value)); 4900 }; 4901 } 4902 //(observationAttributesTable.data[row][1][0] !== undefined ? obsValue = observationAttributesTable.data[row][1][0].value : obsValue = '.'); 4903 var isYear = (index == 0); 4904 var cell_prefix = (isYear) ? '<th scope="row" tabindex="0"' : '<td tabindex="0"'; 4905 var cell_suffix = (isYear) ? '</th>' : '</td>'; 4906 4907 //var dateForTable = (data[index] == 0 && obsValue.indexOf('â') > -1) ? ('â' + obsValue.replace('â, ','').replace(', â','').replace('[â]','')) : (data[index] + ' ' + obsValue); 4908 //var dateForTable = (data[index] == 0 && obsValue.indexOf('0') > -1) ? ('0' + obsValue.replace('0, ','').replace(', 0','').replace('[0]','')) : (data[index] + ' ' + obsValue); 4909 // case: datapoint == 0 --> we do not want 0 plus obsValue in tabel but only 0 (or 0.00) or - feventually ollowed by other obs values 4910 var dateForTable = '' 4911 if (data[index] == 0){ 4912 // case: only one obs-value (0 or -) --> show only the obs-value instead of the value 4913 if (obsValue.length == 1){ 4914 dateForTable = obsValue; 4915 } 4916 // case: more than one obs-value --> setting the value to 0 or - and replace it in the obs-value by '' 4917 else{ 4918 if (obsValue.indexOf('â') > -1){ 4919 dateForTable = 'â [' + obsValue.replace('â, ','').replace(', â','') + ']'; 4920 } 4921 else{ 4922 var deci = ['0', '0.0', '0.00', '0.000'] 4923 for (var i = 0; i < deci.length; i++) { 4924 dateForTable = data[index] + ' [' + obsValue.replace('' + deci[i] + ', ','').replace(', ' + deci[i] + '','') + ']'; 4925 } 4926 //dateForTable = data[index] + ' [' + obsValue.replace('0, ','').replace(', 0','') + ']'; 4927 } 4928 } 4929 } 4930 // case: datapoint is not zero --> datapoint plus obs-value 4931 else { 4932 dateForTable = (data[index] + ' ' + obsValue); 4933 } 4934 4935 //var cell_content = (isYear) ? translations.t(data[index]) : data[index]; 4936 //row_html += cell_prefix + (isYear ? '' : ' class="table-value"') + '>' + (cell_content !== null && cell_content !== undefined ? cell_content : '.') + cell_suffix; 4937 //row_html += cell_prefix + (isYear ? '' : ' class="table-value"') + '>
4937' + (data[index] !== null && data[index] !== undefined ? (data[index] + ' ' + obsValue) : obsValue) + cell_suffix; 4938 row_html += cell_prefix + (isYear ? '' : ' class="table-value"') + '>' + (data[index] !== null && data[index] !== undefined ? dateForTable : obsValue) + cell_suffix; 4939 }); 4940 row_html += '</tr>'; 4941 currentTable.find('tbody').append(row_html); 4942 }); 4943 4944 $(el).append(currentTable); 4945 4946 // initialise data table and provide some info for alterations. 4947 var alterationInfo = { 4948 table: table, 4949 indicatorId: indicatorId, 4950 observationAttributesTable: observationAttributesTable, 4951 chartType: chartType, 4952 }; 4953 initialiseDataTable(el, alterationInfo); 4954 4955 $(el).removeClass('table-has-no-data'); 4956 $('#selectionTableFooter').show(); 4957 4958 $(el).find('th') 4959 .removeAttr('tabindex') 4960 .click(function () { 4961 var sortDirection = $(this).attr('aria-sort'); 4962 $(this).find('span[role="button"]').attr('aria-sort', sortDirection); 4963 }); 4964 4965 let tableWrapper = document.querySelector('.dataTables_wrapper'); 4966 if (tableWrapper) { 4967 tableWrapper.addEventListener('scroll', function(e) { 4968 if (tableWrapper.scrollLeft > 0) { 4969 tableWrapper.classList.add('scrolled-x'); 4970 } 4971 else { 4972 tableWrapper.classList.remove('scrolled-x'); 4973 } 4974 if (tableWrapper.scrollTop > 0) { 4975 tableWrapper.classList.add('scrolled-y'); 4976 } 4977 else { 4978 tableWrapper.classList.remove('scrolled-y'); 4979 } 4980 }); 4981 } 4982 } else { 4983 $(el).append($('<h3 />').text(translations.indicator.data_not_available)); 4984 $(el).addClass('table-has-no-data'); 4985 $('#selectionTableFooter').hide(); 4986 } 4987} 4988 4989/** 4990 * @param {Object} table 4991 * @return null 4992 */ 4993function setDataTableWidth(table) { 4994 table.find('thead th').each(function () { 4995 var textLength = $(this).text().length; 4996 for (var loop = 0; loop < VIEW._tableColumnDefs.length; loop++) { 4997 var def = VIEW._tableColumnDefs[loop]; 4998 if (textLength < def.maxCharCount) { 4999 if (!def.width) { 5000 $(this).css('white-space', 'nowrap'); 5001 } else { 5002 $(this).css('width', def.width + 'px'); 5003 $(this).data('width', def.width); 5004 } 5005 break; 5006 } 5007 } 5008 }); 5009 5010 table.removeAttr('style width'); 5011 table.css('width', '100%'); 5012 // var totalWidth = 0; 5013 // var column = 0; 5014 // table.find('thead th').each(function () { 5015 // column += 1; 5016 // if ($(this).data('width')) { 5017 // totalWidth += $(this).data('width'); 5018 // console.log('a) Column ', column, ': ', $(this).data('width'), ', Total: ' + totalWidth); 5019 // } else { 5020 // totalWidth += $(this).width(); 5021 // console.log('b) Column ', column + ': ', $(this).width(), ', Total: ' + totalWidth); 5022 // } 5023 // }); 5024 5025 // ascertain whether the table should be width 100% or explicit width: 5026 // var containerWidth = table.closest('.dataTables_wrapper').width(); 5027 // console.log('Table: ', totalWidth, 'Container: ', containerWidth); 5028 // if (totalWidth > containerWidth && containerWidth > 0) { 5029 // table.css('width', totalWidth + 'px'); 5030 // } else { 5031 // table.css('width', '100%'); 5032 // } 5033} 5034 5035/** 5036 * @param {Object} table 5037 * @return null 5038 */ 5039function updateChartDownloadButton(table, selectedSeries, selectedUnit) { 5040 if (typeof VIEW._chartDownloadButton !== 'undefined') { 5041 var tableCsv = toCsv(table, selectedSeries, selectedUnit); 5042 var blob = new Blob([tableCsv], { 5043 type: 'text/csv' 5044 }); 5045 var fileName = VIEW._chartDownloadButton.attr('download'); 5046 if (window.navigator && window.navigator.msSaveBlob) { 5047 // Special behavior for IE. 5048 VIEW._chartDownloadButton.off('click.openSdgDownload') 5049 VIEW._chartDownloadButton.on('click.openSdgDownload', function (event) { 5050 window.navigator.msSaveBlob(blob, fileName); 5051 }); 5052 } 5053 else { 5054 VIEW._chartDownloadButton 5055 .attr('href', URL.createObjectURL(blob)) 5056 .data('csvdata', tableCsv); 5057 } 5058 } 5059} 5060 5061 /** 5062 * @param {null|undefined|Float|String} value 5063 * @param {Object} info 5064 * @param {Object} context 5065 * @param {Object} additionalInfo 5066 * @return {null|undefined|Float|String} 5067 */ 5068function alterDataDisplay(value, info, context, additionalInfo) { 5069 // If value is empty, we will not alter it. 5070 if (value == null || value == undefined) { 5071 return value; 5072 } 5073 // Before passing to user-defined dataDisplayAlterations, let's 5074 // do our best to ensure that it starts out as a number. 5075 var altered = value; 5076 var obsText = ''; 5077 if (typeof altered !== 'number') { 5078 if (typeof altered == 'string' && context === 'table cell' && altered.indexOf(' ') > 0) { 5079 obsText = altered.substring(altered.indexOf(' ') + 1); 5080 altered = Number(altered.substring(0, altered.indexOf(' '))); 5081 } 5082 else { 5083 altered = Number(value); 5084 } 5085 } 5086 // If that gave us a non-number, return original. 5087 if (isNaN(altered)) { 5088 return value; 5089 } 5090 // Now go ahead with user-defined alterations. 5091 opensdg.dataDisplayAlterations.forEach(function (callback) { 5092 altered = callback(altered, info, context); 5093 }); 5094 // Now apply our custom precision control if needed. 5095 5096 // Special treatment for numbers on y axis: If stepSize is defined, they should display decimal places as follows: 5097 // StepSize >= 1 --> 0 decimal places, Stepsize >= 0.1 --> 1 decimal place, StepSize >= 0.01 --> 2 decimal places ... 5098 console.log("preIf", context, VIEW._graphStepsize, VIEW._graphStepsize.step != 0 && VIEW._graphStepsize.step != ''); 5099 if (context == 'chart y-axis tick' && VIEW._graphStepsize && VIEW.graphStepsize != 0 && VIEW.graphStepsize != '') { 5100 precision = Math.ceil(Math.log(1 / VIEW._graphStepsize.step) / Math.LN10); 5101 console.log("step", VIEW._graphStepsize.step); 5102 console.log("prec", precision); 5103 if (precision < 0) { 5104 precision = 0 5105 } 5106 } 5107 else { 5108 var precision = VIEW._precision 5109 }; 5110 // If the returned value is not a number, use the legacy logic for 5111 // precision and decimal separator. 5112 if (typeof altered !== 'number') { 5113 // Now apply our custom precision control if needed. 5114 5115 if (precision || precision === 0) { 5116 altered = Number.parseFloat(altered).toFixed(precision); 5117 } 5118 // Now apply our custom decimal separator if needed. 5119 if (OPTIONS.decimalSeparator) { 5120 altered = altered.toString().replace('.', OPTIONS.decimalSeparator); 5121 } 5122 // Apply thousands seperator if needed 5123 if (OPTIONS.thousandsSeparator && precision <=3){ 5124 altered = altered.toString().replace(/\B(?=(\d{3})+(?!\d))/g, OPTIONS.thousandsSeparator); 5125 } 5126 } 5127
5128 // Otherwise if we have a number, use toLocaleString instead. 5129 else { 5130 var localeOpts = {}; 5131 if (VIEW._precision || VIEW._precision === 0) { 5132 localeOpts.minimumFractionDigits = precision; 5133 localeOpts.maximumFractionDigits = precision; 5134 } 5135 altered = altered.toLocaleString(opensdg.language, localeOpts); 5136 // Apply thousands seperator if needed 5137 if (OPTIONS.thousandsSeparator && precision <=3 && opensdg.language == 'de'){ 5138 altered = altered.replaceAll('.', OPTIONS.thousandsSeparator); 5139 } 5140 } 5141 // Now let's add any footnotes from observation attributes. 5142 var obsAttributes = []; 5143 if (context === 'chart tooltip') { 5144 var dataIndex = additionalInfo.dataIndex; 5145 obsAttributes = info.observationAttributes[dataIndex]; 5146 } 5147 else if (context === 'table cell') { 5148 var row = additionalInfo.row, 5149 col = additionalInfo.col, 5150 obsAttributesTable = additionalInfo.observationAttributesTable; 5151 obsAttributes = obsAttributesTable.data[row][col]; 5152 //altered += ' ' + obsText; 5153 } 5154 if (obsAttributes.length > 0) { 5155 var obsAttributeFootnoteNumbers = obsAttributes.map(function(obsAttribute) { 5156 return getObservationAttributeFootnoteSymbol(obsAttribute); 5157 }); 5158 // if (context == 'table cell'){ 5159 // obsAttributeFootnoteNumbers.splice(obsAttributeFootnoteNumbers.indexOf('0'),1); 5160 // } 5161 var attributes = ' [' + obsAttributeFootnoteNumbers.join(', ') + ']'; 5162 } 5163 else { 5164 var attributes = ''; 5165 } 5166 5167 // for table: we do not want "0 [-]" but "-"; and not "0,00 [0]" but "0,00" 5168 if (context == 'table cell'){ 5169 if (parseFloat(altered) == 0){ 5170 // case: "0" 5171 if (attributes.indexOf('0') > -1) { 5172 var deci = ['0', '0.0', '0.00', '0.000'] 5173 for (var i = 0; i < deci.length; i++) { 5174 attributes = attributes.replace('[' + deci[i] + ']','').replace('' + deci[i] + ', ','').replace(', ' + deci[i] + '',''); 5175 } 5176 } 5177 else if (attributes.indexOf('â') > -1){ 5178 altered = 'â'; 5179 attributes = attributes.replace('[â]','').replace('â, ','').replace(', â',''); 5180 } 5181 } 5182 } 5183 altered += attributes; 5184 5185 return altered; 5186} 5187 5188/** 5189 * Convert a number into a string for observation atttribute footnotes. 5190 * 5191 * @param {int} num 5192 * @returns {string} Number converted into unicode character for footnotes. 5193 */ 5194function getObservationAttributeFootnoteSymbol(obsAttribute) { 5195 // make sure we do not get 0.000 for obsValue 5196 if (isNaN(parseInt(obsAttribute.value))) { 5197 return '' + obsAttribute.value + ''; 5198 } 5199 else{ 5200 return '' + String(parseInt(obsAttribute.value)) + ''; 5201 } 5202 5203 //return '[' + translations.indicator.note + ' ' + (num + 1) + ']'; 5204} 5205 5206/** 5207 * Figure out what the browser will be using for the decimal separator. 5208 * 5209 * @returns {string} The decimal separator the browser will use. 5210 */ 5211function getBrowserDecimalSeparator() { 5212 var browserDecimal = 1.1; 5213 browserDecimal = browserDecimal.toLocaleString(opensdg.language).substring(1, 2); 5214 return browserDecimal; 5215} 5216 5217 /** 5218 * @param {String} selectedSeries 5219 * @param {String} selectedUnit 5220 * @return null 5221 */ 5222function updateSeriesAndUnitElements(selectedSeries, selectedUnit) { 5223 var hasSeries = typeof selectedSeries !== 'undefined', 5224 hasUnit = typeof selectedUnit !== 'undefined', 5225 hasBoth = hasSeries && hasUnit; 5226 if (hasSeries || hasUnit || hasBoth) { 5227 $('[data-for-series], [data-for-unit]').each(function () { 5228 var elementSeries = $(this).data('for-series'), 5229 elementUnit = $(this).data('for-unit'), 5230 seriesMatches = elementSeries === selectedSeries, 5231 unitMatches = elementUnit === selectedUnit; 5232 if ((hasSeries || hasBoth) && !seriesMatches && elementSeries !== '') { 5233 $(this).hide(); 5234 } 5235 else if ((hasUnit || hasBoth) && !unitMatches && elementUnit !== '') { 5236 $(this).hide(); 5237 } 5238 else { 5239 $(this).show(); 5240 } 5241 }); 5242 } 5243} 5244 5245/** 5246 * @param {String} contrast 5247 * @return bool 5248 */ 5249function isHighContrast(contrast) { 5250 if (contrast) { 5251 return contrast === 'high'; 5252 } 5253 else { 5254 return $('body').hasClass('contrast-high'); 5255 } 5256} 5257 5258/** 5259 * @param {Object} table 5260 * @param {String} name 5261 * @param {String} indicatorId 5262 * @param {Element} el 5263 * @return null 5264 */ 5265function createDownloadButton(table, name, indicatorId, el, selectedSeries, selectedUnit) { 5266 if (window.Modernizr.blobconstructor) { 5267 var downloadKey = 'download_csv'; 5268 if (name == 'Chart') { 5269 downloadKey = 'download_chart'; 5270 } 5271 if (name == 'Table') { 5272 downloadKey = 'download_table'; 5273 } 5274 var gaLabel = 'Download ' + name + ' CSV: ' + indicatorId.replace('indicator_', ''); 5275 var tableCsv = toCsv(table, selectedSeries, selectedUnit); 5276 var fileName = indicatorId + '.csv'; 5277 var downloadButton = $('<a />').text(translations.indicator[downloadKey]) 5278 .attr(opensdg.autotrack('download_data_current', 'Downloads', 'Download CSV', gaLabel)) 5279 .attr({ 5280 'download': fileName, 5281 'title': translations.indicator.download_csv_title, 5282 'aria-label': translations.indicator.download_csv_title, 5283 'class': 'btn btn-primary btn-download', 5284 'tabindex': 0, 5285 'role': 'button', 5286 }); 5287 var blob = new Blob([tableCsv], { 5288 type: 'text/csv' 5289 }); 5290 if (window.navigator && window.navigator.msSaveBlob) { 5291 // Special behavior for IE. 5292 downloadButton.on('click.openSdgDownload', function (event) { 5293 window.navigator.msSaveBlob(blob, fileName); 5294 }); 5295 } 5296 else { 5297 downloadButton 5298 .attr('href', URL.createObjectURL(blob)) 5299 .data('csvdata', tableCsv); 5300 } 5301 if (name == 'Chart') { 5302 VIEW._chartDownloadButton = downloadButton; 5303 } 5304 $(el).append(downloadButton); 5305 } else { 5306 var headlineId = indicatorId.replace('indicator', 'headline'); 5307 var id = indicatorId.replace('indicator_', ''); 5308 var gaLabel = 'Download Headline CSV: ' + id; 5309 $(el).append($('<a />').text(translations.indicator.download_headline) 5310 .attr(opensdg.autotrack('download_data_headline', 'Downloads', 'Download CSV', gaLabel)) 5311 .attr({ 5312 'href': opensdg.remoteDataBaseUrl + '/headline/' + id + '.csv', 5313 'download': headlineId + '.csv', 5314 'title': translations.indicator.download_headline_title, 5315 'aria-label': translations.indicator.download_headline_title, 5316 'class': 'btn btn-primary btn-download', 5317 'tabindex': 0, 5318 'role': 'button', 5319 })); 5320 } 5321} 5322 5323/** 5324 * @param {String} indicatorId 5325 * @param {Element} el 5326 * @return null 5327 */ 5328function createSourceButton(indicatorId, el) { 5329 var gaLabel = 'Download Source CSV: ' + indicatorId; 5330 $(el).append($('<a />').text(translations.indicator.download_source) 5331 .attr(opensdg.autotrack('download_data_source', 'Downloads', 'Download CSV', gaLabel)) 5332 .attr({ 5333 'href': opensdg.remoteDataBaseUrl + '/data/' + indicatorId + '.csv', 5334 'download': indicatorId + '.csv', 5335 'title': translations.indicator.download_source_title, 5336 'aria-label': translations.indicator.download_source_title, 5337 'class': 'btn btn-primary btn-download', 5338 'tabindex': 0, 5339 'role': 'button', 5340 })); 5341} 5342 5343/** 5344 * @param {Object} indicatorDownloads 5345 * @param {String} indicatorId 5346 * @param {Element} el 5347 * @return null 5348 */ 5349function createIndicatorDownloadButtons(indicatorDownloads, indicatorId, el) { 5350 if (indicatorDownloads) { 5351 var buttonLabels = Object.keys(indicatorDownloads); 5352 for (var i = 0; i < buttonLabels.length; i++) { 5353 var buttonLabel = buttonLabels[i]; 5354 var href = indicatorDownloads[buttonLabel].href; 5355 var buttonLabelTranslated = translations.t(buttonLabel); 5356 var gaLabel = buttonLabel + ': ' + indicatorId; 5357 $(el).append($('<a />').text(buttonLabelTranslated) 5358 .attr(opensdg.autotrack(buttonLabel, 'Downloads', buttonLabel, gaLabel)) 5359 .attr({ 5360 'href': opensdg.remoteDataBaseUrl + '/' + href, 5361 'download': href.split('/').pop(), 5362 'title': buttonLabelTranslated, 5363 'class': 'btn btn-primary btn-download', 5364 'tabindex': 0, 5365 'role': 'button', 5366 })); 5367 } 5368 } 5369} 5370 5371 5372 return { 5373 HIDE_SINGLE_SERIES: HIDE_SINGLE_SERIES, 5374 HIDE_SINGLE_UNIT: HIDE_SINGLE_UNIT, 5375 PROXY_PILL: PROXY_PILL, 5376 initialiseFields: initialiseFields, 5377 initialiseUnits: initialiseUnits, 5378 initialiseSerieses: initialiseSerieses, 5379 updateIndicatorDataUnitStatus: updateIndicatorDataUnitStatus, 5380 updateIndicatorDataSeriesStatus: updateIndicatorDataSeriesStatus, 5381 alterChartConfig: alterChartConfig, 5382 alterTableConfig: alterTableConfig, 5383 alterDataDisplay: alterDataDisplay, 5384 updateChartTitle: updateChartTitle, 5385 updateChartSubtitle: updateChartSubtitle, 5386 updateWithSelectedFields: updateWithSelectedFields, 5387 updateSeriesAndUnitElements: updateSeriesAndUnitElements, 5388 updateUnitElements: updateUnitElements, 5389 updateTimeSeriesAttributes: updateTimeSeriesAttributes, 5390 updateObservationAttributes: updateObservationAttributes, 5391 updatePlot: updatePlot, 5392 isHighContrast: isHighContrast, 5393 getHeadlineColor: getHeadlineColor, 5394 getGridColor: getGridColor, 5395 getTickColor: getTickColor, 5396 updateHeadlineColor: updateHeadlineColor, 5397 createPlot: createPlot, 5398 setPlotEvents: setPlotEvents, 5399 toCsv: toCsv, 5400 createIndicatorDownloadButtons: createIndicatorDownloadButtons, 5401 createSourceButton: createSourceButton, 5402 createDownloadButton: createDownloadButton, 5403 createSelectionsTable: createSelectionsTable, 5404 sortFieldGroup: sortFieldGroup, 5405 getObservationAttributeFootnoteSymbol: getObservationAttributeFootnoteSymbol, 5406 getObservationAttributeText: getObservationAttributeText, 5407 getBrowserDecimalSeparator: getBrowserDecimalSeparator, 5408 } 5409})(); 5410 5411 VIEW.helpers = helpers; 5412 5413 VIEW._chartInstance = undefined; 5414 VIEW._tableColumnDefs = OPTIONS.tableColumnDefs; 5415 VIEW._mapView = undefined; 5416 VIEW._legendElement = OPTIONS.legendElement; 5417 VIEW._precision = undefined; 5418 VIEW._chartInstances = {}; 5419 VIEW._browserDecimalSeparator = helpers.getBrowserDecimalSeparator(); 5420 VIEW._graphStepsize = undefined; 5421
5422 var chartHeight = screen.height < OPTIONS.maxChartHeight ? screen.height : OPTIONS.maxChartHeight; 5423 $('.plot-container', OPTIONS.rootElement).css('height', chartHeight + 'px'); 5424 5425 $(document).ready(function () { 5426 5427 $(OPTIONS.rootElement).find('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { 5428 if ($(e.target).attr('href') == '#tableview') { 5429 setDataTableWidth($(OPTIONS.rootElement).find('#selectionsTable table')); 5430 } else { 5431 $($.fn.dataTable.tables(true)).css('width', '100%'); 5432 $($.fn.dataTable.tables(true)).DataTable().columns.adjust().draw(); 5433 } 5434 }); 5435 5436 // Execute the hide/show functionality for the sidebar, both on 5437 // the currently active tab, and each time a tab is clicked on. 5438 $('.data-view .nav-item.active .nav-link').each(toggleSidebar); 5439 $('.data-view .nav-link').on('click', toggleSidebar); 5440 function toggleSidebar() { 5441 var $sidebar = $('.indicator-sidebar'), 5442 $main = $('.indicator-main'), 5443 hideSidebar = $(this).data('no-disagg'), 5444 mobile = window.matchMedia("screen and (max-width: 990px)"); 5445 if (hideSidebar) { 5446 $sidebar.addClass('indicator-sidebar-hidden'); 5447 $main.addClass('indicator-main-full'); 5448 // On mobile, this can be confusing, so we need to scroll to the tabs. 5449 if (mobile.matches) { 5450 $([document.documentElement, document.body]).animate({ 5451 scrollTop: $("#indicator-main").offset().top - 40 5452 }, 400); 5453 } 5454 } 5455 else { 5456 $sidebar.removeClass('indicator-sidebar-hidden'); 5457 $main.removeClass('indicator-main-full'); 5458 // Make sure the unit/series items are updated, in case 5459 // they were changed while on the map. 5460 helpers.updateChartSubtitle(VIEW._dataCompleteArgs.chartSubtitle); 5461 helpers.updateChartTitle(VIEW._dataCompleteArgs.chartTitle, VIEW._dataCompleteArgs.isProxy); 5462 helpers.updateSeriesAndUnitElements(VIEW._dataCompleteArgs.selectedSeries, VIEW._dataCompleteArgs.selectedUnit); 5463 helpers.updateUnitElements(VIEW._dataCompleteArgs.selectedUnit); 5464 helpers.updateTimeSeriesAttributes(VIEW._dataCompleteArgs.timeSeriesAttributes); 5465 } 5466 }; 5467 }); 5468 5469 MODEL.onDataComplete.attach(function (sender, args) { 5470 5471 VIEW._precision = args.precision; 5472 VIEW._graphStepsize = args.graphStepsize; 5473 5474 if (MODEL.showData) { 5475 $('#dataset-size-warning')[args.datasetCountExceedsMax ? 'show' : 'hide'](); 5476 if (!VIEW._chartInstance) { 5477 helpers.createPlot(args, helpers); 5478 helpers.setPlotEvents(args); 5479 } else { 5480 helpers.updatePlot(args); 5481 } 5482 } 5483 5484 helpers.createSelectionsTable(args); 5485 helpers.updateChartSubtitle(args.chartSubtitle); 5486 helpers.updateChartTitle(args.chartTitle, args.isProxy); 5487 helpers.updateSeriesAndUnitElements(args.selectedSeries, args.selectedUnit); 5488 helpers.updateUnitElements(args.selectedUnit); 5489 helpers.updateTimeSeriesAttributes(args.timeSeriesAttributes); 5490 helpers.updateObservationAttributes(args.allObservationAttributes); 5491 5492 VIEW._dataCompleteArgs = args; 5493 }); 5494 5495 MODEL.onFieldsComplete.attach(function (sender, args) { 5496 5497 helpers.initialiseFields(args); 5498 5499 if (args.hasGeoData && args.showMap) { 5500 VIEW._mapView = new mapView(); 5501 VIEW._mapView.initialise( 5502 args.indicatorId, 5503 args.precision, 5504 args.precisionItems, 5505 OPTIONS.decimalSeparator, 5506 OPTIONS.thousandsSeparator, 5507 args.dataSchema, 5508 VIEW.helpers, 5509 MODEL.helpers, 5510 args.chartTitles, 5511 args.chartSubtitles, 5512 args.startValues, 5513 args.proxy, 5514 args.proxySerieses, 5515 MODEL.allObservationAttributes, 5516 ); 5517 } 5518 }); 5519 5520 MODEL.onUnitsComplete.attach(function (sender, args) { 5521 5522 helpers.initialiseUnits(args); 5523 }); 5524 5525 if (MODEL.onSeriesesComplete) { 5526 5527 MODEL.onSeriesesComplete.attach(function (sender, args) { 5528 helpers.initialiseSerieses(args); 5529 }); 5530 } 5531 5532 if (MODEL.onUnitsSelectedChanged) { 5533 MODEL.onUnitsSelectedChanged.attach(function (sender, args) { 5534 helpers.updateIndicatorDataUnitStatus(args); 5535 }); 5536 } 5537 if (MODEL.onSeriesesSelectedChanged) { 5538 MODEL.onSeriesesSelectedChanged.attach(function (sender, args) { 5539 helpers.updateIndicatorDataSeriesStatus(args); 5540 }); 5541 } 5542 5543 MODEL.onFieldsCleared.attach(function (sender, args) { 5544 5545 $(OPTIONS.rootElement).find(':checkbox').prop('checked', false); 5546 $(OPTIONS.rootElement).find('#clear') 5547 .addClass('disabled') 5548 .attr('aria-disabled', 'true') 5549 .attr('disabled', 'disabled'); 5550 5551 // reset available/unavailable fields 5552 helpers.updateWithSelectedFields(); 5553 5554 $(OPTIONS.rootElement).find('.selected').css('width', '0'); 5555 }); 5556 5557 MODEL.onSelectionUpdate.attach(function (sender, args) { 5558 5559 if (args.selectedFields.length) { 5560 $(OPTIONS.rootElement).find('#clear') 5561 .removeClass('disabled') 5562 .attr('aria-disabled', 'false') 5563 .removeAttr('disabled'); 5564 } 5565 else { 5566 $(OPTIONS.rootElement).find('#clear') 5567 .addClass('disabled') 5568 .attr('aria-disabled', 'true') 5569 .attr('disabled', 'disabled'); 5570 } 5571 5572 // loop through the available fields: 5573 $('.variable-selector').each(function (index, element) { 5574 var currentField = $(element).data('field'); 5575 var element = $(OPTIONS.rootElement).find('.variable-selector[data-field="' + currentField + '"]'); 5576 5577 // is this an allowed field: 5578 if (args.allowedFields.includes(currentField)) { 5579 $(element).removeClass('disallowed'); 5580 $(element).find('> button').removeAttr('aria-describedby'); 5581 } 5582 else { 5583 $(element).addClass('disallowed'); 5584 $(element).find('> button').attr('aria-describedby', 'variable-hint-' + currentField); 5585 } 5586 }); 5587 }); 5588 5589 MODEL.onFieldsStatusUpdated.attach(function (sender, args) { 5590 5591 _.each(args.data, function (fieldGroup) { 5592 _.each(fieldGroup.values, function (fieldItem) { 5593 var element = $(OPTIONS.rootElement).find(':checkbox[value="' + fieldItem.value + '"][data-field="' + fieldGroup.field + '"]'); 5594 element.parent().addClass(fieldItem.state).attr('data-has-data', fieldItem.hasData); 5595 }); 5596 // Indicate whether the fieldGroup had any data. 5597 var fieldGroupElement = $(OPTIONS.rootElement).find('.variable-selector[data-field="' + fieldGroup.field + '"]'); 5598 fieldGroupElement.attr('data-has-data', fieldGroup.hasData); 5599 var fieldGroupButton = fieldGroupElement.find('> button'), 5600 describedByCurrent = fieldGroupButton.attr('aria-describedby') || '', 5601 noDataHintId = 'no-data-hint-' + fieldGroup.field.replace(/ /g, '.'); 5602 if (!fieldGroup.hasData && !describedByCurrent.includes(noDataHintId)) { 5603 fieldGroupButton.attr('aria-describedby', describedByCurrent + ' ' + noDataHintId); 5604 } 5605 else { 5606 fieldGroupButton.attr('aria-describedby', describedByCurrent.replace(noDataHintId, '')); 5607 } 5608 5609 // Re-sort the items. 5610 helpers.sortFieldGroup(fieldGroupElement); 5611 }); 5612 }); 5613 5614 $(OPTIONS.rootElement).on('click', '#clear', function () { 5615 MODEL.clearSelectedFields(); 5616 }); 5617 5618 $(OPTIONS.rootElement).on('click', '#fields label', function (e) { 5619 5620 if (!$(this).closest('.variable-selector').hasClass('disallowed')) { 5621 $(this).find(':checkbox').trigger('click'); 5622 } 5623 5624 e.preventDefault(); 5625 e.stopPropagation(); 5626 }); 5627 5628 $(OPTIONS.rootElement).on('change', '#units input', function () { 5629 MODEL.updateSelectedUnit($(this).val()); 5630 }); 5631 5632 $(OPTIONS.rootElement).on('change', '#serieses input', function () { 5633 MODEL.updateSelectedSeries($(this).val()); 5634 }); 5635 5636 $(OPTIONS.rootElement).on('click', '.variable-options button', function (e) { 5637 var type = $(this).data('type'); 5638 var $options = $(this).closest('.variable-options').find(':checkbox'); 5639 5640 // The clear button can clear all checkboxes. 5641 if (type == 'clear') { 5642 $options.prop('checked', false); 5643 } 5644 // The select button must only select checkboxes that have data. 5645 if (type == 'select') { 5646 $options.parent().not('[data-has-data=false]').find(':checkbox').prop('checked', true) 5647 } 5648 5649 helpers.updateWithSelectedFields(); 5650 e.stopPropagation(); 5651 }); 5652 5653 $(OPTIONS.rootElement).on('click', ':checkbox', function (e) { 5654 5655 // don't permit disallowed selections: 5656 if ($(this).closest('.variable-selector').hasClass('disallowed')) { 5657 return; 5658 } 5659 5660 helpers.updateWithSelectedFields(); 5661 e.stopPropagation(); 5662 }); 5663 5664 $(OPTIONS.rootElement).on('click', '.variable-selector', function (e) { 5665 5666 var $button = $(e.target).closest('button'); 5667 var $options = $(this).find('.variable-options'); 5668 5669 if ($options.is(':visible')) { 5670 $options.hide(); 5671 $button.attr('aria-expanded', 'false'); 5672 } 5673 else { 5674 $options.show(); 5675 $button.attr('aria-expanded', 'true'); 5676 } 5677 5678 e.stopPropagation(); 5679 }); 5680}; 5681var indicatorController = function (model, view) { 5682 this._model = model; 5683 this._view = view; 5684}; 5685 5686indicatorController.prototype = { 5687 initialise: function () { 5688 this._model.initialise(); 5689 } 5690}; 5691var indicatorInit = function () { 5692 if ($('#indicatorData').length) { 5693 var domData = $('#indicatorData').data(); 5694 5695 if (domData.showdata) { 5696 5697 $('.async-loading').each(function (i, obj) { 5698 $(obj).append($('<img />').attr('src', $(obj).data('img')).attr('alt', translations.indicator.loading)); 5699 }); 5700 5701 var remoteUrl = '/comb/' + domData.id + '.json'; 5702 if (opensdg.remoteDataBaseUrl !== '/') { 5703 remoteUrl = opensdg.remoteDataBaseUrl + remoteUrl; 5704 } 5705 5706 $.ajax({ 5707 url: remoteUrl, 5708 success: function (res) { 5709 5710 $('.async-loading').remove(); 5711 $('.async-loaded').show(); 5712 5713 var model = new indicatorModel({ 5714 data: res.data, 5715 edgesData: res.edges, 5716 showMap: domData.showmap, 5717 country: domData.country, 5718 indicatorId: domData.indicatorid, 5719 shortIndicatorId: domData.id, 5720 chartTitle: domData.charttitle, 5721 chartTitles: domData.charttitles, 5722 chartSubtitle: domData.chartsubtitle, 5723 chartSubtitles: domData.chartsubtitles, 5724 measurementUnit: domData.measurementunit, 5725 xAxisLabel: domData.xaxislabel, 5726 showData: domData.showdata, 5727 showInfo: domData.showinfo, 5728 graphType: domData.graphtype, 5729 graphTypes: domData.graphtypes, 5730 startValues: domData.startvalues, 5731 graphLimits: domData.graphlimits, 5732 stackedDisaggregation: domData.stackeddisaggregation, 5733 fill: domData.fill, 5734 fillAbove: domData.fillabove, 5735 fillBelow: domData.fillbelow, 5736 showLine: domData.showline, 5737 mixedTypes: domData.mixedtypes, 5738 spanGaps: domData.spangaps, 5739 graphAnnotations: domData.graphannotations, 5740 graphTargetLines: domData.graphtargetlines, 5741 graphSeriesBreaks: domData.graphseriesbreaks, 5742 graphErrorBars: domData.grapherrorbars, 5743 graphTargetPoints: domData.graphtargetpoints, 5744 graphTargetLabels: domData.graphtargetlabels, 5745 indicatorDownloads: domData.indicatordownloads, 5746 dataSchema: domData.dataschema, 5747 compositeBreakdownLabel: domData.compositebreakdownlabel, 5748 precision: domData.precision,
5749 graphStepsize: domData.graphstepsize, 5750 proxy: domData.proxy, 5751 proxySeries: domData.proxyseries, 5752 }); 5753 var view = new indicatorView(model, { 5754 rootElement: '#indicatorData', 5755 legendElement: '#plotLegend', 5756 decimalSeparator: ',', 5757 thousandsSeparator: ' ', 5758 maxChartHeight: 420, 5759 tableColumnDefs: [ 5760 { maxCharCount: 25 }, // nowrap 5761 //{ maxCharCount: 35, width: 200 }, 5762 { maxCharCount: Infinity, width: 300 } 5763 ] 5764 }); 5765 var controller = new indicatorController(model, view); 5766 controller.initialise(); 5767 } 5768 }); 5769 } 5770 } 5771}; 5772$(document).ready(function() { 5773 $('.nav-tabs').each(function() { 5774 var tabsList = $(this); 5775 5776 // Allow clicking on the <li> to trigger tab click. 5777 tabsList.find('li').click(function(event) { 5778 if (event.target.tagName === 'LI') { 5779 $(event.target).find('> button').click(); 5780 } 5781 }); 5782 }); 5783}); 5784$(document).ready(function() { 5785 $('.nav-tabs').each(function() { 5786 var tabsList = $(this); 5787 var tabs = tabsList.find('li > button'); 5788 var panes = tabsList.parent().find('.tab-pane'); 5789 5790 panes.attr({ 5791 'role': 'tabpanel', 5792 'aria-hidden': 'true', 5793 'tabindex': '0', 5794 }).hide(); 5795 5796 tabsList.attr({ 5797 'role': 'tablist', 5798 }); 5799 5800 tabs.each(function(idx) { 5801 var tab = $(this); 5802 var tabId = 'tab-' + tab.attr('data-bs-target').slice(1); 5803 var pane = tabsList.parent().find(tab.attr('data-bs-target')); 5804 5805 tab.attr({ 5806 'id': tabId, 5807 'role': 'tab', 5808 'aria-selected': 'false', 5809 'tabindex': '-1', 5810 }).parent().attr('role', 'presentation'); 5811 5812 pane.attr('aria-labelledby', tabId); 5813 5814 tab.click(function(e) { 5815 e.preventDefault(); 5816 5817 tabsList.find('> li.active') 5818 .removeClass('active') 5819 .find('> button') 5820 .attr({ 5821 'aria-selected': 'false', 5822 'tabindex': '-1', 5823 }) 5824 .removeClass('active'); 5825 5826 panes.filter(':visible').attr({ 5827 'aria-hidden': 'true', 5828 }).hide(); 5829 5830 pane.attr({ 5831 'aria-hidden': 'false', 5832 }).show(); 5833 5834 tab.attr({ 5835 'aria-selected': 'true', 5836 'tabindex': '0', 5837 }).parent().addClass('active'); 5838 tab.focus(); 5839 }); 5840 }); 5841 5842 // Show the first tabPanel 5843 panes.first().attr('aria-hidden', 'false').show(); 5844 5845 // Set state for the first tabsList li 5846 tabsList.find('li:first').addClass('active').find(' > button').attr({ 5847 'aria-selected': 'true', 5848 'tabindex': '0', 5849 }); 5850 5851 // Set keydown events on tabList item for navigating tabs
5852 tabsList.delegate('button', 'keydown', function(e) { 5853 var tab = $(this); 5854 switch (e.which) { 5855 case 37: 5856 if (tab.parent().prev().length != 0) { 5857 tab.parent().prev().find('> button').click(); 5858 e.preventDefault(); 5859 } 5860 else { 5861 tabsList.find('li:last > button').click(); 5862 e.preventDefault(); 5863 } 5864 break; 5865 case 39: 5866 if (tab.parent().next().length != 0) { 5867 tab.parent().next().find('> button').click(); 5868 e.preventDefault(); 5869 } 5870 else { 5871 tabsList.find('li:first > button').click(); 5872 e.preventDefault(); 5873 } 5874 break; 5875 } 5876 }); 5877 }); 5878}); 5879var indicatorSearch = function() { 5880 5881 function sanitizeInput(input) { 5882 if (input === null) { 5883 return null; 5884 } 5885 var doc = new DOMParser().parseFromString(input, 'text/html'); 5886 var stripped = doc.body.textContent || ""; 5887 var map = { 5888 '&': '&', 5889 '<': '<', 5890 '>': '>', 5891 '"': '"', 5892 "'": ''', 5893 "/": '/', 5894 "`": '`', 5895 }; 5896 var reg = /[&<>"'/`]/ig; 5897 return stripped.replace(reg, function(match) { 5898 return map[match]; 5899 }); 5900 } 5901 5902 var urlParams = new URLSearchParams(window.location.search); 5903 var searchTerms = sanitizeInput(urlParams.get('q')); 5904 if (searchTerms !== null) { 5905 document.getElementById('search-bar-on-page').value = searchTerms; 5906 document.getElementById('search-term').innerHTML = searchTerms; 5907 5908 var searchTermsToUse = searchTerms; 5909 // This is to allow for searching by indicator with dashes. 5910 if (searchTerms.split('-').length == 3 && searchTerms.length < 15) { 5911 // Just a best-guess check to see if the user intended to search for an 5912 // indicator ID. 5913 searchTermsToUse = searchTerms.replace(/-/g, '.'); 5914 } 5915 5916 var useLunr = typeof window.lunr !== 'undefined'; 5917 if (useLunr && opensdg.language != 'en') { 5918 if (typeof lunr[opensdg.language] === 'undefined') { 5919 useLunr = false; 5920 } 5921 } 5922 5923 // Recognize an indicator id as a special case that does not need Lunr. 5924 var searchWords = searchTermsToUse.split(' '), 5925 indicatorIdParts = searchWords[0].split('.'), 5926 isIndicatorSearch = (searchWords.length === 1 && indicatorIdParts.length >= 3); 5927 if (isIndicatorSearch) { 5928 useLunr = false; 5929 } 5930 5931 var results = []; 5932 var alternativeSearchTerms = []; 5933 var noTermsProvided = (searchTerms === ''); 5934 5935 if (useLunr && !noTermsProvided) { 5936 // Engish-specific tweak for words separated only by commas. 5937 if (opensdg.language == 'en') { 5938 lunr.tokenizer.separator = /[\s\-,]+/ 5939 } 5940 5941 var searchIndex = lunr(function () { 5942 if (opensdg.language != 'en' && lunr[opensdg.language]) { 5943 this.use(lunr[opensdg.language]); 5944 } 5945 this.use(storeUnstemmed); 5946 this.ref('url'); 5947 // Index the expected fields. 5948 this.field('title', getSearchFieldOptions('title')); 5949 this.field('content', getSearchFieldOptions('content')); 5950 this.field('id', getSearchFieldOptions('id')); 5951 // Index any extra fields. 5952 var i; 5953 for (i = 0; i < opensdg.searchIndexExtraFields.length; i++) { 5954 var extraField = opensdg.searchIndexExtraFields[i]; 5955 this.field(extraField, getSearchFieldOptions(extraField)); 5956 } 5957 // Index all the documents. 5958 for (var ref in opensdg.searchItems) { 5959 this.add(opensdg.searchItems[ref]); 5960 }; 5961 }); 5962 5963 // Perform the search. 5964 var results = searchIndex.search(searchTermsToUse); 5965 5966 // If we didn't find anything, get progressively "fuzzier" to look for 5967 // alternative search term options. 5968 if (!results.length > 0) { 5969 for (var fuzziness = 1; fuzziness < 5; fuzziness++) { 5970 var fuzzierQuery = getFuzzierQuery(searchTermsToUse, fuzziness);
5971 var alternativeResults = searchIndex.search(fuzzierQuery); 5972 if (alternativeResults.length > 0) { 5973 var matchedTerms = getMatchedTerms(alternativeResults); 5974 if (matchedTerms) { 5975 alternativeSearchTerms = matchedTerms; 5976 } 5977 break; 5978 } 5979 } 5980 } 5981 } 5982 else if (!noTermsProvided) { 5983 // Non-Lunr basic search functionality. 5984 results = _.filter(opensdg.searchItems, function(item) { 5985 var i, match = false; 5986 if (item.title) { 5987 match = match || item.title.indexOf(searchTermsToUse) !== -1; 5988 } 5989 if (item.content) { 5990 match = match || item.content.indexOf(searchTermsToUse) !== -1; 5991 } 5992 for (i = 0; i < opensdg.searchIndexExtraFields.length; i++) { 5993 var extraField = opensdg.searchIndexExtraFields[i]; 5994 if (typeof item[extraField] !== 'undefined') { 5995 match = match || item[extraField].indexOf(searchTermsToUse) !== -1; 5996 } 5997 } 5998 return match; 5999 }); 6000 // Mimic what Lunr does. 6001 results = _.map(results, function(item) { 6002 return { ref: item.url } 6003 }); 6004 } 6005 6006 var resultItems = []; 6007 6008 results.forEach(function(result) { 6009 var doc = opensdg.searchItems[result.ref] 6010 // Truncate the contents. 6011 if (doc.content.length > 400) { 6012 doc.content = doc.content.substring(0, 400) + '...'; 6013 } 6014 // Indicate the matches. 6015 doc.content = doc.content.replace(new RegExp('(' + escapeRegExp(searchTerms) + ')', 'gi'), '<span class="match">$1</span>'); 6016 doc.title = doc.title.replace(new RegExp('(' + escapeRegExp(searchTerms) + ')', 'gi'), '<span class="match">$1</span>'); 6017 resultItems.push(doc); 6018 }); 6019 6020 $('.loader').hide(); 6021 6022 // Print the results using a template. 6023 var template = _.template( 6024 $("script.results-template").html() 6025 ); 6026 $('div.results').html(template({ 6027 searchResults: resultItems, 6028 resultsCount: resultItems.length, 6029 didYouMean: (alternativeSearchTerms.length > 0) ? alternativeSearchTerms : false, 6030 })); 6031 6032 // Hide the normal header search. 6033 $('.header-search-bar').hide(); 6034 } 6035 6036 // Helper function to make a search query "fuzzier", using the ~ syntax. 6037 // See https://lunrjs.com/guides/searching.html#fuzzy-matches. 6038 function getFuzzierQuery(query, amountOfFuzziness) { 6039 return query 6040 .split(' ') 6041 .map(function(x) { return x + '~' + amountOfFuzziness; }) 6042 .join(' '); 6043 } 6044 6045 // Helper function to get the matched words from a result set. 6046 function getMatchedTerms(results) { 6047 var matchedTerms = {}; 6048 results.forEach(function(result) { 6049 Object.keys(result.matchData.metadata).forEach(function(stemmedTerm) { 6050 Object.keys(result.matchData.metadata[stemmedTerm]).forEach(function(fieldName) { 6051 result.matchData.metadata[stemmedTerm][fieldName].unstemmed.forEach(function(unstemmedTerm) { 6052 matchedTerms[unstemmedTerm] = true; 6053 }); 6054 }); 6055 }); 6056 }); 6057 return Object.keys(matchedTerms); 6058 } 6059 6060 // Helper function to get a boost score, if any. 6061 function getSearchFieldOptions(field) { 6062 var opts = {} 6063 var fieldBoost = opensdg.searchIndexBoost.find(function(boost) { 6064 return boost.field === field; 6065 }); 6066 if (fieldBoost) { 6067 opts['boost'] = parseInt(fieldBoost.boost) 6068 } 6069 return opts 6070 } 6071 6072 // Used to highlight search term matches on the screen. 6073 function escapeRegExp(str) { 6074 return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/gi, "\\$&"); 6075 }; 6076 6077 // Define a pipeline function that keeps the unstemmed word. 6078 // See: https://github.com/olivernn/lunr.js/issues/287#issuecomment-454923675 6079 function storeUnstemmed(builder) { 6080 function pipelineFunction(token) {
6081 token.metadata['unstemmed'] = token.toString(); 6082 return token; 6083 }; 6084 lunr.Pipeline.registerFunction(pipelineFunction, 'storeUnstemmed'); 6085 var firstPipelineFunction = builder.pipeline._stack[0]; 6086 builder.pipeline.before(firstPipelineFunction, pipelineFunction); 6087 builder.metadataWhitelist.push('unstemmed'); 6088 } 6089}; 6090 6091$(function() { 6092 6093 var $el = $('#indicator_search'); 6094 $('#jump-to-search').show(); 6095 $('#jump-to-search a').click(function() { 6096 if($el.is(':hidden')) { 6097 $('.navbar span[data-target="search"]').click(); 6098 } 6099 $el.focus(); 6100 }); 6101 6102 indicatorSearch(); 6103}); 6104 6105/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js */ 6106"document"in self&&("classList"in document.createElement("_")&&(!document.createElementNS||"classList"in document.createElementNS("http://www.w3.org/2000/svg","g"))||!function(t){"use strict";if("Element"in t){var e="classList",n="prototype",i=t.Element[n],s=Object,r=String[n].trim||function(){return this.replace(/^\s+|\s+$/g,"")},o=Array[n].indexOf||function(t){for(var e=0,n=this.length;n>e;e++)if(e in this&&this[e]===t)return e;return-1},a=function(t,e){this.name=t,this.code=DOMException[t],this.message=e},c=function(t,e){if(""===e)throw new a("SYNTAX_ERR","An invalid or illegal string was specified");if(/\s/.test(e))throw new a("INVALID_CHARACTER_ERR","String contains an invalid character");return o.call(t,e)},l=function(t){for(var e=r.call(t.getAttribute("class")||""),n=e?e.split(/\s+/):[],i=0,s=n.length;s>i;i++)this.push(n[i]);this._updateClassName=function(){t.setAttribute("class",""+this)}},u=l[n]=[],h=function(){return new l(this)};if(a[n]=Error[n],u.item=function(t){return this[t]||null},u.contains=function(t){return t+="",-1!==c(this,t)},u.add=function(){var t,e=arguments,n=0,i=e.length,s=!1;do t=e[n]+"",-1===c(this,t)&&(this.push(t),s=!0);while(++n<i);s&&this._updateClassName()},u.remove=function(){var t,e,n=arguments,i=0,s=n.length,r=!1;do for(t=n[i]+"",e=c(this,t);-1!==e;)this.splice(e,1),r=!0,e=c(this,t);while(++i<s);r&&this._updateClassName()},u.toggle=function(t,e){t+="";var n=this.contains(t),i=n?e!==!0&&"remove":e!==!1&&"add";return i&&this[i](t),e===!0||e===!1?e:!n},u.toString=function(){return this.join(" ")},s.defineProperty){var f={get:h,enumerable:!0,configurable:!0};try{s.defineProperty(i,e,f)}catch(g){(void 0===g.number||-2146823252===g.number)&&(f.enumerable=!1,s.defineProperty(i,e,f))}}else s[n].__defineGetter__&&i.__defineGetter__(e,h)}}(self),function(){"use strict";var t=document.createElement("_");if(t.classList.add("c1","c2"),!t.classList.contains("c2")){var e=function(t){var e=DOMTokenList.prototype[t];DOMTokenList.prototype[t]=function(t){var n,i=arguments.length;for(n=0;i>n;n++)t=arguments[n],e.call(this,t)}};e("add"),e("remove")}if(t.classList.toggle("c3",!1),t.classList.contains("c3")){var n=DOMTokenList.prototype.toggle;DOMTokenList.prototype.toggle=function(t,e){return 1 in arguments&&!this.contains(t)==!e?e:n.call(this,t)}}t=null}());/*! modernizr 3.5.0 (Custom Build) | MIT * 6107 * https://modernizr.com/download/?-blobconstructor-localstorage-setclasses !*/ 6108 !function(e,n,o){function s(e,n){return typeof e===n}function t(){var e,n,o,t,a,l,c;for(var f in i)if(i.hasOwnProperty(f)){if(e=[],n=i[f],n.name&&(e.push(n.name.toLowerCase()),n.options&&n.options.aliases&&n.options.aliases.length))for(o=0;o<n.options.aliases.length;o++)e.push(n.options.aliases[o].toLowerCase());for(t=s(n.fn,"function")?n.fn():n.fn,a=0;a<e.length;a++)l=e[a],c=l.split("."),1===c.length?Modernizr[c[0]]=t:(!Modernizr[c[0]]||Modernizr[c[0]]instanceof Boolean||(Modernizr[c[0]]=new Boolean(Modernizr[c[0]])),Modernizr[c[0]][c[1]]=t),r.push((t?"":"no-")+c.join("-"))}}function a(e){var n=c.className,o=Modernizr._config.classPrefix||"";if(f&&(n=n.baseVal),Modernizr._config.enableJSClass){var s=new RegExp("(^|\\s)"+o+"no-js(\\s|$)");n=n.replace(s,"$1"+o+"js$2")}Modernizr._config.enableClasses&&(n+=" "+o+e.join(" "+o),f?c.className.baseVal=n:c.className=n)}var r=[],i=[],l={_version:"3.5.0",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(e,n){var o=this;setTimeout(function(){n(o[e])},0)},addTest:function(e,n,o){i.push({name:e,fn:n,options:o})},addAsyncTest:function(e){i.push({name:null,fn:e})}},Modernizr=function(){};Modernizr.prototype=l,Modernizr=new Modernizr,Modernizr.addTest("blobconstructor",function(){try{return!!new Blob}catch(e){return!1}},{aliases:["blob-constructor"]}),Modernizr.addTest("localstorage",function(){var e="modernizr";try{return localStorage.setItem(e,e),localStorage.removeItem(e),!0}catch(n){return!1}});var c=n.documentElement,f="svg"===c.nodeName.toLowerCase();t(),a(r),delete l.addTest,delete l.addAsyncTest;for(var u=0;u<Modernizr._q.length;
6108u++)Modernizr._q[u]();e.Modernizr=Modernizr}(window,document);/* 6109 * Leaflet selection legend. 6110 * 6111 * This is a Leaflet control designed to keep track of selected layers on a map 6112 * and visualize the selections as stacked bar graphs. 6113 */ 6114(function () { 6115 "use strict"; 6116 6117 if (typeof L === 'undefined') { 6118 return; 6119 } 6120 6121 L.Control.SelectionLegend = L.Control.extend({ 6122 6123 initialize: function(plugin) { 6124 this.selections = []; 6125 this.plugin = plugin; 6126 }, 6127 6128 addSelection: function(selection) { 6129 this.selections.push(selection); 6130 this.update(); 6131 }, 6132 6133 removeSelection: function(selection) { 6134 var index = this.selections.indexOf(selection); 6135 this.selections.splice(index, 1); 6136 this.update(); 6137 }, 6138 6139 isSelected: function(selection) { 6140 return (this.selections.indexOf(selection) !== -1); 6141 }, 6142 6143 onAdd: function() { 6144 var div = L.DomUtil.create('div', 'selection-legend'); 6145 this.legendDiv = div; 6146 this.resetSwatches(); 6147 return div; 6148 }, 6149 6150 renderSwatches: function() { 6151 var controlTpl = '' + 6152 '<dl id="selection-list"></dl>' + 6153 '<div class="legend-footer">' + 6154 '<div class="legend-swatches">' + 6155 '{legendSwatches}' + 6156 '</div>' + 6157 '<div class="legend-values">' + 6158 '<span class="legend-value left">{lowValue}</span>' + 6159 '<span class="arrow left"></span>' + 6160 '<span class="legend-value right">{highValue}</span>' + 6161 '<span class="arrow right"></span>' + 6162 '</div>' + 6163 '</div>'; 6164 var swatchTpl = '<span class="legend-swatch" style="width:{width}%; background:{color};"></span>'; 6165 var swatchWidth = 100 / this.plugin.options.colorRange.length; 6166 var swatches = this.plugin.options.colorRange.map(function(swatchColor) { 6167 return L.Util.template(swatchTpl, { 6168 width: swatchWidth, 6169 color: swatchColor, 6170 }); 6171 }).join(''); 6172 var context = { indicatorId: this.plugin.indicatorId }; 6173 return L.Util.template(controlTpl, { 6174 lowValue: this.plugin.alterData(opensdg.dataRounding(this.plugin.valueRanges[this.plugin.currentDisaggregation][0], context)), 6175 highValue: this.plugin.alterData(opensdg.dataRounding(this.plugin.valueRanges[this.plugin.currentDisaggregation][1], context)), 6176 legendSwatches: swatches, 6177 }); 6178 }, 6179 6180 resetSwatches: function() { 6181 this.legendDiv.innerHTML = this.renderSwatches(); 6182 }, 6183 6184 update: function() { 6185 var selectionList = L.DomUtil.get('selection-list'); 6186 var selectionTplHighValue = '' + 6187 '<dt class="selection-name"><span class="selection-name-background">{name}</span></dt>' + 6188 '<dd class="selection-value-item {valueStatus}">' + 6189 '<span class="selection-bar" style="background-color: {color}; width: {percentage}%;">' + 6190 '<span class="selection-value selection-value-high">' + 6191 '<span class="selection-value-high-background">{value}</span>' + 6192 '</span>' + 6193 '</span>' + 6194 '<i class="selection-close fa fa-remove"></i>' + 6195 '</dd>'; 6196 var selectionTplLowValue = '' + 6197 '<dt class="selection-name"><span class="selection-name-background">{name}</span></dt>' + 6198 '<dd class="selection-value-item {valueStatus}">' + 6199 '<span class="selection-bar" style="background-color: {color}; width: {percentage}%;"></span>' + 6200 '<span class="selection-value selection-value-low" style="left: {percentage}%;">' + 6201 '<span class="selection-value-low-background">{value}</span>' + 6202 '</span>' + 6203 '<i class="selection-close fa fa-remove"></i>' + 6204 '</dd>'; 6205 var plugin = this.plugin; 6206 var valueRange = this.plugin.valueRanges[this.plugin.currentDisaggregation]; 6207 selectionList.innerHTML = this.selections.map(function(selection) { 6208 var value = plugin.getData(selection.feature.properties); 6209 var color = '#FFFFFF'; 6210 var percentage, valueStatus; 6211 var templateToUse = selectionTplHighValue; 6212 if (typeof value === 'number') { 6213 color = plugin.colorScale(value).hex(); 6214 valueStatus = 'has-value'; 6215 var fraction = (value - valueRange[0]) / (valueRange[1] - valueRange[0]); 6216 percentage = Math.round(fraction * 100); 6217 if (percentage <= 50) { 6218 templateToUse = selectionTplLowValue; 6219 } 6220 } 6221 else { 6222 value = ''; 6223 valueStatus = 'no-value'; 6224 percentage = 0; 6225 } 6226 return L.Util.template(templateToUse, { 6227 name: selection.feature.properties.name, 6228 valueStatus: valueStatus, 6229 percentage: percentage, 6230 value: plugin.alterData(value),
6231 color: color, 6232 }); 6233 }).join(''); 6234 6235 // Assign click behavior. 6236 var control = this, 6237 clickSelector = '#selection-list dd'; 6238 $(clickSelector).click(function(e) { 6239 var index = $(clickSelector).index(this), 6240 selection = control.selections[index]; 6241 control.removeSelection(selection); 6242 control.plugin.unhighlightFeature(selection); 6243 }); 6244 } 6245 6246 }); 6247 6248 // Factory function for this class. 6249 L.Control.selectionLegend = function(plugin) { 6250 return new L.Control.SelectionLegend(plugin); 6251 }; 6252}()); 6253 6254/* 6255 * Leaflet year Slider. 6256 * 6257 * This is merely a specific configuration of Leaflet of L.TimeDimension. 6258 * See here: https://github.com/socib/Leaflet.TimeDimension 6259 */ 6260(function () { 6261 "use strict"; 6262 6263 if (typeof L === 'undefined') { 6264 return; 6265 } 6266 6267 var defaultOptions = { 6268 // YearSlider options. 6269 yearChangeCallback: null, 6270 years: [], 6271 // TimeDimensionControl options. 6272 timeSliderDragUpdate: true, 6273 speedSlider: false, 6274 position: 'bottomleft', 6275 playButton: false, 6276 }; 6277 6278 L.Control.YearSlider = L.Control.TimeDimension.extend({ 6279 6280 // Hijack the displayed date format. 6281 _getDisplayDateFormat: function(date){ 6282 var time = date.toISOString().slice(0, 10); 6283 var match = this.options.years.find(function(y) { return y.time == time; }); 6284 if (match) { 6285 return match.display; 6286 } 6287 else { 6288 return date.getFullYear(); 6289 } 6290 }, 6291 6292 // Override the _createButton method to prevent the date from being a link. 6293 _createButton: function(title, container) { 6294 if (title === 'Date') { 6295 var span = L.DomUtil.create('span', this.options.styleNS + ' timecontrol-' + title.toLowerCase(), container); 6296 span.title = title; 6297 return span; 6298 } 6299 else { 6300 return L.Control.TimeDimension.prototype._createButton.call(this, title, container); 6301 } 6302 }, 6303 6304 // Override the _createSliderTime method to give the slider accessibility features. 6305 _createSliderTime: function(className, container) { 6306 var knob = L.Control.TimeDimension.prototype._createSliderTime.call(this, className, container), 6307 control = this, 6308 times = this._timeDimension.getAvailableTimes(), 6309 years = times.map(function(time) { 6310 var date = new Date(time); 6311 return control._getDisplayDateFormat(date); 6312 }), 6313 minYear = years[0], 6314 maxYear = years[years.length - 1], 6315 knobElement = knob._element; 6316 6317 control._buttonBackward.title = translations.indicator.map_slider_back; 6318 control._buttonBackward.setAttribute('aria-label', control._buttonBackward.title); 6319 control._buttonForward.title = translations.indicator.map_slider_forward; 6320 control._buttonForward.setAttribute('aria-label', control._buttonForward.title); 6321 6322 knobElement.setAttribute('tabindex', '0'); 6323 knobElement.setAttribute('role', 'slider'); 6324 knobElement.setAttribute('aria-label', translations.indicator.map_slider_keyboard); 6325 knobElement.title = translations.indicator.map_slider_mouse; 6326 knobElement.setAttribute('aria-valuemin', minYear); 6327 knobElement.setAttribute('aria-valuemax', maxYear); 6328 6329 function updateSliderAttributes() { 6330 var yearIndex = 0; 6331 if (knob.getValue()) { 6332 yearIndex = knob.getValue(); 6333 } 6334 knobElement.setAttribute('aria-valuenow', years[yearIndex]); 6335 } 6336 updateSliderAttributes(); 6337 6338 // Give the slider left/right keyboard functionality. 6339 knobElement.addEventListener('keydown', function(e) { 6340 if (e.which === 37 || e.which === 40) { 6341 var min = knob.getMinValue(); 6342 var value = knob.getValue(); 6343 value = value - 1; 6344 if (value >= min) { 6345 knob.setValue(value); 6346 control._sliderTimeValueChanged(value); 6347 updateSliderAttributes(); 6348 } 6349 e.preventDefault(); 6350 } 6351 else if (e.which === 39 || e.which === 38) { 6352 var max = knob.getMaxValue(); 6353 var value = knob.getValue(); 6354 value = value + 1; 6355 if (value <= max) { 6356 knob.setValue(value); 6357 control._sliderTimeValueChanged(value); 6358 updateSliderAttributes(); 6359 } 6360 e.preventDefault(); 6361 } 6362 }); 6363 return knob; 6364 } 6365 6366 }); 6367 6368 // Helper function to compose the full widget. 6369 L.Control.yearSlider = function(options) { 6370 var years = getYears(options.years); 6371 // Extend the defaults. 6372 options = L.Util.extend(defaultOptions, options); 6373 // Hardcode the timeDimension to year intervals. 6374 options.timeDimension = new L.TimeDimension({ 6375 // We pad our years to at least January 2nd, so that timezone issues don't 6376 // cause any problems. This converts the array of years into a comma- 6377 // delimited string of YYYY-MM-DD dates. 6378 times: years.map(function(y) { return y.time }).join(','), 6379 //Set the map to the most recent year 6380 currentTime: new Date(years.slice(-1)[0].time).getTime(), 6381 }); 6382 // Listen for time changes. 6383 if (typeof options.yearChangeCallback === 'function') { 6384 options.timeDimension.on('timeload', options.yearChangeCallback); 6385 }; 6386 // Also pass in another callback for managing the back/forward buttons. 6387 options.timeDimension.on('timeload', function(e) { 6388 var currentTimeIndex = this.getCurrentTimeIndex(), 6389 availableTimes = this.getAvailableTimes(), 6390 $backwardButton = $('.timecontrol-backward'), 6391 $forwardButton = $('.timecontrol-forward'), 6392 isFirstTime = (currentTimeIndex === 0), 6393 isLastTime = (currentTimeIndex === availableTimes.length - 1); 6394 $backwardButton 6395 .attr('disabled', isFirstTime) 6396 .attr('aria-disabled', isFirstTime); 6397 $forwardButton 6398 .attr('disabled', isLastTime) 6399 .attr('aria-disabled', isLastTime); 6400 }); 6401 // Pass in our years for later use. 6402 options.years = years; 6403 // Return the control. 6404 return new L.Control.YearSlider(options); 6405 }; 6406 6407 function isYear(year) { 6408 var parsedInt = parseInt(year, 10); 6409 return /^\d+$/.test(year) && parsedInt > 1900 && parsedInt < 3000; 6410 } 6411 6412 function getYears(years) {
6413 // Support an array of years or an array of strings starting with years. 6414 var day = 2; 6415 return years.map(function(year) { 6416 var mapped = { 6417 display: year, 6418 time: year, 6419 }; 6420 // Usually this is a year. 6421 if (isYear(year)) { 6422 mapped.time = year + '-01-02'; 6423 // Start over that day variable. 6424 day = 2; 6425 } 6426 // Otherwise we get the year from the beginning of the string. 6427 else { 6428 var delimiters = ['-', '.', ' ', '/']; 6429 for (var i = 0; i < delimiters.length; i++) { 6430 var parts = year.split(delimiters[i]); 6431 if (parts.length > 1 && isYear(parts[0])) { 6432 mapped.time = parts[0] + '-01-0' + day; 6433 day += 1; 6434 break; 6435 } 6436 } 6437 } 6438 return mapped; 6439 }); 6440 } 6441}()); 6442/* 6443 * Leaflet fullscreenAccessible. 6444 * 6445 * This is an override of L.Control.Fullscreen for accessibility fixes. 6446 * See here: https://github.com/Leaflet/Leaflet.fullscreen 6447 */ 6448(function () { 6449 "use strict"; 6450 6451 if (typeof L === 'undefined') { 6452 return; 6453 } 6454 6455 L.Control.FullscreenAccessible = L.Control.Fullscreen.extend({ 6456 onAdd: function(map) { 6457 var container = L.Control.Fullscreen.prototype.onAdd.call(this, map); 6458 this.link.setAttribute('role', 'button'); 6459 this.link.setAttribute('aria-label', this.link.title); 6460 this.link.innerHTML = '<i class="fa fa-expand" aria-hidden="true"></i>'; 6461 return container; 6462 }, 6463 _toggleTitle: function() { 6464 L.Control.Fullscreen.prototype._toggleTitle.call(this); 6465 this.link.setAttribute('aria-label', this.link.title); 6466 var faClass = this._map.isFullscreen() ? 'fa-compress' : 'fa-expand' 6467 this.link.innerHTML = '<i class="fa ' + faClass + '" aria-hidden="true"></i>'; 6468 } 6469 }); 6470 6471 }()); 6472/* 6473 * Leaflet search. 6474 * 6475 * This is customized version of L.Control.Search. 6476 * See here: https://github.com/stefanocudini/leaflet-search 6477 */ 6478(function () { 6479 "use strict"; 6480 6481 if (typeof L === 'undefined') { 6482 return; 6483 } 6484 6485 L.Control.SearchAccessible = L.Control.Search.extend({ 6486 onAdd: function(map) { 6487 var container = L.Control.Search.prototype.onAdd.call(this, map); 6488 6489 this._input.setAttribute('aria-label', this._input.placeholder); 6490 this._input.removeAttribute('role'); 6491 this._tooltip.setAttribute('aria-label', this._input.placeholder); 6492 6493 this._button.setAttribute('role', 'button'); 6494 this._accessibleCollapse(); 6495 this._button.innerHTML = '<i class="fa fa-search" aria-hidden="true"></i>'; 6496 6497 this._cancel.setAttribute('role', 'button'); 6498 this._cancel.title = translations.indicator.map_search_cancel; 6499 this._cancel.setAttribute('aria-label', this._cancel.title); 6500 this._cancel.innerHTML = '<i class="fa fa-close" aria-hidden="true"></i>'; 6501 6502 // Prevent the delayed collapse when tabbing out of the input box. 6503 L.DomEvent.on(this._cancel, 'focus', this.collapseDelayedStop, this); 6504 6505 return container; 6506 }, 6507 _createInput: function (text, className) { 6508 var input = L.Control.Search.prototype._createInput.call(this, text, className); 6509 input.setAttribute('aria-autocomplete', 'list'); 6510 input.setAttribute('aria-controls', 'map-search-listbox'); 6511 var combobox = L.DomUtil.create('div', '', this._container); 6512 combobox.setAttribute('role', 'combobox'); 6513 combobox.setAttribute('aria-expanded', 'false'); 6514 combobox.setAttribute('aria-owns', 'map-search-listbox'); 6515 combobox.setAttribute('aria-haspopup', 'listbox'); 6516 combobox.id = 'map-search-combobox'; 6517 combobox.append(input); 6518 this._combobox = combobox; 6519 return input; 6520 }, 6521 _createTooltip: function(className) { 6522 var tooltip = L.Control.Search.prototype._createTooltip.call(this, className); 6523 tooltip.id = 'map-search-listbox'; 6524 tooltip.setAttribute('role', 'listbox'); 6525 return tooltip; 6526 }, 6527 _accessibleExpand: function() { 6528 this._accessibleDescription(translations.indicator.map_search_hide); 6529 this._button.setAttribute('aria-expanded', 'true'); 6530 }, 6531 _accessibleCollapse: function() { 6532 this._accessibleDescription(translations.indicator.map_search_show); 6533 this._button.setAttribute('aria-expanded', 'false'); 6534 this._button.focus(); 6535 }, 6536 _accessibleDescription: function(description) { 6537 this._button.title = description; 6538 this._button.setAttribute('aria-label', description); 6539 }, 6540 expand: function(toggle) { 6541 L.Control.Search.prototype.expand.call(this, toggle); 6542 this._accessibleExpand(); 6543 return this; 6544 }, 6545 collapse: function() { 6546 L.Control.Search.prototype.collapse.call(this); 6547 this._accessibleCollapse(); 6548 return this; 6549 }, 6550 cancel: function() { 6551 L.Control.Search.prototype.cancel.call(this); 6552 this._accessibleExpand(); 6553 this._combobox.setAttribute('aria-expanded', 'false'); 6554 this._input.removeAttribute('aria-activedescendant'); 6555 return this; 6556 }, 6557 showTooltip: function(records) { 6558 L.Control.Search.prototype.showTooltip.call(this, records); 6559 this._accessibleDescription(translations.indicator.map_search); 6560 this._button.removeAttribute('aria-expanded'); 6561 this._combobox.setAttribute('aria-expanded', 'true'); 6562 if (this._countertips > 0) { 6563 this._input.setAttribute('aria-activedescendant', this._tooltip.childNodes[0].id); 6564 } 6565 return this._countertips; 6566 }, 6567 _createTip: function(text, val) { 6568 var tip = L.Control.Search.prototype._createTip.call(this, text, val); 6569 tip.setAttribute('role', 'option'); 6570 tip.id = 'map-search-option-' + val.layer.feature.properties.geocode; 6571 return tip; 6572 }, 6573 _handleSubmit: function(e) { 6574 // Prevent the enter key from immediately collapsing the search bar. 6575 if ((typeof e === 'undefined' || e.type === 'keyup') && this._input.value === '') { 6576 return; 6577 } 6578 if (this._tooltip.childNodes.length > 0 && this._input.value !== '') { 6579 // This is a workaround for the bug where non-exact matches 6580 // do not successfully search. See this Github issue: 6581 // https://github.com/stefanocudini/leaflet-search/issues/264 6582 var firstSuggestion = this._tooltip.childNodes[0].innerText; 6583 var firstSuggestionLower = firstSuggestion.toLowerCase(); 6584 var userInput = this._input.value; 6585 var userInputLower = userInput.toLowerCase();
6586 if (firstSuggestion !== userInput && firstSuggestionLower.includes(userInputLower)) { 6587 this._input.value = firstSuggestion; 6588 } 6589 } 6590 L.Control.Search.prototype._handleSubmit.call(this, e); 6591 }, 6592 _handleArrowSelect: function(velocity) { 6593 L.Control.Search.prototype._handleArrowSelect.call(this, velocity); 6594 var searchTips = this._tooltip.hasChildNodes() ? this._tooltip.childNodes : []; 6595 for (i=0; i<searchTips.length; i++) { 6596 searchTips[i].setAttribute('aria-selected', 'false'); 6597 } 6598 var selectedTip = searchTips[this._tooltip.currentSelection]; 6599 if (typeof selectedTip === 'undefined') { 6600 selectedTip = searchTips[0]; 6601 } 6602 selectedTip.setAttribute('aria-selected', 'true'); 6603 this._input.setAttribute('aria-activedescendant', selectedTip.id); 6604 }, 6605 _createAlert: function(className) { 6606 var alert = L.Control.Search.prototype._createAlert.call(this, className); 6607 alert.setAttribute('role', 'alert'); 6608 return alert; 6609 } 6610 }); 6611}()); 6612/* 6613 * Leaflet disaggregation controls. 6614 * 6615 * This is a Leaflet control designed replicate the disaggregation 6616 * controls that are in the sidebar for tables and charts. 6617 */ 6618(function () { 6619 "use strict"; 6620 6621 if (typeof L === 'undefined') { 6622 return; 6623 } 6624 6625 L.Control.DisaggregationControls = L.Control.extend({ 6626 6627 options: { 6628 position: 'bottomleft' 6629 }, 6630 6631 initialize: function (plugin) { 6632 this.plugin = plugin; 6633 this.list = null; 6634 this.form = null; 6635 this.currentDisaggregation = 0; 6636 this.displayedDisaggregation = 0; 6637 this.needsMapUpdate = false; 6638 this.seriesColumn = 'Series'; 6639 this.unitsColumn = 'Units'; 6640 this.displayForm = true; 6641 this.updateDisaggregations(plugin.startValues); 6642 }, 6643 6644 updateDisaggregations: function(startValues) { 6645 // TODO: Not all of this needs to be done 6646 // at every update. 6647 var features = this.getFeatures(); 6648 if (startValues && startValues.length > 0 && startValues != '""' ) { 6649 this.currentDisaggregation = this.getStartingDisaggregation(features, startValues); 6650 this.displayedDisaggregation = this.currentDisaggregation; 6651 this.needsMapUpdate = true; 6652 } 6653 this.disaggregations = this.getVisibleDisaggregations(features); 6654 this.fieldsInOrder = this.getFieldsInOrder(); 6655 this.valuesInOrder = this.getValuesInOrder(); 6656 this.allSeries = this.getAllSeries(); 6657 this.allUnits = this.getAllUnits(); 6658 this.allDisaggregations = this.getAllDisaggregations(); 6659 this.hasSeries = (this.allSeries.length > 0); 6660 this.hasUnits = (this.allUnits.length > 0); 6661 this.hasDisaggregations = this.hasDissagregationsWithValues(); 6662 this.hasDisaggregationsWithMultipleValuesFlag = this.hasDisaggregationsWithMultipleValues(); 6663 }, 6664 6665 getFeatures: function() { 6666 return this.plugin.getVisibleLayers().toGeoJSON().features.filter(function(feature) { 6667 return typeof feature.properties.disaggregations !== 'undefined'; 6668 }); 6669 }, 6670 6671 getStartingDisaggregation: function(features, startValues) { 6672 if (features.length === 0) { 6673 return; 6674 } 6675 6676 var disaggregations = features[0].properties.disaggregations, 6677 fields = Object.keys(disaggregations[0]), 6678 validStartValues = startValues.filter(function(startValue) { 6679 return fields.includes(startValue.field); 6680 }), 6681 weighted = _.sortBy(disaggregations.map(function(disaggregation, index) { 6682 var disaggClone = Object.assign({}, disaggregation); 6683 disaggClone.emptyFields = 0; 6684 disaggClone.index = index; 6685 fields.forEach(function(field) { 6686 if (disaggClone[field] == '') { 6687 disaggClone.emptyFields += 1; 6688 } 6689 }); 6690 return disaggClone; 6691 }), 'emptyFields').reverse(), 6692 match = weighted.find(function(disaggregation) { 6693 return _.every(validStartValues, function(startValue) { 6694 return disaggregation[startValue.field] === startValue.value; 6695 }); 6696 }); 6697 if (match) { 6698 return match.index; 6699 } 6700 else { 6701 return 0; 6702 } 6703 }, 6704 6705 getVisibleDisaggregations: function(features) { 6706 if (features.length === 0) { 6707 return []; 6708 } 6709 6710 var disaggregations = features[0].properties.disaggregations;
6711 // The purpose of the rest of this function is to identiy 6712 // and remove any "region columns" - ie, any columns that 6713 // correspond exactly to names of map regions. These columns 6714 // are useful on charts and tables but should not display 6715 // on maps. 6716 var allKeys = Object.keys(disaggregations[0]); 6717 var relevantKeys = {}; 6718 var rememberedValues = {}; 6719 disaggregations.forEach(function(disagg) { 6720 for (var i = 0; i < allKeys.length; i++) { 6721 var key = allKeys[i]; 6722 if (rememberedValues[key]) { 6723 if (rememberedValues[key] !== disagg[key]) { 6724 relevantKeys[key] = true; 6725 } 6726 } 6727 rememberedValues[key] = disagg[key]; 6728 } 6729 }); 6730 relevantKeys = Object.keys(relevantKeys); 6731 if (features.length > 1) { 6732 // Any columns not already identified as "relevant" might 6733 // be region columns. 6734 var regionColumnCandidates = allKeys.filter(function(item) { 6735 return relevantKeys.includes(item) ? false : true; 6736 }); 6737 // Compare the column value across map regions - if it is 6738 // different then we assume the column is a "region column". 6739 // For efficiency we only check the first and second region. 6740 var regionColumns = regionColumnCandidates.filter(function(candidate) { 6741 var region1 = features[0].properties.disaggregations[0][candidate]; 6742 var region2 = features[1].properties.disaggregations[0][candidate]; 6743 return region1 === region2 ? false : true; 6744 }); 6745 // Now we can treat any non-region columns as relevant. 6746 regionColumnCandidates.forEach(function(item) { 6747 if (!regionColumns.includes(item)) { 6748 relevantKeys.push(item); 6749 } 6750 }); 6751 } 6752 relevantKeys.push(this.seriesColumn); 6753 relevantKeys.push(this.unitsColumn); 6754 var pruned = []; 6755 disaggregations.forEach(function(disaggregation) { 6756 var clone = Object.assign({}, disaggregation); 6757 Object.keys(clone).forEach(function(key) { 6758 if (!(relevantKeys.includes(key))) { 6759 delete clone[key]; 6760 } 6761 }); 6762 pruned.push(clone); 6763 }); 6764 return pruned; 6765 }, 6766 6767 update: function() { 6768 this.updateDisaggregations(); 6769 this.updateList(); 6770 if (this.displayForm) { 6771 this.updateForm(); 6772 } 6773 }, 6774 6775 getFieldsInOrder: function () { 6776 return this.plugin.dataSchema.fields.map(function(field) { 6777 return field.name; 6778 }); 6779 }, 6780 6781 getValuesInOrder: function () { 6782 var valuesInOrder = {}; 6783 this.plugin.dataSchema.fields.forEach(function(field) { 6784 if (field.constraints && field.constraints.enum) { 6785 valuesInOrder[field.name] = field.constraints.enum; 6786 } 6787 }); 6788 return valuesInOrder; 6789 }, 6790 6791 hasDissagregationsWithValues: function () { 6792 var hasDisaggregations = false; 6793 this.allDisaggregations.forEach(function(disaggregation) { 6794 if (disaggregation.values.length > 0 && disaggregation.values[0] !== '') { 6795 hasDisaggregations = true; 6796 } 6797 }); 6798 return hasDisaggregations; 6799 }, 6800 6801 hasDisaggregationsWithMultipleValues: function () { 6802 var hasDisaggregations = false; 6803 this.allDisaggregations.forEach(function(disaggregation) { 6804 if (disaggregation.values.length > 1 && disaggregation.values[1] !== '') { 6805 hasDisaggregations = true; 6806 } 6807 }); 6808 return hasDisaggregations; 6809 }, 6810 6811 updateList: function () { 6812 var list = this.list; 6813 list.innerHTML = '';
6814 if (this.hasSeries) { 6815 var title = L.DomUtil.create('dt', 'disaggregation-title'), 6816 definition = L.DomUtil.create('dd', 'disaggregation-definition'), 6817 container = L.DomUtil.create('div', 'disaggregation-container'); 6818 title.innerHTML = translations.indicator.series; 6819 definition.innerHTML = this.getCurrentSeries(); 6820 container.append(title); 6821 container.append(definition); 6822 list.append(container); 6823 } 6824 if (this.hasUnits) { 6825 var title = L.DomUtil.create('dt', 'disaggregation-title'), 6826 definition = L.DomUtil.create('dd', 'disaggregation-definition'), 6827 container = L.DomUtil.create('div', 'disaggregation-container'); 6828 title.innerHTML = translations.indicator.unit; 6829 definition.innerHTML = this.getCurrentUnit(); 6830 container.append(title); 6831 container.append(definition); 6832 list.append(container); 6833 } 6834 if (this.hasDisaggregations) { 6835 var currentDisaggregation = this.disaggregations[this.currentDisaggregation]; 6836 this.allDisaggregations.forEach(function(disaggregation) { 6837 var title = L.DomUtil.create('dt', 'disaggregation-title'), 6838 definition = L.DomUtil.create('dd', 'disaggregation-definition'), 6839 container = L.DomUtil.create('div', 'disaggregation-container'), 6840 field = disaggregation.field; 6841 title.innerHTML = translations.t(field); 6842 var disaggregationValue = currentDisaggregation[field]; 6843 if (disaggregationValue !== '') { 6844 definition.innerHTML = disaggregationValue; 6845 container.append(title); 6846 container.append(definition); 6847 list.append(container); 6848 } 6849 }); 6850 } 6851 }, 6852 6853 updateForm: function() { 6854 var seriesColumn = this.seriesColumn, 6855 unitsColumn = this.unitsColumn, 6856 container = this.form, 6857 formInputs = L.DomUtil.create('div', 'disaggregation-form-inner'), 6858 that = this; 6859 container.innerHTML = ''; 6860 container.append(formInputs) 6861 L.DomEvent.disableScrollPropagation(formInputs); 6862 if (this.hasSeries) { 6863 var form = L.DomUtil.create('div', 'disaggregation-fieldset-container'), 6864 legend = L.DomUtil.create('legend', 'disaggregation-fieldset-legend'), 6865 fieldset = L.DomUtil.create('fieldset', 'disaggregation-fieldset'); 6866 legend.innerHTML = translations.indicator.series; 6867 fieldset.append(legend); 6868 form.append(fieldset); 6869 formInputs.append(form); 6870 this.allSeries.forEach(function(series) { 6871 var input = L.DomUtil.create('input', 'disaggregation-input'); 6872 input.type = 'radio'; 6873 input.name = 'map-' + seriesColumn; 6874 input.value = series; 6875 input.tabindex = 0; 6876 input.checked = (series === that.getCurrentSeries()) ? 'checked' : ''; 6877 var label = L.DomUtil.create('label', 'disaggregation-label'); 6878 label.innerHTML = series; 6879 if (that.plugin.proxySerieses.includes(series)) { 6880 label.innerHTML += ' ' + that.plugin.viewHelpers.PROXY_PILL; 6881 } 6882 label.prepend(input); 6883 fieldset.append(label); 6884 input.addEventListener('change', function(e) { 6885 that.currentDisaggregation = that.getSelectedDisaggregationIndex(seriesColumn, series); 6886 that.updateForm(); 6887 }); 6888 }); 6889 } 6890 if (this.hasUnits) { 6891 var form = L.DomUtil.create('div', 'disaggregation-fieldset-container'), 6892 legend = L.DomUtil.create('legend', 'disaggregation-fieldset-legend'), 6893 fieldset = L.DomUtil.create('fieldset', 'disaggregation-fieldset'); 6894 legend.innerHTML = translations.indicator.unit_of_measurement; 6895 fieldset.append(legend); 6896 form.append(fieldset); 6897 formInputs.append(form);
6898 this.allUnits.forEach(function(unit) { 6899 var input = L.DomUtil.create('input', 'disaggregation-input'); 6900 if (that.isDisaggegrationValidGivenCurrent(unitsColumn, unit)) { 6901 input.type = 'radio'; 6902 input.name = 'map-' + unitsColumn; 6903 input.value = unit; 6904 input.tabindex = 0; 6905 input.checked = (unit === that.getCurrentUnit()) ? 'checked' : ''; 6906 var label = L.DomUtil.create('label', 'disaggregation-label'); 6907 label.innerHTML = unit; 6908 label.prepend(input); 6909 fieldset.append(label); 6910 input.addEventListener('change', function(e) { 6911 that.currentDisaggregation = that.getSelectedDisaggregationIndex(unitsColumn, unit); 6912 that.updateForm(); 6913 }); 6914 } 6915 }); 6916 } 6917 if (this.hasDisaggregations) { 6918 var currentDisaggregation = this.disaggregations[this.currentDisaggregation]; 6919 this.allDisaggregations.forEach(function (disaggregation) { 6920 var form = L.DomUtil.create('div', 'disaggregation-fieldset-container'), 6921 legend = L.DomUtil.create('legend', 'disaggregation-fieldset-legend'), 6922 fieldset = L.DomUtil.create('fieldset', 'disaggregation-fieldset'), 6923 field = disaggregation.field; 6924 legend.innerHTML = translations.t(field); 6925 fieldset.append(legend); 6926 form.append(fieldset); 6927 formInputs.append(form); 6928 disaggregation.values.forEach(function (value) { 6929 var input = L.DomUtil.create('input', 'disaggregation-input'); 6930 if (that.isDisaggegrationValidGivenCurrent(field, value)) { 6931 input.type = 'radio'; 6932 input.name = 'map-' + field; 6933 input.value = value; 6934 input.tabindex = 0; 6935 input.checked = (value === currentDisaggregation[field]) ? 'checked' : ''; 6936 var label = L.DomUtil.create('label', 'disaggregation-label'); 6937 label.innerHTML = (value === '') ? translations.indicator.total : value; 6938 label.prepend(input); 6939 fieldset.append(label); 6940 input.addEventListener('change', function(e) { 6941 that.currentDisaggregation = that.getSelectedDisaggregationIndex(field, value); 6942 that.updateForm(); 6943 }); 6944 } 6945 }); 6946 }); 6947 } 6948 6949 var applyButton = L.DomUtil.create('button', 'disaggregation-apply-button'), 6950 cancelButton = L.DomUtil.create('button', 'disaggregation-cancel-button'), 6951 buttonContainer = L.DomUtil.create('div', 'disaggregation-form-buttons'); 6952 applyButton.innerHTML = translations.indicator.apply; 6953 buttonContainer.append(applyButton); 6954 cancelButton.innerHTML = translations.indicator.cancel; 6955 buttonContainer.append(cancelButton); 6956 container.append(buttonContainer); 6957 6958 cancelButton.addEventListener('click', function(e) { 6959 that.currentDisaggregation = that.displayedDisaggregation; 6960 $('.disaggregation-form-outer').toggle(); 6961 that.updateForm(); 6962 }); 6963 applyButton.addEventListener('click', function(e) { 6964 that.updateMap(); 6965 that.updateList(); 6966 $('.disaggregation-form-outer').toggle(); 6967 }); 6968 }, 6969 6970 updateMap: function() { 6971 this.needsMapUpdate = false; 6972 this.plugin.currentDisaggregation = this.currentDisaggregation; 6973 this.plugin.updatePrecision(); 6974 this.plugin.setColorScale(); 6975 this.plugin.updateColors(); 6976 this.plugin.updateTooltips(); 6977 this.plugin.selectionLegend.resetSwatches(); 6978 this.plugin.selectionLegend.update(); 6979 this.plugin.updateTitle(); 6980 this.plugin.updateFooterFields(); 6981 this.plugin.replaceYearSlider(); 6982 }, 6983 6984 onAdd: function () { 6985 var div = L.DomUtil.create('div', 'disaggregation-controls'), 6986 list = L.DomUtil.create('dl', 'disaggregation-list'), 6987 that = this; 6988 6989 if (this.hasSeries || this.hasUnits || this.hasDisaggregations) { 6990 this.list = list; 6991 div.append(list); 6992 this.updateList(); 6993 6994 var numSeries = this.allSeries.length, 6995 numUnits = this.allUnits.length, 6996 displayForm = this.displayForm; 6997 6998 if (displayForm && (this.hasDisaggregationsWithMultipleValuesFlag || (numSeries > 1 || numUnits > 1))) { 6999 7000 var button = L.DomUtil.create('button', 'disaggregation-button'); 7001 button.innerHTML = translations.indicator.change_breakdowns; 7002 button.addEventListener('click', function(e) { 7003 that.displayedDisaggregation = that.currentDisaggregation; 7004 $('.disaggregation-form-outer').show(); 7005 }); 7006 div.append(button); 7007 7008 var container = L.DomUtil.create('div', 'disaggregation-form'); 7009 var containerOuter = L.DomUtil.create('div', 'disaggregation-form-outer'); 7010 containerOuter.append(container); 7011 this.form = container; 7012 div.append(containerOuter); 7013 this.updateForm(); 7014 } 7015 } 7016 7017 return div; 7018 }, 7019 7020 getCurrentSeries: function() { 7021 var disaggregation = this.disaggregations[this.currentDisaggregation]; 7022 return disaggregation[this.seriesColumn]; 7023 }, 7024 7025 getCurrentUnit: function() { 7026 var disaggregation = this.disaggregations[this.currentDisaggregation]; 7027 return disaggregation[this.unitsColumn]; 7028 }, 7029 7030 getAllSeries: function () { 7031 var seriesColumn = this.seriesColumn; 7032 if (typeof this.disaggregations[0][seriesColumn] === 'undefined' || !this.disaggregations[0][seriesColumn]) { 7033 return []; 7034 } 7035 var allSeries = _.uniq(this.disaggregations.map(function(disaggregation) { 7036 return disaggregation[seriesColumn]; 7037 })); 7038 var sortedSeries = this.valuesInOrder[seriesColumn]; 7039 allSeries.sort(function(a, b) { 7040 return sortedSeries.indexOf(a) - sortedSeries.indexOf(b); 7041 }); 7042 return allSeries; 7043 }, 7044 7045 getAllUnits: function () { 7046 var unitsColumn = this.unitsColumn; 7047 if (typeof this.disaggregations[0][unitsColumn] === 'undefined' || !this.disaggregations[0][unitsColumn]) { 7048 return []; 7049 } 7050 var allUnits = _.uniq(this.disaggregations.map(function(disaggregation) { 7051 return disaggregation[unitsColumn]; 7052 })); 7053 var sortedUnits = this.valuesInOrder[unitsColumn]; 7054 allUnits.sort(function(a, b) { 7055 return sortedUnits.indexOf(a) - sortedUnits.indexOf(b); 7056 }); 7057 return allUnits; 7058 }, 7059 7060 getAllDisaggregations: function () { 7061 var disaggregations = this.disaggregations, 7062 valuesInOrder = this.valuesInOrder, 7063 validFields = Object.keys(disaggregations[0]), 7064 invalidFields = [this.seriesColumn, this.unitsColumn], 7065 allDisaggregations = []; 7066 if (this.plugin.configObsAttributes && this.plugin.configObsAttributes.length > 0) {
7067 this.plugin.configObsAttributes.forEach(function(obsAttribute) { 7068 invalidFields.push(obsAttribute.field); 7069 }); 7070 } 7071 7072 this.fieldsInOrder.forEach(function(field) { 7073 if (!(invalidFields.includes(field)) && validFields.includes(field)) { 7074 var sortedValues = valuesInOrder[field], 7075 item = { 7076 field: field, 7077 values: _.uniq(disaggregations.map(function(disaggregation) { 7078 return disaggregation[field]; 7079 })), 7080 }; 7081 if (typeof sortedValues === 'undefined') { 7082 return; 7083 } 7084 item.values.sort(function(a, b) { 7085 return sortedValues.indexOf(a) - sortedValues.indexOf(b); 7086 }); 7087 allDisaggregations.push(item); 7088 } 7089 }); 7090 7091 return allDisaggregations; 7092 }, 7093 7094 getSelectedDisaggregationIndex: function(changedKey, newValue) { 7095 for (var i = 0; i < this.disaggregations.length; i++) { 7096 var disaggregation = this.disaggregations[i], 7097 keys = Object.keys(disaggregation), 7098 matchesSelections = true; 7099 for (var j = 0; j < keys.length; j++) { 7100 var key = keys[j], 7101 inputName = 'map-' + key, 7102 $inputElement = $('input[name="' + inputName + '"]:checked'), 7103 selection = $inputElement.val(); 7104 if ($inputElement.length > 0 && selection !== disaggregation[key]) { 7105 matchesSelections = false; 7106 break; 7107 } 7108 } 7109 if (matchesSelections) { 7110 return i; 7111 } 7112 } 7113 // If we are still here, it means that a recent change 7114 // has resulted in an illegal combination. In this case 7115 // we look at the recently-changed key and its value, 7116 // and we pick the first disaggregation that matches. 7117 for (var i = 0; i < this.disaggregations.length; i++) { 7118 var disaggregation = this.disaggregations[i], 7119 keys = Object.keys(disaggregation); 7120 if (keys.includes(changedKey) && disaggregation[changedKey] === newValue) { 7121 return i; 7122 } 7123 } 7124 // If we are still here, something went wrong. 7125 throw('Could not find match'); 7126 }, 7127 7128 isDisaggegrationValidGivenCurrent: function(field, value) { 7129 var currentDisaggregation = Object.assign({}, this.disaggregations[this.currentDisaggregation]); 7130 currentDisaggregation[field] = value; 7131 var keys = Object.keys(currentDisaggregation); 7132 for (var i = 0; i < this.disaggregations.length; i++) { 7133 var valid = true; 7134 var otherDisaggregation = this.disaggregations[i]; 7135 for (var j = 0; j < keys.length; j++) { 7136 var key = keys[j]; 7137 if (currentDisaggregation[key] !== otherDisaggregation[key]) { 7138 valid = false; 7139 } 7140 } 7141 if (valid) { 7142 return true; 7143 } 7144 } 7145 return false; 7146 }, 7147 7148 }); 7149 7150 // Factory function for this class. 7151 L.Control.disaggregationControls = function (plugin) { 7152 return new L.Control.DisaggregationControls(plugin); 7153 }; 7154}()); 7155$(document).ready(function() { 7156 $('a[href="#top"]').prepend('<svg class="app-c-back-to-top__icon" xmlns="http://www.w3.org/2000/svg" width="13" height="17" viewBox="0 0 13 17" aria-hidden="true" focusable="false"><path fill="currentColor" d="M6.5 0L0 6.5 1.4 8l4-4v12.7h2V4l4.3 4L13 6.4z"></path></svg>'); 7157}); 7158function confirm_alert(source, lang) { 7159 if (source && source != '') { 7160 if (lang == 'De'){ 7161 var text = 'Sie verlassen unsere Webseite!\nDer Link führt Sie zur Webseite ' 7162 } else{ 7163 var text = 'You are leaving our website!\nThe link leads to the website of ' 7164 } 7165 return confirm(text + source + '.'); 7166 } else { 7167 if (lang == 'De'){ 7168 var text = 'Sie verlassen unsere Webseite!\nDer Link führt Sie zu einer externen Webseite.' 7169 }else{ 7170 var text = 'You are leaving our website!\nThe link leads to an external website.' 7171 } 7172 return confirm(text) 7173 } 7174}
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.