Commit 580141b7 authored by Anne Blankert's avatar Anne Blankert

fix special cases, refactor attribute info UI

parent 6f1a7ae7
...@@ -2,13 +2,57 @@ const sqlTableName = require('./utils/sqltablename.js'); ...@@ -2,13 +2,57 @@ const sqlTableName = require('./utils/sqltablename.js');
const sql = (params, query) => { const sql = (params, query) => {
return ` return `
select count(1)::integer as "count", ${params.column} as "value" select count(1)::integer as "count", "${params.column}" as "value"
from ${params.table} from ${sqlTableName(params.table)}
where ${query.geom_column} is not null where ${query.geom_column} is not null
group by ${params.column} order by count(1) desc limit 2000; group by "${params.column}" order by count(1) desc limit 100;
` `
} // TODO, use sql place holders $1, $2 etc. instead of inserting user-parameters into query } // TODO, use sql place holders $1, $2 etc. instead of inserting user-parameters into query
// https://leafo.net/guides/postgresql-calculating-percentile.html
const sqlPercentiles = (params, query) => {
return `
select min(buckets.value) "from", max(buckets.value) "to", count(ntile)::integer "count", ntile as percentile
from
(select "${params.column}" as value, ntile(100) over (order by "${params.column}")
from ${sqlTableName(params.table)}
where "${params.column}" is not null and ${query.geom_column} is not null)
as buckets
group by ntile order by ntile;
`
}
const sqlPercentilesBoolean = (params, query) => {
return `
select case when min(buckets.value) = 0 then false else true end "from", case when max(buckets.value) = 0 then false else true end "to", count(ntile)::integer "count", ntile as percentile
from
(select "${params.column}"::integer as value, ntile(100) over (order by "${params.column}")
from ${sqlTableName(params.table)}
where "${params.column}" is not null and ${query.geom_column} is not null)
as buckets
group by ntile order by ntile;
`
}
let typeMap = null;
async function getTypeName(id, pool) {
if (!typeMap) {
const sql = "select oid,typname from pg_type where oid < 1000000 order by oid";
try {
const queryResult = await pool.query(sql);
typeMap = new Map(queryResult.rows.map(row=>[row.oid, row.typname]));
} catch(err) {
console.log(`error loading types: ${err}`);
return id.toString();
}
}
const result = typeMap.get(id);
if (!result) {
return id.toString();
}
return result;
}
module.exports = function(app, pool, cache) { module.exports = function(app, pool, cache) {
let cacheMiddleWare = async(req, res, next) => { let cacheMiddleWare = async(req, res, next) => {
...@@ -16,7 +60,7 @@ module.exports = function(app, pool, cache) { ...@@ -16,7 +60,7 @@ module.exports = function(app, pool, cache) {
next(); next();
return; return;
} }
const cacheDir = `${req.params.table}/attrstats/`; const cacheDir = `${req.params.table}/colstats/`;
const key = ((req.query.geom_column?req.query.geom_column:'geom') + (req.params.column?','+req.params.column:'')) const key = ((req.query.geom_column?req.query.geom_column:'geom') + (req.params.column?','+req.params.column:''))
.replace(/[\W]+/g, '_'); .replace(/[\W]+/g, '_');
...@@ -108,22 +152,49 @@ module.exports = function(app, pool, cache) { ...@@ -108,22 +152,49 @@ module.exports = function(app, pool, cache) {
if (!req.query.geom_column) { if (!req.query.geom_column) {
req.query.geom_column = 'geom'; // default req.query.geom_column = 'geom'; // default
} }
const sqlString = sql(req.params, req.query); let sqlString = sql(req.params, req.query);
//console.log(sqlString); //console.log(sqlString);
try { try {
const result = await pool.query(sqlString); let queryResult = await pool.query(sqlString);
const stats = result.rows let datatype = await getTypeName(queryResult.fields[1].dataTypeID, pool);
if (stats.length === 0) { if (datatype === "numeric" || datatype === "int8") {
res.status(204).json({}); // numeric datatype, try to convert to Number
return; try {
queryResult.rows = queryResult.rows.map(row=>{row.value=row.value?Number(row.value):row.value; return row});
} catch(err) {
// failed Numeric conversion
}
} }
res.json({ const stats = queryResult.rows
const result = {
table: req.params.table, table: req.params.table,
column: req.params.column, column: req.params.column,
datatype: datatype,
numvalues: stats.length < 2000?stats.length:null, numvalues: stats.length < 2000?stats.length:null,
uniquevalues: stats[0].value !== null?stats[0].count === 1:stats.length>1?stats[1].count === 1:false, uniquevalues: stats.length?stats[0].value !== null?stats[0].count === 1:stats.length>1?stats[1].count === 1:false:[],
values: stats values: stats
}) }
if (stats.length === 0) {
result.percentiles = [];
res.json(result);
return;
}
if (datatype === "bool") {
sqlString = sqlPercentilesBoolean(req.params, req.query);
} else {
sqlString = sqlPercentiles(req.params, req.query);
}
queryResult = await pool.query(sqlString);
if (datatype === "numeric" || datatype === "int8") {
// numeric datatype, try to convert to Number
try {
queryResult.rows = queryResult.rows.map(row=>{row.from=Number(row.from); row.to=Number(row.to); return row});
} catch(err) {
// failed Numeric conversion
}
}
result.percentiles = queryResult.rows;
res.json(result);
} catch(err) { } catch(err) {
console.log(err); console.log(err);
let status = 500; let status = 500;
......
...@@ -6,73 +6,80 @@ ...@@ -6,73 +6,80 @@
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Info</title> <title>Info</title>
<style> <style>
.canvascontainer {width: 500px; height: auto; border: 1px solid lightblue}
.medium {width: 300px; height: auto;}
#map {display: inline-block; width: 500px; height: 500px; border: 1 px solid lightblue;} #map {display: inline-block; width: 500px; height: 500px; border: 1 px solid lightblue;}
#info {display: inline-block;} #info {display: inline-block;}
#attrinfo {
display: flex;
flex-wrap: wrap;
}
#attrinfo div {
margin: 5px;
}
</style> </style>
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script> <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> <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='https://api.tiles.mapbox.com/mapbox-gl-js/v1.2.0/mapbox-gl.css' rel='stylesheet' />
<link href='loader.css' rel='stylesheet' />
<script> <script>
"use strict"; "use strict";
let chart1 = null, chart2 = null; let map = null;
function quantile(arr, p) { function init() {
let len = arr.length, id; const urlParams = new URLSearchParams(window.location.search);
if ( p === 0.0 ) { const fullTableName = urlParams.get('table');
return arr[ 0 ]; const attrName = urlParams.get("column");
} const attrType = urlParams.get("columntype");
if ( p === 1.0 ) { const geomType = urlParams.get('geomtype');
return arr[ len-1 ]; const geomColumn = urlParams.get('geom_column');
} document.querySelector('#tablename').innerHTML = `${fullTableName} ${geomColumn}`;
id = ( len*p ) - 1; document.querySelector('#columnname').innerHTML = `${attrName} (${attrType})`;
document.querySelector('#back').innerHTML = `<a href="tableinfo.html?table=${fullTableName}&geom_column=${geomColumn}">Back to layer info</a>`;
// [2] Is the index an integer? initMap();
if ( id === Math.floor( id ) ) {
// Value is the average between the value at id and id+1: fetch(`data/${fullTableName}/colstats/${attrName}?geom_column=${geomColumn}`).then(response=>{
return ( arr[ id ] + arr[ id+1 ] ) / 2.0; const attrInfoElement = document.querySelector('#attrinfo');
} attrInfoElement.innerHTML = "";
// [3] Round up to the next index: if (!response.ok) {
id = Math.ceil( id ); try {
return arr[ id ]; response.json(json=>attrInfoElement.innerHtml = json.error);
} } catch(err) {
function niceNumbers (range, round) { attrInfoElement.innerHTML = err;
const exponent = Math.floor(Math.log10(range)); }
const fraction = range / Math.pow(10, exponent); return;
let niceFraction; }
if (round) { response.json()
if (fraction < 1.5) niceFraction = 1; .then(json=>{
else if (fraction < 3) niceFraction = 2; if (json.datatype === 'timestamptz' || json.datatype === 'date') {
else if (fraction < 7) niceFraction = 5; json.percentiles = json.percentiles.map(percentile=>{
else niceFraction = 10; percentile.from = percentile.from?new Date(percentile.from):null;
} else { percentile.to = percentile.to?new Date(percentile.to):null;
if (fraction <= 1.0) niceFraction = 1; return percentile;
else if (fraction <= 2) niceFraction = 2; })
else if (fraction <= 5) niceFraction = 5; json.values = json.values.map(value=>{
else niceFraction = 10; value.value = value.value?new Date(value.value):null;
} return value;
return niceFraction * Math.pow(10, exponent); })
}
function getLinearTicks (min, max, maxTicks) {
const range = niceNumbers(max - min, false);
let tickSpacing;
if (range === 0) {
tickSpacing = 1;
} else {
tickSpacing = niceNumbers(range / (maxTicks), true);
} }
return { graphStats(json)
min: Math.floor(min / tickSpacing) * tickSpacing, })
max: Math.ceil(max / tickSpacing) * tickSpacing, .catch(err=>
tickWidth: tickSpacing attrInfoElement.innerHTML = `Failed to parse response, message: ${err.message}`
}; );
})
} }
let map = null;
function initMap() 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 = { const mapDefinition = {
container: 'map', container: 'map',
"style": { "style": {
...@@ -81,7 +88,7 @@ ...@@ -81,7 +88,7 @@
"id": "defaultbasestyle", "id": "defaultbasestyle",
"glyphs": `https://free.tilehosting.com/fonts/{fontstack}/{range}.pbf?key=`, "glyphs": `https://free.tilehosting.com/fonts/{fontstack}/{range}.pbf?key=`,
"sources": { "sources": {
"osmgrijs": { "osmgray": {
"type":"raster", "type":"raster",
"tileSize":256, "tileSize":256,
"tiles":[ "tiles":[
...@@ -92,103 +99,31 @@ ...@@ -92,103 +99,31 @@
}, },
"layers": [ "layers": [
{ {
"id": "osmgrijs", "id": "osmgray",
"type": "raster", "type": "raster",
"source": "osmgrijs" "source": "osmgray"
} }
],
"filter": [
"has",
`${attrName}`
] ]
} }
} }
const urlParams = new URLSearchParams(window.location.search);
const bboxll = urlParams.get('bboxll'); const bboxll = urlParams.get('bboxll');
if (bboxll) { if (bboxll) {
mapDefinition.bounds = JSON.parse(bboxll); mapDefinition.bounds = JSON.parse(bboxll);
} }
map = new mapboxgl.Map(mapDefinition); map = new mapboxgl.Map(mapDefinition);
map.on('mousemove', function (e) { map.on('load', () => {
var features = map.queryRenderedFeatures(e.point).map(function(feature){ return {layer: {id: feature.layer.id, type: feature.layer.type}, properties:(feature.properties)};});
document.getElementById('info').innerHTML = JSON.stringify(features.map(feature=>feature.properties), null, 2);
});
}
const colors1 = ["#686868", "#e60000", "#000000", "#faaa00", "#734c00", "#b2b2b2", "#267300", "#beffdc", "#ffff73",
"#a3ff73", "#e9ffbe", "#b4d79e", "#7ed2fc", "#ffffff"];
const colors = ["#14a4ab", "#a9a9a9", "#4e4e4e", "#67ae00", '#febe00', "#e2fe5f", "#ada4fe", '#e8beff', '#d7d79e',
'#0afeb3', '#a83800', '#73dfff','#69d5b4', '#bee8ff', '#97dbf2', '#feee00', '#e9ffbe', '#97dbf2',
'#b1d600', '#73b2ff', '#97dbf2', '#fefac2', '#fe8419', '#7ab6f5', '#000000', '#00e6a9', '#b39d3d',
'#97fe00', '#97fe00', '#ffeabe', '#ae974b', '#97dbf2', '#bed2ff', '#73dfff' ,'#97dbf2', '#ffbebe',
'#ac7a9d']
const greenScheme = [{red:247, green:252, blue:253}, {red:0, green:68, blue:27}];
function hex (i) {
let result = i.toString(16);
if (result.length < 2) {
result = '0' + result;
}
return result;
}
function createRangeColors(classLabels) {
const length = classLabels.length;
const result = [];
if (length > 1) {
const redStep = (greenScheme[0].red - greenScheme[1].red) / (length - 1);
const greenStep = (greenScheme[0].green - greenScheme[1].green) / (length - 1);
const blueStep = (greenScheme[0].blue - greenScheme[1].blue) / (length - 1);
for (let i = 0; i < length; i++) {
result.push('#' + hex(greenScheme[0].red - Math.round(redStep * i)) + hex(greenScheme[0].green - Math.round(greenStep * i)) + hex(greenScheme[0].blue - Math.round(blueStep * i)));
result.push(classLabels[i]);
}
result.push('rgba(0,0,0,1)');
}
return result;
}
function showMapLayer(fullTableName, geomColumn, geomType, attrName, attrType, classLabels) {
let layerType, paint; let layerType, paint;
const classLabelAndColors = classLabels.reduce((result, key, index)=>{
result.push(key);
result.push(colors[index % colors.length])
return result;
}, []);
const rangeColors = createRangeColors(classLabels);
let styledColors;
if (classLabels.length < 2) {
styledColors = colors[0];
} else {
switch (attrType) {
case 'varchar':
styledColors = [
"match",
["get", attrName],
...classLabelAndColors,
colors[0]
];
break;
case 'numeric':
styledColors = [
"step",
["to-number", ["get", attrName]],
...rangeColors
]
break;
default:
styledColors = [
"step",
["get", attrName],
...rangeColors
]
}
}
switch (geomType) { switch (geomType) {
case 'MULTIPOLYGON': case 'MULTIPOLYGON':
case 'POLYGON': case 'POLYGON':
layerType = 'fill'; layerType = 'fill';
paint = { paint = {
"fill-color": styledColors, "fill-color": "red",
"fill-outline-color": "black", "fill-outline-color": "white",
"fill-opacity": 0.8 "fill-opacity": 0.8
} }
break; break;
...@@ -196,34 +131,33 @@ ...@@ -196,34 +131,33 @@
case 'LINESTRING': case 'LINESTRING':
layerType = 'line'; layerType = 'line';
paint = { paint = {
"line-color": styledColors, "line-color": "red",
"line-width": 1 "line-width": 1
} }
break; break;
case 'MULTIPOINT': case 'MULTIPOINT':
case 'POINT': case 'POINT':
layerType = 'circle'; layerType = "circle";
paint = { paint = {
"circle-radius": 5, "circle-radius": 5,
"circle-color": styledColors, "circle-color": "red",
"circle-stroke-color": "black", "circle-stroke-color": "white",
"circle-stroke-width" : 1 "circle-stroke-width" : 1
} }
break; break;
default: default:
document.querySelector("#layerjson").innerHTML = `Field geom of type: '${geomType}' not supported<br>Supported types: (MULTI-) POINT/LINE/POLYGON<p>` break;
} }
if (layerType){ 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 baseUrl = new URL(`/data`, window.location.href).href;
const url = `${baseUrl}/${fullTableName}/mvt/{z}/{x}/{y}?columns="${attrName}"${geomColumn?`&geom_column=${geomColumn}`:''}`
console.log(`url: ${url}`);
const layer = { const layer = {
"id": "attrlayer", "id": "attrlayer",
"type": layerType, "type": layerType,
"source": { "source": {
"type": "vector", "type": "vector",
"tiles": [url], "tiles": [`${baseUrl}/${fullTableName}/mvt/{z}/{x}/{y}?geom_column=${geomColumn}&columns=${attrName}`],
}, },
"source-layer": fullTableName, "source-layer": fullTableName,
"paint": paint, "paint": paint,
...@@ -232,206 +166,132 @@ ...@@ -232,206 +166,132 @@
map.addLayer(layer); map.addLayer(layer);
document.querySelector("#layerjson").innerHTML = `<pre>${JSON.stringify(layer, null, 2)}</pre>`; document.querySelector("#layerjson").innerHTML = `<pre>${JSON.stringify(layer, null, 2)}</pre>`;
} }
})
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.getElementById('info').innerHTML = JSON.stringify(features.map(feature=>feature.properties), null, 2);
});
} }
function init() { function addCanvas() {
initMap(); const container = document.createElement('div');
const urlParams = new URLSearchParams(window.location.search); container.classList.add('canvascontainer');
const fullTableName = urlParams.get('table'); const canvas = document.createElement('canvas');
const attrName = urlParams.get("column"); container.classList.add('medium');
const attrType = urlParams.get("columntype"); container.appendChild(canvas);
const geomType = urlParams.get('geomtype'); document.querySelector('#attrinfo').appendChild(container);
const geomColumn = urlParams.get('geom_column'); return canvas;
document.querySelector('#tablename').innerHTML = fullTableName;
document.querySelector('#columnname').innerHTML = `${attrName} (${attrType})`;
document.querySelector('#back').innerHTML = `<a href="tableinfo.html?table=${fullTableName}&geom_column=${geomColumn}">Terug naar layer informatie</a>`
const parts = fullTableName.split('.');
const tableName = (parts.length > 1) ? parts[1] : parts[0];
fetch(`data/query/${fullTableName}?columns=count("${attrName}"),count(distinct+"${attrName}")+as+distinct,min("${attrName}"),max("${attrName}")`).then(response=>{
if (response.ok) {
response.json().then(json=> {
const div = document.querySelector('#attrinfo');
div.innerHTML = `<b>count</b>:${json[0].count}<br>
<b>nr of unique values</b>:${json[0].distinct}<br>
<b>minimum</b>:${json[0].min}<br>
<b>maximum</b>:${json[0].max}<br>`;
})
} }
})
fetch(`data/query/${fullTableName}?columns="${attrName}"`).then(response=>{
if (response.ok) {
response.json().then(json=> {
let histogram, verdeling;
let histLabels = [], verdLabels = [], classBorders = [];
let arr;
switch(attrType) {
case "varchar":
arr = json
.map(item=>item[attrName])
.filter(value=>value !== null)
.sort()
histogram = new Map();
for (let i = 0; i < arr.length; i++) {
const value = arr[i];
const count = histogram.get(value);
histogram.set(value, count?count+1:1);
}
histLabels = Array.from(histogram.keys());
classBorders = histLabels;
break;
case "int4":
case "int8":
case "float8":
case "numeric":
verdeling = new Map();
histogram = new Map();
arr = json
.map(item=>{
let result = item[attrName];
if (typeof result === "string") {
result = Number(result);
if (isNaN(result)) {
result = undefined;
}
}
return result;
})
.filter(value=>value !== null)
.filter(value=>value !== undefined)
.sort((value1,value2)=>value1-value2)
function graphStats(stats) {
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);
new Chart(addCanvas(), {
type: 'doughnut',
data: {
labels: ['null','non-null',],
datasets: [{
backgroundColor: ['lightgray', 'red'],
borderColor: 'white',
borderWidth: 0,
data: [nullValues, rowCount]
}]
const sampleInterval = Math.ceil(arr.length / 100);
for (let i = 0; i * sampleInterval < arr.length; i++) {
verdeling.set(i, arr[i*sampleInterval]);
verdLabels.push(`${1 + i * sampleInterval}`);
}
const min = arr[0];
const max = arr[arr.length - 1];
const iqr = quantile(arr, 0.75) - quantile(arr, 0.25);
let bins;
if (max === min) {
bins = 1
} else {
bins = Math.round((max - min) / (2 * (iqr/Math.pow(arr.length, 1/3))));
}
if (bins > 100) {
bins = 100;
}
const linearTicks = getLinearTicks(min, max, bins);
const binwidth = linearTicks.tickWidth;
console.log(`count: ${arr.length}, min: ${min}, max: ${max}, iqr: ${iqr}, bins: ${bins}, binwidth: ${binwidth}
linearTicks.min: ${linearTicks.min}, linearTicks.max: ${linearTicks.max}, linearTicks.tickWidth: ${linearTicks.tickWidth}`)
bins = Math.ceil((linearTicks.max - linearTicks.min) / binwidth);
if (bins < 1) {
bins = 1;
}
for (let i = 0; i < bins; i++) {
histogram.set(i, 0);
const start = Math.round(10000 * (linearTicks.min + i * binwidth)) / 10000;
const end = Math.round(10000 * (linearTicks.min + (i+1) * binwidth)) / 10000;
if (linearTicks.max === linearTicks.min) {
histLabels.push(`${start}`)
classBorders.push(start);
} else {
histLabels.push(`${start} - ${end}`);
classBorders.push(end);
}
}
for (let i = 0; i < arr.length; i++) {
const value = arr[i];
const bin = Math.floor((value - linearTicks.min)/binwidth);
histogram.set(bin, histogram.get(bin) + 1);
} }
break; });
} if (stats.percentiles.length && typeof(stats.percentiles[0].from) !== 'string') {
if (histogram) { new Chart(addCanvas(), {
const values = Array.from(histogram.values()); type: 'line',
const canvas = document.querySelector('#histogram');
const ctx = canvas.getContext('2d');
if (chart1) {
chart1.destroy();
}
chart1 = new Chart(ctx, {
type: 'bar',
data: { data: {
labels: histLabels, labels: stats.percentiles.map((percentile, index, arr)=>Math.round((index/(arr.length - 1)) * 100)),
datasets: [{ datasets: [{
label: `count of ${tableName}`, backgroundColor: 'red',
data: values, borderColor: 'lightred',
backgroundColor: "blue", data: stats.percentiles.map((percentile,index,arr)=>(index===arr.length-1)?percentile.to:percentile.from),
borderColor: "white", fill: false
borderWidth: 1
}] }]
}, },
options: { options : {
title: { title: {
display: true, display: false,
position: 'bottom', text : 'some title'
padding: 0, },
text: attrName legend: {
display: false
}, },
scales: { scales: {
yAxes: [{ yAxes: [
ticks: { {
beginAtZero: true scaleLabel: {
display: true,
labelString: stats.column
} }
}]
} }
],
xAxes: [
{
scaleLabel: {
display: true,
labelString: 'percentage of rows',
padding: 0
} }
});
showMapLayer(fullTableName, geomColumn, geomType, attrName, attrType, classBorders);
} }
if (verdeling) { ]
const values = Array.from(verdeling.values());
const canvas = document.querySelector('#verdeling');
const ctx = canvas.getContext('2d');
if (chart2) {
chart2.destroy();
} }
chart2 = new Chart(ctx, { }
type: 'line', })
}
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
})
}
new Chart(addCanvas(), {
type: "horizontalBar",
data: { data: {
labels: verdLabels, labels: valuesSummary.map(value=>value.value),
datasets: [{ datasets: [
label: `${attrName}`, {
data: values, backgroundColor: "red",
backgroundColor: "blue", data: valuesSummary.map(value=>value.count)
borderColor: "white", }
borderWidth: 1 ]
}]
}, },
options: { options : {
title: { legend: {
display: true, display: false,
position: 'bottom',
padding: 0,
text: tableName
}, },
scales: { scales: {
yAxes: [{ xAxes: [
{
ticks: { ticks: {
beginAtZero: true min: 0
} }
}]
} }
]
} }
});
}
})
} }
}) })
} }
</script> </script>
</head> </head>
<body onload="init()"> <body onload="init()">
<h1>Attribuut-information</h1> <h1>Attribute information</h1>
<h2 id="tablename"></h2> <h2 id="tablename"></h2>
<h2 id="columnname"></h2> <h2 id="columnname"></h2>
<div id="attrinfo"></div>
<div class="canvascontainer"><canvas id="histogram" width=1000 height=500></canvas></div> <div id="attrinfo">
<div class="canvascontainer"><canvas id="verdeling" width=1000 height=500></canvas></div> <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="map"></div> <div id="map"></div>
<div id="info"></div> <div id="info"></div>
<div id="layerjson"></div> <div id="layerjson"></div>
......
/* source https://codepen.io/TheDutchCoder/pen/gholk */
/* The loader container */
.loader {
position: absolute;
top: 50%;
left: 50%;
width: 200px;
height: 200px;
margin-top: -100px;
margin-left: -100px;
perspective: 400px;
/* transform-type: preserve-3d; */
}
/* The dot */
.dot {
position: absolute;
top: 50%;
left: 50%;
z-index: 10;
width: 40px;
height: 40px;
margin-top: -20px;
margin-left: -80px;
/* transform-type: preserve-3d;*/
transform-origin: 80px 50%;
transform: rotateY(0);
background-color: #1e3f57;
animation: dot1 2000ms cubic-bezier(.56,.09,.89,.69) infinite;
}
.dot:nth-child(2) {
z-index: 9;
animation-delay: 150ms;
}
.dot:nth-child(3) {
z-index: 8;
animation-delay: 300ms;
}
.dot:nth-child(4) {
z-index: 7;
animation-delay: 450ms;
}
.dot:nth-child(5) {
z-index: 6;
animation-delay: 600ms;
}
.dot:nth-child(6) {
z-index: 5;
animation-delay: 750ms;
}
.dot:nth-child(7) {
z-index: 4;
animation-delay: 900ms;
}
.dot:nth-child(8) {
z-index: 3;
animation-delay: 1050ms;
}
@keyframes dot1 {
0% {
transform: rotateY(0) rotateZ(0) rotateX(0);
background-color: #1e3f57;
}
45% {
transform: rotateZ(180deg) rotateY(360deg) rotateX(90deg);
background-color: #6bb2cd;
animation-timing-function: cubic-bezier(.15,.62,.72,.98);
}
90%, 100% {
transform: rotateY(0) rotateZ(360deg) rotateX(180deg);
background-color: #1e3f57;
}
}
\ No newline at end of file
...@@ -46,13 +46,21 @@ ...@@ -46,13 +46,21 @@
` `
}) })
} else { } else {
bbox.innerHTML = `Error getting bbox, response: response: ${response.status} ${response.statusText?response.statusText:''} ${response.url}` response.json().then(json=>{
bbox.innerHTML = `Error getting bbox: ${json.error}`;
}).catch(err => {
bbox.innerHTML = `Error parsing bbox: ${err}`;
})
} }
}) })
}) })
} else { } else {
const li = document.createElement('li'); const li = document.createElement('li');
li.innerHTML = `Error getting list, response: ${response.status} ${response.statusText?response.statusText:''} ${response.url}` response.json().then(json=>{
li.innerHTML = `Error getting column info: ${json.error}`;
}).catch(err => {
li.innerHTML = `Error parsing column info: ${err}`;
})
list.appendChild(li); list.appendChild(li);
} }
}) })
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment