<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>PGServer Info</title> <style> .medium {width: 300px; height: auto;} #mapcontainer { display: flex; flex-wrap: wrap; } #map {display: inline-block; width: 500px; height: 500px; border: 1 px solid lightblue;} #legendcontainer {display: flex;} #legend {min-width: 250px;} #featurecontainer { display: flex; flex-direction: column; justify-content: space-between; } #featureinfo { display: inline-block; max-width: 400px; } #graphs { display: flex; flex-wrap: wrap; } #graphs div { margin: 5px; } #colorschemes { display: flex; align-content: center; } #colorschemes div { padding: 2px; margin: 2px; cursor: pointer; } #colorschemes div:hover { background-color: lightgray; } #colorschemes div.selected { background-color: darkgray; } #colorschemes svg { display: block; } #colorschemes rect { stroke: #333; stroke-width: 0.5; } </style> <script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script> <script src='https://api.tiles.mapbox.com/mapbox-gl-js/v1.2.0/mapbox-gl.js'></script> <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v1.2.0/mapbox-gl.css' rel='stylesheet' /> <link href='loader.css' rel='stylesheet' /> <script src="./colorbrewer.js"></script> <script src="./ticks.js"></script> <script> "use strict"; let map = null; let globalStats = null; let selectedColorScheme = 0; function init() { const urlParams = new URLSearchParams(window.location.search); const fullTableName = urlParams.get('table'); const attrName = urlParams.get("column"); const attrType = urlParams.get("columntype"); const geomType = urlParams.get('geomtype'); const geomColumn = urlParams.get('geom_column'); document.querySelector('#tablename').innerHTML = `Layer: ${fullTableName}<br>Geometry: ${geomColumn}`; document.querySelector('#columnname').innerHTML = `Attribute: ${attrName} (${attrType})`; document.querySelector('#back').innerHTML = `<a href="tableinfo.html?table=${fullTableName}&geom_column=${geomColumn}">Back to layer info</a>`; document.querySelectorAll('[name="colorscheme"]').forEach(radio=>radio.onchange=updateLegendColorSchemes); document.querySelectorAll('[name="classtype"]').forEach(radio=>radio.onchange=applyLegendToMap); document.querySelector('#classcount').onchange = updateLegendColorSchemes; document.querySelector('input[name="colorsreversed"]').onchange = updateLegendColorSchemes initMap(); fetch(`data/${fullTableName}/colstats/${attrName}?geom_column=${geomColumn}`).then(response=>{ const loadingElement = document.querySelector('#loader'); loadingElement.innerHTML = ""; if (!response.ok) { try { response.json(json=>loadingElement.innerHtml = json.error); } catch(err) { loadingElement.innerHTML = err; } return; } response.json() .then(json=>{ if (json.datatype === 'timestamptz' || json.datatype === 'date') { json.percentiles = json.percentiles.map(percentile=>{ percentile.from = percentile.from?new Date(percentile.from):null; percentile.to = percentile.to?new Date(percentile.to):null; return percentile; }) json.values = json.values.map(value=>{ value.value = value.value?new Date(value.value):null; return value; }) } graphStats(json) globalStats = json; }) .catch(err=> loadingElement.innerHTML = `Failed to parse response, message: ${err.message}` ); }) } function initMap() { const urlParams = new URLSearchParams(window.location.search); const fullTableName = urlParams.get('table'); const geomType = urlParams.get('geomtype'); const geomColumn = urlParams.get('geom_column'); const attrName = urlParams.get("column"); const mapDefinition = { container: 'map', "style": { "version": 8, "name": "DefaultBaseStyle", "id": "defaultbasestyle", "glyphs": `https://free.tilehosting.com/fonts/{fontstack}/{range}.pbf?key=`, "sources": { "osmgray": { "type":"raster", "tileSize":256, "tiles":[ "https://tiles.edugis.nl/mapproxy/osm/tiles/osmgrayscale_EPSG900913/{z}/{x}/{y}.png?origin=nw" ], "attribution":"© <a href=\"https://www.openstreetmap.org/about\" target=\"copyright\">OpenStreetMap contributors</a>", "maxzoom":19 } }, "layers": [ { "id": "osmgray", "type": "raster", "source": "osmgray" } ] } } const bboxll = urlParams.get('bboxll'); if (bboxll) { mapDefinition.bounds = JSON.parse(bboxll); } map = new mapboxgl.Map(mapDefinition); map.on('load', () => { let layerType, paint; switch (geomType) { case 'MULTIPOLYGON': case 'POLYGON': layerType = 'fill'; paint = { "fill-color": "red", "fill-outline-color": "white", "fill-opacity": 0.8 } break; case 'MULTILINESTRING': case 'LINESTRING': layerType = 'line'; paint = { "line-color": "red", "line-width": 1 } break; case 'MULTIPOINT': case 'POINT': layerType = "circle"; paint = { "circle-radius": 5, "circle-color": "red", "circle-stroke-color": "white", "circle-stroke-width" : 1 } break; default: break; } if (!layerType) { document.querySelector("#layerjson").innerHTML = `Field geom of type: '${geomType}' not supported<br>Supported types: (MULTI-) POINT/LINE/POLYGON<p>` } else { const baseUrl = new URL(`/data`, window.location.href).href; const layer = { "id": "attrlayer", "type": layerType, "source": { "type": "vector", "tiles": [`${baseUrl}/${fullTableName}/mvt/{z}/{x}/{y}?geom_column=${geomColumn}&columns=${attrName}&include_nulls=0`], }, "source-layer": fullTableName, "paint": paint, //"filter": ['has', attrName] } map.addLayer(layer); addLegendLine('red', fullTableName, layerType); updateLayerJsonDisplay(); } }) map.on('mousemove', function (e) { var features = map.queryRenderedFeatures(e.point).map(function(feature){ return {layer: {id: feature.layer.id, type: feature.layer.type}, properties:(feature.properties)};}); document.querySelector('#featureinfo').innerHTML = JSON.stringify(features.map(feature=>feature.properties), null, 2); }); } function updateLayerJsonDisplay() { const layerjson = map.getStyle().layers.filter(l=>l.id==='attrlayer')[0]; layerjson.source = map.getSource(layerjson.source).serialize(); document.querySelector("#layerjson").innerHTML = `<pre>${JSON.stringify(layerjson, null, 2)}</pre>`; } function prepareGraphColors(classType) { globalStats.graphColors = []; globalStats.classType = classType; } function addGraphColors(color, upperValue) { globalStats.graphColors.push({color: color, upperValue: upperValue}); } let graphNoData = null; let graphValues = null; let graphMostFrequent = null; function destroyGraphs() { if (graphNoData) { graphNoData.destroy(); } if (graphValues) { graphValues.destroy(); } if (graphMostFrequent) { graphMostFrequent.destroy(); } } function graphStats(stats) { destroyGraphs(); const nullValues = stats.values.filter(value=>value.value === null).reduce((result, value)=>result+value.count,0); const rowCount = stats.percentiles.reduce((result, percentile)=>result + percentile.count, 0); graphNoData = new Chart(document.querySelector('#graphnodata canvas'), { type: 'doughnut', data: { labels: ['no data','data',], datasets: [{ backgroundColor: ['lightgray', 'red'], borderColor: 'white', borderWidth: 0, data: [nullValues, rowCount] }] } }); if (stats.percentiles.length && typeof(stats.percentiles[0].from) !== 'string') { graphValues = new Chart(document.querySelector('#graphvalues canvas'), { type: 'line', data: { labels: stats.percentiles.map((percentile, index, arr)=>Math.round((index/(arr.length - 1)) * 100)), datasets: [{ backgroundColor: stats.graphColors? stats.percentiles.map((percentile)=>{ let color = stats.classType === 'mostfrequent' ? stats.graphColors.find(graphColor=>percentile.to == graphColor.upperValue) : stats.graphColors.find(graphColor=>percentile.to < graphColor.upperValue); if (!color) { color = stats.graphColors[stats.graphColors.length - 1]; } return color.color; }):'red', //borderColor: 'lighgray', data: stats.percentiles.map((percentile,index,arr)=>(index===arr.length-1)?percentile.to:percentile.from), fill: false }] }, options : { title: { display: false, text : 'some title' }, legend: { display: false }, scales: { yAxes: [ { scaleLabel: { display: true, labelString: stats.column } } ], xAxes: [ { scaleLabel: { display: true, labelString: 'percentage of rows', padding: 0 } } ] } } }) } const valuesSummary = stats.values.filter(value=>value.value !== null).slice(0,10); const valuesSummeryRowCount = valuesSummary.reduce((result, value)=>result+value.count,0); if (rowCount > valuesSummeryRowCount) { valuesSummary.push({ value:"other", count: rowCount - valuesSummeryRowCount }) } graphMostFrequent = new Chart(document.querySelector('#graphmostfrequent canvas'), { type: "horizontalBar", data: { labels: valuesSummary.map(value=>value.value), datasets: [ { backgroundColor: stats.graphColors? valuesSummary.map((value)=>{ let color = stats.graphColors.find(graphColor=>value.value === graphColor.upperValue) if (!color) { color = stats.graphColors[stats.graphColors.length - 1]; } return color.color; }):'red', data: valuesSummary.map(value=>value.count) } ] }, options : { legend: { display: false, }, scales: { xAxes: [ { ticks: { min: 0 } } ] } } }) } function addLegendLine(color, label, type) { if (!type) { type = 'fill'; } const legend = document.querySelector('#legend'); let svg; switch (type) { case 'fill': svg = `<svg width="30" height="15"> <rect width="30" height="15" style="fill:${color};fill-opacity:1;stroke-width:1;stroke:#444"></rect> </svg>` break; case 'line': svg = `<svg width="30" height="15"> <line x1="0" y1="15" x2="30" y2="0" style="stroke:${color};stroke-width:${color.width};" /> </svg>` break; case 'circle': svg = `<svg width="12" height="12"> <circle cx="6" cy="6" r="5" style="fill:${color};fill-opacity:1 stroke-width:1;stroke:white" /> </svg>` } const legendLine = document.createElement('div'); legendLine.innerHTML = `<div> <span>${svg}</span> <span>${label}<span></div>` legend.appendChild(legendLine); } function prepareLegend() { if (globalStats) { document.querySelector('#legend').innerHTML = ''; return true; } let messageElem = document.querySelector('#legend.message'); if (messageElem) { return false; } messageElem = document.createElement('div'); messageElem.classList.add('message'); messageElem.innerHTML = "waiting for stats, retry later..."; return false; } // schemeTypes 'div', 'qual', 'seq' // for diverging, qualitative and sequential legends function getColorSchemes(numClasses, schemeType, reversed) { for (; numClasses > 2; numClasses--) { let result = colorbrewer.filter(scheme=>scheme.type===schemeType && scheme.sets.length > numClasses - 3) .map(scheme=>{ let result = Object.assign({}, scheme.sets[numClasses - 3]); result.name = scheme.name; result.type = scheme.type; return result; }); if (result.length) { if (reversed) { result.forEach(scheme=>scheme.colors = scheme.colors.map(c=>c).reverse()) } return result; } } if (numClasses === 2) { let result = colorbrewer.filter(scheme=>scheme.type===schemeType) .map(scheme=>{ const result = {colors: [scheme.sets[0].colors[0],scheme.sets[0].colors[2]]} result.name = scheme.name; result.type = scheme.type; return result; }); if (reversed) { result.forEach(scheme=>scheme.colors = scheme.colors.map(c=>c).reverse()); } return result; } return [{colors:['#ff0000']}]; } function selectColorScheme(schemeIndex) { let schemeDiv = document.querySelector('#colorschemes'); schemeDiv.querySelectorAll('div').forEach(div=>div.classList.remove('selected')); schemeDiv.querySelectorAll('div').forEach((div,index)=>{if (index === schemeIndex) {div.classList.add('selected')}}); selectedColorScheme = schemeIndex; applyLegendToMap(); } // display the available colorschemes for current scheme type and class number function updateLegendColorSchemes() { let schemeDiv = document.querySelector('#colorschemes'); schemeDiv.innerHTML = ''; let classCount = Number(document.querySelector('#classcount').value); let schemeType = document.querySelector('input[name="colorscheme"]:checked').value; let reversed = document.querySelector('input[name="colorsreversed"]').checked; let schemes = getColorSchemes(classCount, schemeType, reversed); classCount = schemes[0].colors.length; if (selectedColorScheme > schemes.length - 1) { selectedColorScheme = schemes.length - 1; } schemes.forEach((scheme, schemeIndex) => { let ramp = document.createElement('div'); if (schemeIndex === selectedColorScheme) { ramp.classList.add('selected'); selectedColorScheme = schemeIndex; } let svg = `<svg width="15" height="${classCount * 15}">${ scheme.colors.map((color, index)=>`<rect fill="${color}" width="15" height="15" y="${index * 15}"></rect>`).join('\n') }</svg>`; ramp.innerHTML = svg; ramp.onclick = ()=>selectColorScheme(schemeIndex); schemeDiv.appendChild(ramp); }) applyLegendToMap(); } function applyLegendToMap() { let classType = document.querySelector('input[name="classtype"]:checked').value; let reversed = document.querySelector('input[name="colorsreversed"]').checked; if (prepareLegend()) { prepareGraphColors(classType); let classCount = Number(document.querySelector('#classcount').value); if (classCount === 1) { // special case, single classification } else { // classCount > 1 let layerType = map.getLayer('attrlayer').type; let rowCount = globalStats.percentiles.reduce((result, percentile)=>result + percentile.count, 0); let mapboxPaint; let schemeType = document.querySelector('input[name="colorscheme"]:checked').value; switch(classType) { case 'mostfrequent': let schemes = getColorSchemes(classCount, schemeType, reversed); classCount = schemes[0].colors.length; let classValues = globalStats.values.filter(value=>value.value !== null); let needsSlice = classValues.length > classCount; if (needsSlice) { classValues = classValues.slice(0, classCount - 1); let classValuesRowCount = classValues.reduce((result, value)=>result+value.count,0); classValues.push({ value:"other", count: rowCount - classValuesRowCount }) } if (globalStats.datatype === 'numeric') { mapboxPaint = [ "match", ["to-number", ["get",`${globalStats.column}`], 0] ]; } else { mapboxPaint = [ "match", ["get",`${globalStats.column}`] ]; } classValues.forEach((value, index) => { addLegendLine(schemes[selectedColorScheme].colors[index], value.value, layerType); addGraphColors(schemes[selectedColorScheme].colors[index], value.value) if (index < classValues.length - 1 || !needsSlice) { mapboxPaint.push(value.value); mapboxPaint.push(schemes[selectedColorScheme].colors[index]); } }); mapboxPaint.push(schemes[selectedColorScheme].colors[classValues.length -1]); break; case 'quantile': let percentileBreaks = globalStats.percentiles.reduce((result, percentile)=>{ if (result.length === 0) { result.push(Object.assign({}, percentile)); // spread operator not supported by current Edge return result; } if (result[result.length - 1].from === percentile.from) { result[result.length - 1].to = percentile.to; result[result.length - 1].count += percentile.count; return result; } result.push(Object.assign({}, percentile)); return result; },[]); let seqSchemes = getColorSchemes(classCount, schemeType, reversed); classCount = seqSchemes[selectedColorScheme].colors.length; if (classCount > percentileBreaks.length) { classCount = percentileBreaks.length } else { let totalRowCount = percentileBreaks.reduce((result, percentile)=>result+percentile.count, 0); let rowCountPerClass = totalRowCount / classCount; let sumRowCount = 0; let sumClassCount = 0 percentileBreaks = percentileBreaks.reduce((result, percentile)=>{ sumRowCount += percentile.count; if (sumRowCount > sumClassCount * rowCountPerClass && result.length < classCount) { // new class result.push(percentile); sumClassCount++; } else { result[result.length - 1].to = percentile.to; result[result.length - 1].count += percentile.count; } return result; },[]) } mapboxPaint = ["case"] percentileBreaks.forEach((brk, index, arr)=>{ addLegendLine(seqSchemes[selectedColorScheme].colors[index], `${brk.from} - ${brk.to}`, layerType); addGraphColors(seqSchemes[selectedColorScheme].colors[index], brk.to); if (globalStats.datatype === 'numeric') { mapboxPaint.push(["<",["to-number", ["get", `${globalStats.column}`], 0],brk.to],seqSchemes[selectedColorScheme].colors[index]); } else { mapboxPaint.push(["<",["get", `${globalStats.column}`],brk.to],seqSchemes[selectedColorScheme].colors[index]); } }) mapboxPaint.push(seqSchemes[selectedColorScheme].colors[classCount - 1]) break; case 'interval': let min = globalStats.percentiles[0].from; let max = globalStats.percentiles[globalStats.percentiles.length - 1].to; if (typeof min === "number") { let intervalSchemes = getColorSchemes(classCount, schemeType, reversed); classCount = intervalSchemes[0].colors.length; let classTicks = getIntervalClassTicks(min, max, classCount); mapboxPaint = ["case"]; classTicks.classes.forEach((classStart, index)=>{ let legendFrom = classStart; let legendTo = (index === classCount - 1 ? classTicks.max : classTicks.classes[index + 1]); if (globalStats.datatype === 'int4' || globalStats.datatype === 'int8') { legendFrom = Math.floor(legendFrom); legendTo = Math.floor(legendTo); } addLegendLine(intervalSchemes[selectedColorScheme].colors[index], `${(legendFrom)} - ${legendTo}`, layerType); addGraphColors(intervalSchemes[selectedColorScheme].colors[index], legendTo) if (globalStats.datatype === 'numeric') { // convert javscript string to number mapboxPaint.push(["<", ["to-number",["get", globalStats.column], 0],legendTo], intervalSchemes[selectedColorScheme].colors[index]); } else { mapboxPaint.push(["<", ["get", globalStats.column],legendTo], intervalSchemes[selectedColorScheme].colors[index]); } }) mapboxPaint.push(intervalSchemes[selectedColorScheme].colors[classCount - 1]) } break; } if (mapboxPaint) { let colorprop = `${layerType}-color`; map.setPaintProperty('attrlayer', colorprop, mapboxPaint); updateLayerJsonDisplay(); graphStats(globalStats); } } let nullValues = globalStats.values.filter(value=>value.value === null).reduce((result, value)=>result+value.count,0); let checkButtonNullValues = document.querySelector('#hidenulls') if (nullValues) { checkButtonNullValues.removeAttribute('disabled'); } else { checkButtonNullValues.setAttribute('disabled', '') } } } </script> </head> <body onload="init()"> <h1>Attribute information</h1> <h2 id="tablename"></h2> <h2 id="columnname"></h2> <div id="loader"> <h2>Loading statistics...</h2> <div class="loader"> <div class="dot"></div><div class="dot"></div><div class="dot"></div><div class="dot"></div> <div class="dot"></div><div class="dot"></div><div class="dot"></div><div class="dot"></div> </div> </div> <div id="graphs"> <div id="graphnodata" class="canvascontainer medium"><canvas></canvas></div> <div id="graphvalues" class="canvascontainer medium"><canvas></canvas></div> <div id="graphmostfrequent" class="canvascontainer medium"><canvas></canvas></div> </div> <div id="mapcontainer"> <div id="map"></div> <div id="featurecontainer"> <div id="legendcontainer"> <div id="legend"></div> <div id="legendformcontainer"> <select id="classcount" name="classcount"> <option value="1" selected>1</option> <option value="2">2</option> <option value="3">3</option> <option value="4">4</option> <option value="5">5</option> <option value="6">6</option> <option value="7">7</option> <option value="8">8</option> <option value="9">9</option> <option value="10">10</option> <option value="11">11</option> <option value="12">12</option> </select><label for="numclasses">number of classes</label><br> Classification methods:<br> <input type="radio" name="classtype" id="interval" value="interval"><label for="interval">equal interval</label> <input type="radio" name="classtype" id="quantile" value="quantile" checked><label for="quantile">quantile</label> <input type="radio" name="classtype" id="mostfrequent" value="mostfrequent"><label for="mostfrequent">most frequent</label><br> Color schemes:<br> <input type="radio" name="colorscheme" id="sequential" value="seq"><label for="sequential">sequential</label> <input type="radio" name="colorscheme" id="diverting" value="div"><label for="diverting">diverting</label> <input type="radio" name="colorscheme" id="qualitative" value="qual" checked><label for="qualitative">qualitative</label><br> <input type="checkbox" name="colorsreversed" id="colorsreversed"><label for="colorsreversed">reverse color order</label><br> <input type="checkbox" id="hidenulls" name="hidenulls" checked><label for="hidenulls">Hide no-data</label><br> <div id="colorschemes"></div> </div> </div> <div id="featureinfo"></div> </div> </div> <div id="layerjson"></div> <div id="back"></div> </body> </html>