From 20d5ca5d5e922a8e64ab8672177f6f20034e22e4 Mon Sep 17 00:00:00 2001 From: slowe <299787+slowe@users.noreply.github.com> Date: Thu, 7 Sep 2023 13:51:40 +0100 Subject: [PATCH] Add warnings, save CSV and save image The warnings are explanations to go with the colour scale. I've added a new "Download CSV for this view" button which will generate a CSV file for the current view. I've added a "Save map as PNG" link that will save a screenshot of the map and scale. --- 2023-DFES/index.html | 63 +++++++----- 2023-DFES/resources/config.js | 130 +++++++++++++++++++++++- 2023-DFES/resources/dom-to-image.min.js | 2 + 2023-DFES/resources/style.css | 21 ++++ 4 files changed, 191 insertions(+), 25 deletions(-) create mode 100644 2023-DFES/resources/dom-to-image.min.js diff --git a/2023-DFES/index.html b/2023-DFES/index.html index eb30e8a..b623fda 100644 --- a/2023-DFES/index.html +++ b/2023-DFES/index.html @@ -13,6 +13,7 @@ + @@ -72,37 +73,52 @@

:

+ +

Select year ():

-

:

-
- In year -
- - -
- +

To better show changes in values over time you may wish to change the colour scale.

+ +
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ : +
+ In year +
+ +
+ +
+ +
+

Select In year and the colour scale will change depending on the year you have selected. This will help improve contrast for each area within your chosen year. Select By 2050 and the colour scale will stay the same as you move through the years.

-
- +
@@ -196,7 +212,8 @@

Falling Short — formerly known as Steady Progression

Geographies

-

For the DFES visualisations we created geographies (polygons) for each Primary substation in Northern Powergrid's network. We chose to use Output Areas (2011) as these are the building blocks of census geography as used by the Office of National Statistics. We used customer postcodes to identify each Output Area supplied by each Primary substation. Output Areas with fewer than 10 customers connected to a Primary substation were excluded. This helped with anonymisation and also reduced data issues in the customer database e.g. incorrect customer postcodes. We constructed representative geographies for each Primary substation from the remaining Output Areas. In cases where multiple Primary substations serve the same Output Area, that Output Area was assigned to the Primary substation that serves the most customers.

+

To show data on a map we need to have unique polygons for each Primary substation. To do this we used customer postcodes to identify each ONS Output Area supplied by each Primary substation using a lookup table from ONS. Output Areas with fewer than 10 customers connected to a Primary substation were excluded. This helped with anonymisation and also reduced data issues in the customer database e.g. incorrect customer postcodes. We constructed representative geographies for each Primary substation from the remaining Output Areas. In cases where multiple Primary substations serve the same Output Area, that Output Area was assigned to the Primary substation that serves the most customers. For the 2023 DFES, we have built new polygons for each Primary substation in Northern Powergrid's network using the latest postcode data and the latest Output Areas produced from the 2021 Census.

+

Knowing the Output Areas connected to each Primary allows us to also build a mapping from Primary to Local Authority.

Some Primary substation geographies may show larger areas on the map than they cover in practice particularly in rural areas where network connectivity may be concentrated in specific parts of an Output Area. The areas shown here are representative for the purpose of showing the Future Energy Scenario model data and should not be relied upon for checking connectivity or to assess the terms of connection for specific premises.

In the Primaries layer we have included Bulk and Grid Supply Points as "pseudo-Primaries" to give Local Authorities the full picture in their area. These pseudo-Primaries appear as small cut-outs. They are used to include large generation and storage connections that are not seen by Primary substations.

Local Authority view

diff --git a/2023-DFES/resources/config.js b/2023-DFES/resources/config.js index e896816..d0487e2 100644 --- a/2023-DFES/resources/config.js +++ b/2023-DFES/resources/config.js @@ -1,16 +1,43 @@ // Define a new instance of the FES var dfes +function saveDOMImage(el,opt){ + if(!opt) opt = {}; + if(!opt.src) opt.src = "map.png"; + if(opt.scale){ + if(!opt.height) opt.height = el.offsetHeight*2; + if(!opt.width) opt.width = el.offsetWidth*2; + // Force bigger size for element + w = el.style.getPropertyValue('width'); + h = el.style.getPropertyValue('height'); + el.style.setProperty('width',(opt.width)+'px'); + el.style.setProperty('height',(opt.height)+'px'); + } + el.classList.add('capture'); + domtoimage.toPng(el,opt).then(function(dataUrl){ + var link = document.createElement('a'); + link.download = opt.src; + link.href = dataUrl; + link.click(); + // Reset element + if(opt.scale){ + el.style.setProperty('width',w); + el.style.setProperty('height',h); + } + el.classList.remove('capture'); + }); +} + S(document).ready(function(){ dfes = new FES({ "options": { "scenario": "NPg Planning Scenario", "view": "LAD", - "key": "2021", + "key": "2022", "parameter": "ev", "scale": "relative", - "years": {"min":2021, "max":2050}, + "years": {"min":2022, "max":2050}, "map": { "bounds": [[52.6497,-5.5151],[56.01680,2.35107]], "attribution": "Vis: Open Innovations, Data: NPG/Element Energy" @@ -195,6 +222,14 @@ S(document).ready(function(){ "setParameter": function(){ if(OI.log) OI.log.add('action=click&content='+this.parameters[this.options.parameter].title); }, + "setScale": function(t){ + var abs = document.querySelectorAll("[data-scale='absolute']"); + var rel = document.querySelectorAll("[data-scale='relative']"); + console.log('setScale',abs,rel,t); + if(abs.length > 0) abs.forEach(function(e){ e.style.display = (t=="absolute") ? '' : 'none'; }); + if(rel.length > 0) rel.forEach(function(e){ e.style.display = (t=="relative") ? '' : 'none'; }); + return this; + }, "buildMap": function(){ var el,div,_obj; el = document.querySelector('.leaflet-top.leaflet-left'); @@ -369,4 +404,95 @@ S(document).ready(function(){ } } }); + + + + // Add download button + if(S('#download-csv')){ + S('#download-csv').on('click',{me:dfes},function(e){ + e.preventDefault(); + e.stopPropagation(); + var csv = ""; + var opt = e.data.me.options; + var filename = ("DFES-2022--{{scenario}}--{{parameter}}--{{view}}.csv").replace(/\{\{([^\}]+)\}\}/g,function(m,p1){ return (opt[p1]||"").replace(/[ ]/g,"_") }); + var values,r,rs,y,v,l,layerid,p,ky,nm; + values = e.data.me.data.scenarios[e.data.me.options.scenario].data[e.data.me.options.parameter].layers[e.data.me.options.view].values; + v = e.data.me.options.view; + layerid = ''; + // We need to loop over the view's layers + for(l = 0; l < e.data.me.views[v].layers.length; l++){ + if(e.data.me.views[v].layers[l].heatmap) layerid = l; + } + ky = e.data.me.layers[e.data.me.views[v].layers[layerid].id].key; + nm = e.data.me.layers[e.data.me.views[v].layers[layerid].id].name; + if(typeof ky==="undefined") console.warn('No key provided for this layer in the layers structure.'); + if(typeof nm==="undefined") console.warn('No name provided for this layer in the layers structure.'); + + rs = Object.keys(values).sort(); + csv = ky.toUpperCase()+','+e.data.me.views[v].title; + for(y = e.data.me.options.years.min; y <= e.data.me.options.years.max; y++) csv += ','+y+(e.data.me.parameters[e.data.me.options.parameter] && e.data.me.parameters[e.data.me.options.parameter].units ? ' ('+e.data.me.parameters[e.data.me.options.parameter].units+')' : ''); + csv += '\n'; + for(i = 0; i < rs.length; i++){ + r = rs[i]; + p = getGeoJSONPropertiesByKeyValue(e.data.me.layers[e.data.me.views[v].layers[layerid].id].geojson,ky,r); + csv += r; + csv += ','; + csv += (typeof nm==="string" && p[nm] ? (p[nm].match(',') ? '"'+p[nm]+'"' : p[nm]) : "?"); + for(y = e.data.me.options.years.min; y <= e.data.me.options.years.max; y++){ + csv += ','; + if(typeof values[r][y]==="number") csv += (typeof e.data.me.parameters[e.data.me.options.parameter].dp==="number" ? values[r][y].toFixed(e.data.me.parameters[e.data.me.options.parameter].dp) : values[r][y]); + } + csv += '\n' + } + saveToFile(csv,filename,'text/plain'); + }); + } + function saveToFile(txt,fileNameToSaveAs,mime){ + // Bail out if there is no Blob function + if(typeof Blob!=="function") return this; + + var textFileAsBlob = new Blob([txt], {type:(mime||'text/plain')}); + + function destroyClickedElement(event){ document.body.removeChild(event.target); } + + var dl = document.createElement("a"); + dl.download = fileNameToSaveAs; + dl.innerHTML = "Download File"; + + if(window.webkitURL != null){ + // Chrome allows the link to be clicked without actually adding it to the DOM. + dl.href = window.webkitURL.createObjectURL(textFileAsBlob); + }else{ + // Firefox requires the link to be added to the DOM before it can be clicked. + dl.href = window.URL.createObjectURL(textFileAsBlob); + dl.onclick = destroyClickedElement; + dl.style.display = "none"; + document.body.appendChild(dl); + } + dl.click(); + } + function getGeoJSONPropertiesByKeyValue(geojson,key,value){ + if(!geojson.features || typeof geojson.features!=="object"){ + fes.log('WARNING','Invalid GeoJSON',geojson); + return {}; + } + for(var i = 0; i < geojson.features.length; i++){ + if(geojson.features[i].properties[key] == value) return geojson.features[i].properties; + } + return {}; + }; + function getGeoJSONPropertyValue(l,value){ + if(!fes.layers[l].key){ + fes.log('WARNING','No key set for layer '+l); + return ""; + } + if(fes.layers[l] && fes.layers[l].geojson){ + key = (fes.layers[l].name||fes.layers[l].key); + for(var i = 0; i < fes.layers[l].geojson.features.length; i++){ + if(fes.layers[l].geojson.features[i].properties[fes.layers[l].key] == value) return fes.layers[l].geojson.features[i].properties[key]; + } + return ""; + }else return ""; + }; + }); diff --git a/2023-DFES/resources/dom-to-image.min.js b/2023-DFES/resources/dom-to-image.min.js new file mode 100644 index 0000000..bc73227 --- /dev/null +++ b/2023-DFES/resources/dom-to-image.min.js @@ -0,0 +1,2 @@ +/*! dom-to-image 10-06-2017 */ +!function(a){"use strict";function b(a,b){function c(a){return b.bgcolor&&(a.style.backgroundColor=b.bgcolor),b.width&&(a.style.width=b.width+"px"),b.height&&(a.style.height=b.height+"px"),b.style&&Object.keys(b.style).forEach(function(c){a.style[c]=b.style[c]}),a}return b=b||{},g(b),Promise.resolve(a).then(function(a){return i(a,b.filter,!0)}).then(j).then(k).then(c).then(function(c){return l(c,b.width||q.width(a),b.height||q.height(a))})}function c(a,b){return h(a,b||{}).then(function(b){return b.getContext("2d").getImageData(0,0,q.width(a),q.height(a)).data})}function d(a,b){return h(a,b||{}).then(function(a){return a.toDataURL()})}function e(a,b){return b=b||{},h(a,b).then(function(a){return a.toDataURL("image/jpeg",b.quality||1)})}function f(a,b){return h(a,b||{}).then(q.canvasToBlob)}function g(a){"undefined"==typeof a.imagePlaceholder?v.impl.options.imagePlaceholder=u.imagePlaceholder:v.impl.options.imagePlaceholder=a.imagePlaceholder,"undefined"==typeof a.cacheBust?v.impl.options.cacheBust=u.cacheBust:v.impl.options.cacheBust=a.cacheBust}function h(a,c){function d(a){var b=document.createElement("canvas");if(b.width=c.width||q.width(a),b.height=c.height||q.height(a),c.bgcolor){var d=b.getContext("2d");d.fillStyle=c.bgcolor,d.fillRect(0,0,b.width,b.height)}return b}return b(a,c).then(q.makeImage).then(q.delay(100)).then(function(b){var c=d(a);return c.getContext("2d").drawImage(b,0,0),c})}function i(a,b,c){function d(a){return a instanceof HTMLCanvasElement?q.makeImage(a.toDataURL()):a.cloneNode(!1)}function e(a,b,c){function d(a,b,c){var d=Promise.resolve();return b.forEach(function(b){d=d.then(function(){return i(b,c)}).then(function(b){b&&a.appendChild(b)})}),d}var e=a.childNodes;return 0===e.length?Promise.resolve(b):d(b,q.asArray(e),c).then(function(){return b})}function f(a,b){function c(){function c(a,b){function c(a,b){q.asArray(a).forEach(function(c){b.setProperty(c,a.getPropertyValue(c),a.getPropertyPriority(c))})}a.cssText?b.cssText=a.cssText:c(a,b)}c(window.getComputedStyle(a),b.style)}function d(){function c(c){function d(a,b,c){function d(a){var b=a.getPropertyValue("content");return a.cssText+" content: "+b+";"}function e(a){function b(b){return b+": "+a.getPropertyValue(b)+(a.getPropertyPriority(b)?" !important":"")}return q.asArray(a).map(b).join("; ")+";"}var f="."+a+":"+b,g=c.cssText?d(c):e(c);return document.createTextNode(f+"{"+g+"}")}var e=window.getComputedStyle(a,c),f=e.getPropertyValue("content");if(""!==f&&"none"!==f){var g=q.uid();b.className=b.className+" "+g;var h=document.createElement("style");h.appendChild(d(g,c,e)),b.appendChild(h)}}[":before",":after"].forEach(function(a){c(a)})}function e(){a instanceof HTMLTextAreaElement&&(b.innerHTML=a.value),a instanceof HTMLInputElement&&b.setAttribute("value",a.value)}function f(){b instanceof SVGElement&&(b.setAttribute("xmlns","http://www.w3.org/2000/svg"),b instanceof SVGRectElement&&["width","height"].forEach(function(a){var c=b.getAttribute(a);c&&b.style.setProperty(a,c)}))}return b instanceof Element?Promise.resolve().then(c).then(d).then(e).then(f).then(function(){return b}):b}return c||!b||b(a)?Promise.resolve(a).then(d).then(function(c){return e(a,c,b)}).then(function(b){return f(a,b)}):Promise.resolve()}function j(a){return s.resolveAll().then(function(b){var c=document.createElement("style");return a.appendChild(c),c.appendChild(document.createTextNode(b)),a})}function k(a){return t.inlineAll(a).then(function(){return a})}function l(a,b,c){return Promise.resolve(a).then(function(a){return a.setAttribute("xmlns","http://www.w3.org/1999/xhtml"),(new XMLSerializer).serializeToString(a)}).then(q.escapeXhtml).then(function(a){return''+a+""}).then(function(a){return''+a+""}).then(function(a){return"data:image/svg+xml;charset=utf-8,"+a})}function m(){function a(){var a="application/font-woff",b="image/jpeg";return{woff:a,woff2:a,ttf:"application/font-truetype",eot:"application/vnd.ms-fontobject",png:"image/png",jpg:b,jpeg:b,gif:"image/gif",tiff:"image/tiff",svg:"image/svg+xml"}}function b(a){var b=/\.([^\.\/]*?)$/g.exec(a);return b?b[1]:""}function c(c){var d=b(c).toLowerCase();return a()[d]||""}function d(a){return a.search(/^(data:)/)!==-1}function e(a){return new Promise(function(b){for(var c=window.atob(a.toDataURL().split(",")[1]),d=c.length,e=new Uint8Array(d),f=0;f *:last-child { margin-bottom: 0; } +.info-bubble.warning { color: rgb(95, 82, 7); background-color: rgb(251, 245, 208); text-align: left; filter: drop-shadow(0px 0px 1px rgb(95, 82, 7)); } +.info-bubble.warning:after { border-bottom-color: rgb(251, 245, 208); } + .placesearch .submit { background-image: url(search.svg); }