Commit bdb7e537 authored by Rodrigo Tapia-McClung's avatar Rodrigo Tapia-McClung

Updateable OD matrix amchart w/ gradient heatLegend

parent 3172bb29
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Crecimiento urbano en la regi&oacute;n metropolitana centro pa&iacute</title> <title>Crecimiento urbano en la regi&oacute;n metropolitana centro pa&iacute;s</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" /> <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<link rel="stylesheet" href="http://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css" /> <link rel="stylesheet" href="http://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css" /> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css" />
...@@ -76,8 +76,8 @@ ...@@ -76,8 +76,8 @@
<div class="loader"></div> <div class="loader"></div>
<div class="row h-50 border-bottom"> <div class="row h-50 border-bottom">
<div class="col-6 border-right"> <div class="col-6 border-right">
<div id="area-graph"> <div id="tablediv">
<table class="table table-dark table-striped" id="tblViajesDesde"> <table class="table table-dark table-striped" id="tblViajes">
<thead> <thead>
<tr> <tr>
<th scope="col">Origen</th> <th scope="col">Origen</th>
...@@ -95,7 +95,7 @@ ...@@ -95,7 +95,7 @@
</div> </div>
<div class="row h-50"> <div class="row h-50">
<div class="col-6 border-right"> <div class="col-6 border-right">
<div id="costa-graph"></div> <div id="amchartdiv"></div>
</div> </div>
<div class="col-6"> <div class="col-6">
<div id="df-graph"></div> <div id="df-graph"></div>
...@@ -163,9 +163,15 @@ ...@@ -163,9 +163,15 @@
<!-- load animation tweening lib requirement for CanvasFlowMapLayer --> <!-- load animation tweening lib requirement for CanvasFlowMapLayer -->
<script src="https://unpkg.com/@tweenjs/tween.js@18.5/dist/tween.umd.js"></script> <script src="https://unpkg.com/@tweenjs/tween.js@18.5/dist/tween.umd.js"></script>
<script src="../js/CanvasFlowmapLayer.js"></script> <script src="../js/CanvasFlowmapLayer.js"></script>
<!-- amcharts-->
<script src="https://www.amcharts.com/lib/4/core.js"></script>
<script src="https://www.amcharts.com/lib/4/charts.js"></script>
<script src="https://www.amcharts.com/lib/4/themes/animated.js"></script>
<script src="../js/centropais_functions.js"></script> <script src="../js/centropais_functions.js"></script>
<script src="../js/centropais_basemap.js"></script> <script src="../js/centropais_basemap.js"></script>
<!--<script src="../js/grijalva_charts.js"></script>--> <script src="../js/centropais_charts.js"></script>
</body> </body>
......
:root { :root {
--main-bg-color: #262626; --main-bg-color:#262626;
--main-text-color: #b2b2b2; --main-text-color:#b2b2b2;
--cell-bg-color:#333;
} }
*{ *{
...@@ -15,6 +16,10 @@ body { ...@@ -15,6 +16,10 @@ body {
color:#333; color:#333;
font-family:sans-serif; font-family:sans-serif;
background-color: var(--main-bg-color); background-color: var(--main-bg-color);
/*line-height: 1.5;
font: 12px sans-serif;
/*color: var(--main-text-color);
background-color: var(--cell-bg-color);*/
} }
.img_bg{ .img_bg{
...@@ -237,6 +242,10 @@ span.frac > span { ...@@ -237,6 +242,10 @@ span.frac > span {
display: none; display: none;
} }
#tblViajes {
font: 12px sans-serif;
}
/****** /******
Icons Icons
******/ ******/
...@@ -328,7 +337,7 @@ Icons ...@@ -328,7 +337,7 @@ Icons
[class^="fa-"], [class*=" fa-"] { [class^="fa-"], [class*=" fa-"] {
/* use !important to prevent issues with browser extensions that change fonts */ /* use !important to prevent issues with browser extensions that change fonts */
font-family: 'icomoon' !important; font-family: 'icomoon' /*!important*/;
speak: none; speak: none;
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
...@@ -428,6 +437,16 @@ Icons ...@@ -428,6 +437,16 @@ Icons
} }
/* Graphs */ /* Graphs */
#amchartdiv {
width: 100%;
height: 100%;
}
.amcharttooltip {
font: 12px sans-serif;
}
.active { .active {
text-decoration: underline; text-decoration: underline;
font-weight: 600; font-weight: 600;
...@@ -482,7 +501,7 @@ Icons ...@@ -482,7 +501,7 @@ Icons
.tooltip { .tooltip {
position: absolute; position: absolute;
bottom: 50%; /* so it is not added below the map and scroll bars appear */ /*bottom: 50%; /* so it is not added below the map and scroll bars appear */
/*width: 120px; /*width: 120px;
height: 20px;*/ height: 20px;*/
z-index: 1001; z-index: 1001;
......
/* /*
* Copyright 2019 - All rights reserved. * Copyright 2019-2020 - All rights reserved.
* Rodrigo Tapia-McClung * Rodrigo Tapia-McClung
* *
* August-September 2019 * August 2019 - June 2020
*/ */
/* global baseFileSize, formatBytes, Promise, omnivore, JSZip, map, layerControl, intervals, updateCharts, odClick*/ /* global baseFileSize, formatBytes, Promise, omnivore, JSZip, map, layerControl, intervals, updateCharts, odClick, am4core*/
/* exported makeBaseMap, baseLayerPromises, drawnItems */ /* exported, odData, makeBaseMap, baseLayerPromises, drawnItems */
/* Lines related to displaying loading bar are commented */ /* Lines related to displaying loading bar are commented */
...@@ -18,8 +18,8 @@ let baseLayerPromises = []; ...@@ -18,8 +18,8 @@ let baseLayerPromises = [];
baseLayerCounter = 0, baseLayerCounter = 0,
currentBaseLayer = 1;*/ currentBaseLayer = 1;*/
let drawnItems; //let drawnItems;
let od, flowMapsArray = []; let od, flowMapsArray = [], odData = {};
/*Object.keys(baseFileSize).forEach((name) => { /*Object.keys(baseFileSize).forEach((name) => {
if (name.split(".")[1] == "zip") { if (name.split(".")[1] == "zip") {
...@@ -29,6 +29,10 @@ let od, flowMapsArray = []; ...@@ -29,6 +29,10 @@ let od, flowMapsArray = [];
} }
});*/ });*/
function compare(el1, el2, index) {
return el1[index] == el2[index] ? 0 : (el1[index] < el2[index] ? -1 : 1);
}
// function to read compressed json and create a leaflet layer // function to read compressed json and create a leaflet layer
const zip2Lyr = (zipFile, layerName, layerTemplate) => { const zip2Lyr = (zipFile, layerName, layerTemplate) => {
baseLayerPromises.push( baseLayerPromises.push(
...@@ -924,6 +928,39 @@ const createFlowLayer = (geojson, type, addOnCreate) => { ...@@ -924,6 +928,39 @@ const createFlowLayer = (geojson, type, addOnCreate) => {
}); });
$.getJSON(geojson, data => { $.getJSON(geojson, data => {
// build data array from geojson to be used in amchart
let source = data.features;
let newData = [];
source.forEach(o => newData.push({'xVar': o.properties.muni_origen, 'yVar': o.properties.muni_destino, 'flowCount': o.properties.viajes}) );
let origins= [],
destinations = [];
newData.forEach( data => {
origins.push(data.xVar); // all origins
destinations.push(data.yVar) // all destinations
});
origins = origins.filter((v, i, a) => a.indexOf(v) === i); // get unique ones
destinations = destinations.filter((v, i, a) => a.indexOf(v) === i); // get unique ones
// add missing combinations with 0 trips
origins.forEach( (o) => {
destinations.forEach( (d) => {
if (!newData.some( data => data.xVar === o && data.yVar === d ) ) {
newData.push({'xVar': o, 'yVar': d, 'flowCount': 0});
}
})
});
// sort array by origins and then by destinations
newData.sort( (el1,el2) => {
let compared = compare(el1, el2, "xVar")
return compared == 0 ? compare(el1, el2, "yVar") : compared;
});
odData[type] = newData; // push to chart data object to reuse
// TODO: add heatColors to each data array
let flowMapLayer = L.canvasFlowmapLayer(data, { let flowMapLayer = L.canvasFlowmapLayer(data, {
// Define origins and destination from json values // Define origins and destination from json values
originAndDestinationFieldIds: { originAndDestinationFieldIds: {
...@@ -989,9 +1026,11 @@ const createFlowLayer = (geojson, type, addOnCreate) => { ...@@ -989,9 +1026,11 @@ const createFlowLayer = (geojson, type, addOnCreate) => {
return label[0].properties.nombre; return label[0].properties.nombre;
}) })
// if layer is to be added to on creation, add click funcionality // if layer is to be added on creation, add click funcionality and populate chart
if (addOnCreate) { if (addOnCreate) {
flowMapLayer.addTo(map).on('click', odClick); flowMapLayer.addTo(map).on('click', odClick);
let amchart = am4core.registry.baseSprites.find(c => c.htmlContainer.id === "amchartdiv");
amchart.data = newData;
} }
flowMapsArray.push(flowMapLayer); flowMapsArray.push(flowMapLayer);
resolve(flowMapsArray); resolve(flowMapsArray);
...@@ -1114,9 +1153,12 @@ const makeBaseMap = () => { ...@@ -1114,9 +1153,12 @@ const makeBaseMap = () => {
} }
}); });
layer._resetCanvas(); layer._resetCanvas();
$("#tblViajesDesde tbody").html(""); $("#tblViajes tbody").html("");
} }
}); });
// TODO: reset amchart
//let amchart = am4core.registry.baseSprites.find(c => c.htmlContainer.id === "amchartdiv")
//amchart.data = odData.ocupadosDesde;
}); });
//resolve(layers); //resolve(layers);
}); });
......
/*
* Copyright 2020 - All rights reserved.
* Rodrigo Tapia-McClung
*
* June 2020
*/
/* globals am4core, am4charts, am4themes_animated, intervals */
let minValue, maxValue;
let mainTextColor = getComputedStyle(document.body).getPropertyValue('--main-text-color');
let cellbgColor = getComputedStyle(document.body).getPropertyValue('--main-bg-color');
// amChart
am4core.ready(function() {
// Themes begin
//am4core.useTheme(am4themes_animated);
am4core.options.autoSetClassName = true;
// Themes end
var chart = am4core.create("amchartdiv", am4charts.XYChart);
chart.maskBullets = false;
var title = chart.titles.create();
//title.text = "Viajes de ocupados desde centros de mercado";
title.fill = am4core.color(mainTextColor);
title.fontSize = 15;
title.marginBottom = 15;
chart.events.on("beforedatavalidated", function(ev) {
let option = $("#indicatorSelect").val();
let optionTitle = $("#indicatorSelect option:selected").text();
title.text = optionTitle;
var data = ev.target.data;
minValue = d3.min(data, function(d) { return d.flowCount || Infinity; });
maxValue = d3.max(data, function(d) { return d.flowCount; });
//heatLegend.minValue = minValue;
//heatLegend.maxValue = maxValue;
heatLegend.minValue = 0;
heatLegend.maxValue = intervals[option].values[intervals[option].values.length-1];
// update heatLegend colors
let heatColors = [];
["#333", ...intervals[option].colors].forEach( c => heatColors.push(am4core.color(c)) );
heatLegend.minColor = heatColors[0];
heatLegend.maxColor = heatColors[intervals[option].colors.length - 1];
//let checkConditions = [minValue, ...intervals[option].values.slice(1)];
let checkConditions = [minValue, ...intervals[option].values];
let lastValue = intervals[option].values[intervals[option].values.length-1]
// Override heatLegend gradient
let gradient = new am4core.LinearGradient();
heatColors.forEach(function(color, index) {
// addColor(color, opacity, offset) use offset to put colors in proper alignment
gradient.addColor(color, undefined, (checkConditions[index] - checkConditions[0])/lastValue);
});
//heatLegend.markers.template.applyOnClones = true;
heatLegend.markers.clear();
heatLegend.markers.template.adapter.add("fill", function() {
return gradient;
});
// update cell colors
columnTemplate.adapter.add("fill", function(fill, column) {
let workingValue = column.dataItem.values["value"].workingValue;
// use min max values calculated from data on beforedatavalidated
if (am4core.type.isNumber(workingValue)) {
checkConditions.forEach( (condition, index) => {
if ( index < checkConditions.length-1 ) {
if (workingValue >= condition && workingValue <= checkConditions[index+1]) {
//console.log(`${workingValue} entre ${condition} y ${checkConditions[index+1]}`)
fill = new am4core.Color(
am4core.colors.interpolate(
heatColors[index].rgb,
heatColors[index+1].rgb,
workingValue
)
);
//console.log(workingValue, intervals.ocupadosDesde.colors[index])
} else if (workingValue < checkConditions[0]) {
fill = new am4core.color(cellbgColor);
}
}
});
}
return fill;
});
});
var xAxis = chart.xAxes.push(new am4charts.CategoryAxis());
var yAxis = chart.yAxes.push(new am4charts.CategoryAxis());
xAxis.dataFields.category = "xVar";
yAxis.dataFields.category = "yVar";
xAxis.renderer.grid.template.disabled = true;
xAxis.renderer.minGridDistance = 10;
xAxis.renderer.opposite = true;
//xAxis.renderer.labels.template.rotation = -45;
/*xAxis.renderer.labels.template.adapter.add("dy", function(dy, target) {
if (target.dataItem && target.dataItem.index & 2 == 2) {
return dy - 15;
}
return dy;
});*/
xAxis.title.text = "[bold]Origen[/]";
xAxis.title.fontSize = 15
xAxis.title.paddingBottom = -10;
xAxis.title.paddingTop = -10;
xAxis.renderer.labels.template.truncate = true;
// on axis size change, resize labels
//FIXME: 80% hack should be better
xAxis.events.on("sizechanged", function(ev) {
let axis = ev.target;
let cellWidth = axis.pixelWidth / (axis.endIndex - axis.startIndex);
axis.renderer.labels.template.maxWidth = 2*Math.ceil(cellWidth)*0.8;
});
// on data change change, resize labels
//FIXME: 80% hack should be better
xAxis.events.on("datarangechanged", function(ev) {
let axis = ev.target;
let cellWidth = axis.pixelWidth / (axis.endIndex - axis.startIndex);
axis.renderer.labels.template.maxWidth = 2*Math.ceil(cellWidth)*0.8;
});
xAxis.renderer.labels.template.fontSize = 12;
xAxis.tooltip.fontSize = 13;
xAxis.title.fill = am4core.color(mainTextColor);
xAxis.renderer.labels.template.fill = am4core.color(mainTextColor);
yAxis.renderer.grid.template.disabled = true;
yAxis.renderer.inversed = true;
yAxis.renderer.minGridDistance = 1;
yAxis.title.text = "[bold]Destino[/]";
yAxis.title.fontSize = 15
yAxis.title.paddingBottom = -10;
yAxis.renderer.labels.template.truncate = true;
yAxis.renderer.labels.template.maxWidth = 120;
yAxis.renderer.labels.template.fontSize = 12;
yAxis.tooltip.fontSize = 13;
yAxis.title.fill = am4core.color(mainTextColor);
yAxis.renderer.labels.template.fill = am4core.color(mainTextColor);
var series = chart.series.push(new am4charts.ColumnSeries());
series.dataFields.categoryX = "xVar";
series.dataFields.categoryY = "yVar";
series.dataFields.value = "flowCount";
series.sequencedInterpolation = true;
series.cursorTooltipEnabled = false;
series.defaultState.transitionDuration = 3000;
// Add cursor
chart.cursor = new am4charts.XYCursor();
chart.cursor.behavior = "none";
chart.cursor.xAxis = xAxis;
chart.cursor.yAxis = yAxis;
chart.cursor.fullWidthLineX = true;
chart.cursor.fullWidthLineY = true;
chart.cursor.lineX.fill = am4core.color(mainTextColor);
chart.cursor.lineX.fillOpacity = 0.05;
chart.cursor.lineY.fill = am4core.color(mainTextColor);
chart.cursor.lineY.fillOpacity = 0.05;
var bgColor = new am4core.InterfaceColorSet().getFor("background");
var columnTemplate = series.columns.template;
columnTemplate.applyOnClones = true;
columnTemplate.strokeWidth = 2;
columnTemplate.strokeOpacity = 0;
columnTemplate.stroke = bgColor;
//columnTemplate.tooltipText = "{xVar} -> {yVar}: {value.workingValue.formatNumber('#.')}";
// TODO: FIX long-arrow not present in fa icons - conflict with
// <i class="fas fa-long-arrow-alt-right"> does not work
columnTemplate.tooltipHTML = `<div class="amcharttooltip">{xVar} <i class="fas">&#xf30b;</i> {yVar}: {value.workingValue.formatNumber('#,###')}</div>`;
columnTemplate.width = am4core.percent(100);
columnTemplate.height = am4core.percent(100);
columnTemplate.column.cornerRadius(4, 4, 4, 4);
columnTemplate.column.padding(0.5, 0.5, 0.5, 0.5);
columnTemplate.adapter.add("strokeWidth", function(width, column) {
var workingValue = column.dataItem.values["value"].workingValue;
if (am4core.type.isNumber(workingValue)) {
width = workingValue != 0 ? 1: 0;
}
return width;
});
// heat legend
//var heatLegend = chart.createChild(am4charts.HeatLegend);
var heatLegend = chart.bottomAxesContainer.createChild(am4charts.HeatLegend);
heatLegend.marginTop = 5;
//heatLegend.marginBotom = 50;
heatLegend.width = am4core.percent(100);
//heatLegend.align = "right";
//heatLegend.markerContainer.height = 10;
heatLegend.markerContainer.minHeight = 20;
heatLegend.markers.template.minHeight = 20;
heatLegend.series = series;
heatLegend.valueAxis.renderer.labels.template.fontSize = 12;
heatLegend.valueAxis.renderer.minGridDistance = 35;
heatLegend.valueAxis.renderer.labels.template.fill = am4core.color(mainTextColor);
heatLegend.valueAxis.renderer.labels.template.paddingTop = 5;
heatLegend.valueAxis.tooltip.fontSize = 13;
// heatLegend min, max, colors and fill are modified above using adapters
// when chart changes size, what happens with heatLegend fill
heatLegend.markers.template.events.on("sizechanged", function(event) {
event.target.fill = event.target.fill;
});
// heat legend behavior
series.columns.template.events.on("over", function(event) {
handleHover(event.target);
})
series.columns.template.events.on("hit", function(event) {
handleHover(event.target);
})
function handleHover(column) {
if (!isNaN(column.dataItem.value)) {
column.strokeWidth = 2;
column.strokeOpacity = 0.2;
heatLegend.valueAxis.showTooltipAt(column.dataItem.value);
}
else {
column.strokeWidth = 0;
column.strokeOpacity = 0;
heatLegend.valueAxis.hideTooltip();
}
}
series.columns.template.events.on("out", function(event) {
event.target.strokeWidth = 0;
event.target.strokeOpacity = 0;
heatLegend.valueAxis.hideTooltip();
})
//chart.dataSource.url = "data/odm.json";
//chart.dataSource.url = "data/viajes_ocupados_desde.geojson";
chart.responsive.enabled = true;
}); // end am4core.ready()
/* /*
* Copyright 2019 - All rights reserved. * Copyright 2019-2020 - All rights reserved.
* Rodrigo Tapia-McClung * Rodrigo Tapia-McClung
* *
* August-September 2019 * August 2019 - June 2020
*/ */
/* globals omnivore, Promise, chroma, makeBaseMap, makeIndicatorGraph, getData, getDataInSelection */ /* globals omnivore, Promise, chroma, makeBaseMap, makeIndicatorGraph, getData, getDataInSelection */
...@@ -71,7 +71,7 @@ let intervals = { ...@@ -71,7 +71,7 @@ let intervals = {
}, },
"ocupadosHacia": { "ocupadosHacia": {
"classes": ["Menos de 1,000", "1,001 - 2,000", "2,001 - 6,000", "6,001 - 32,000", "32,001 - 53,300"], "classes": ["Menos de 1,000", "1,001 - 2,000", "2,001 - 6,000", "6,001 - 32,000", "32,001 - 53,300"],
"values" : [1000, 2000, 6000, 32000, 533000], "values" : [1000, 2000, 6000, 32000, 53300],
"colors": ["#ffbee8", "#ff73df", "#ff00c5", "#ad027d", "#80006b"], "colors": ["#ffbee8", "#ff73df", "#ff00c5", "#ad027d", "#80006b"],
"thickness": [0.5, 1.5, 2, 4, 10], "thickness": [0.5, 1.5, 2, 4, 10],
"animThickness": [0.5, 1.5, 4, 10, 15] "animThickness": [0.5, 1.5, 4, 10, 15]
...@@ -423,9 +423,10 @@ const odClick = (e) => { ...@@ -423,9 +423,10 @@ const odClick = (e) => {
e.target.selectFeaturesForPathDisplay(dests, "SELECTION_ADD"); e.target.selectFeaturesForPathDisplay(dests, "SELECTION_ADD");
dests.forEach( dest => { dests.forEach( dest => {
//origins += `${dest.properties.muni_origen} &rarr; ${dest.properties.muni_destino}: ${dest.properties.viajes} viajes <br>`; //origins += `${dest.properties.muni_origen} &rarr; ${dest.properties.muni_destino}: ${dest.properties.viajes} viajes <br>`;
origins += `<tr><td>${dest.properties.muni_origen}</td><td>${dest.properties.muni_destino}</td><td>${dest.properties.viajes}</td></tr>`; // style viajes thousands with #,###
origins += `<tr><td>${dest.properties.muni_origen}</td><td>${dest.properties.muni_destino}</td><td>${dest.properties.viajes.toLocaleString()}</td></tr>`;
}); });
$("#tblViajesDesde tbody").html(origins); $("#tblViajes tbody").html(origins);
if (e.sharedDestinationFeatures.length) { if (e.sharedDestinationFeatures.length) {
//e.target.selectFeaturesForPathDisplay(e.sharedDestinationFeatures, "SELECTION_ADD"); //e.target.selectFeaturesForPathDisplay(e.sharedDestinationFeatures, "SELECTION_ADD");
...@@ -435,6 +436,8 @@ const odClick = (e) => { ...@@ -435,6 +436,8 @@ const odClick = (e) => {
/*if (e.sharedDestinationFeatures.length) { /*if (e.sharedDestinationFeatures.length) {
e.target.selectFeaturesForPathDisplay(e.sharedDestinationFeatures, "SELECTION_NEW"); e.target.selectFeaturesForPathDisplay(e.sharedDestinationFeatures, "SELECTION_NEW");
}*/ }*/
// TODO: trigger chart update to only draw selected origins and destinations?
} }
const populateMap = async(mapData) => { const populateMap = async(mapData) => {
...@@ -784,7 +787,6 @@ L.timeDimension.layer.Tile = (layer, options) => { ...@@ -784,7 +787,6 @@ L.timeDimension.layer.Tile = (layer, options) => {
return new L.TimeDimension.Layer.Tile(layer, options); return new L.TimeDimension.Layer.Tile(layer, options);
}; };
// When selecting indicator from dropdown, style tiles. // When selecting indicator from dropdown, style tiles.
$("#indicatorSelect").on("change", function() { $("#indicatorSelect").on("change", function() {
// style currentTiles // style currentTiles
...@@ -806,6 +808,10 @@ $("#indicatorSelect").on("change", function() { ...@@ -806,6 +808,10 @@ $("#indicatorSelect").on("change", function() {
} }
}); });
// on select, update chart data
let amchart = am4core.registry.baseSprites.find(c => c.htmlContainer.id === "amchartdiv")
amchart.data = odData[option];
styleTiles(option, minIndicators, maxIndicators) styleTiles(option, minIndicators, maxIndicators)
.then(legend.addTo(map)); // add legend control -> it updates .then(legend.addTo(map)); // add legend control -> it updates
// FIXME: re-adding control updates its contents... why? // FIXME: re-adding control updates its contents... why?
......
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