Public
Edited
Nov 13, 2022
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart = SankeyChart({links: Data.sankey},{
nodeGroup: d => d.id.split(/\W/)[0],
width,
format: (f => d => `${f(d)} Acres`)(d3.format(",.1~f")),

// Had to fork the built in Sankey Library so I could make the colors match the Land-Use classes
adjustNodeColor: (c,t)=>
{

switch (t.split(/[\W\n]/)[0])
{
case "Water":
return "#419bdf";
case "Trees":
return "#397d49";
case "Rangeland":
return "#e3e2c3";
case "Crops":
return "#e49635";
case "Built":
return "#c4281b";
case "Bare":
return "#a59b8f";
case "Snow":
return "#aaeafd";
case "Flooded":
return "#7a87c6";
}},
height: 300})


Insert cell
Insert cell
async function performAnalysis()
{
let result={};
console.log(map.getCenter());
console.log(map.getZoom());
console.log("starting analyze");
const NO_DATA =0; //255,255,255
const WATER=1; // 65,155,223
const TREES=2; // 57,125, 73
const FLOODED_VEGETATION=3; //122,135,198
const CROPS=4; //228,150, 53
const BUILT_AREA = 5; //196, 40, 27
const BARE_GROUND=6; //165,155,143
const SNOW_ICE=7; //168,235,255
const CLOUDS=8; // 97, 97, 97
const RANGELAND=9; //227,226,195
const FRIENDLY_NAMES = ["No Data","Water","Trees","Flooded Vegetation","Crops","Built Area","Bare Ground","Snow/Ice","Clouds","Rangeland"];
const SQM_PER_ACRE = 4046.86; //ugh, why no metric in this country
const WGS_EARTH_RADIUS = 6378137.0; // used to calculate areas
// ripped from the Leaflet-draw library because it wasn't happy being included here and is total ovekill
let geodesicArea = function (latLngs) {
var pointsCount = latLngs.length,
area = 0.0,
d2r = Math.PI / 180,
p1, p2;
if (pointsCount > 2) {
for (var i = 0; i < pointsCount; i++) {
p1 = latLngs[i];
p2 = latLngs[(i + 1) % pointsCount];
area += ((p2.lng - p1.lng) * d2r) *
(2 + Math.sin(p1.lat * d2r) + Math.sin(p2.lat * d2r));
}
area = area * (WGS_EARTH_RADIUS*WGS_EARTH_RADIUS) / 2.0;
}
return Math.abs(area);
}
// We can determine the use type entirely by the red pixels as each item in the symbology varies
function getUseType(redPixel)
{
switch (redPixel)
{
case 65: return WATER;
case 57: return TREES;
case 122: return FLOODED_VEGETATION;
case 228: return CROPS;
case 196: return BUILT_AREA;
case 165: return BARE_GROUND;
case 168: return SNOW_ICE;
case 97: return CLOUDS;
case 227: return RANGELAND;
}
return redPixel;
// console.log("Undefined r of "+ redPixel);
}


// This is a custom forked version of the LeafletImage library that will let me render a single layer
// instead of the map as it shows up on screen.
let complete=false;
let ls2017only = function(l){ return l.name=="2017";}
LeafletImage(map, ls2017only, function(err,canvas2017) {
let ls2021Only = function(l){ return l.name=="2021";}
LeafletImage(map, ls2021Only, function(err,canvas2021) {
var dimensions = map.getSize();
const image2017 = canvas2017.getContext("2d").getImageData(0,0,dimensions.x,dimensions.y);
const image2021 = canvas2021.getContext("2d").getImageData(0,0,dimensions.x,dimensions.y);
result.totalMapAreaSqM = geodesicArea(
[map.getBounds().getNorthWest(),
map.getBounds().getNorthEast(),
map.getBounds().getSouthEast(),
map.getBounds().getSouthWest() ]);
result.acresPerPixel = result.totalMapAreaSqM/ SQM_PER_ACRE / (dimensions.x * dimensions.y);
result.pixelsChangedDict=[];
result.totalPixels=0;
result.pixelsChanged=0;
for (let i = 0; i < image2017.data.length; i += 4)
{
result.totalPixels++;
let pixelUse2017 =getUseType(image2017.data[i]);
let pixelUse2021 =getUseType(image2021.data[i]);
if (pixelUse2017!=pixelUse2021)
{
result.pixelsChanged++;
}
if (!result.pixelsChangedDict[pixelUse2017 + "|" + pixelUse2021])
{
result.pixelsChangedDict[pixelUse2017 + "|" + pixelUse2021]=0;
}
result.pixelsChangedDict[pixelUse2017 + "|" + pixelUse2021]++;
}
result.sankey=[];
for (let src=0;src<FRIENDLY_NAMES.length;src++)
{
for (let dest=0;dest<FRIENDLY_NAMES.length;dest++)
{
if ((result.pixelsChangedDict[src + "|" + dest]) && result.pixelsChangedDict[src + "|" + dest]>1000)
{

result.sankey.push({source:FRIENDLY_NAMES[src] , target: FRIENDLY_NAMES[dest]+" ", value:(result.pixelsChangedDict[src + "|" + dest]*result.acresPerPixel) });
}
}
}
console.log("done analyzing");
complete=true;
console.log(result);
});
});
// THis is horribly hacky, but stops us returning until everything is loaded
while (!complete)
{
await new Promise(r => setTimeout(r, 100));
}
return result;
}
Insert cell
Insert cell
Insert cell
// github sets the wrong mimetype on js files so we using jsdelivr as a proxy to load this
// after updating github i have to hit https://purge.jsdelivr.net/gh/grahamsz/leaflet-image-layers/leaflet-image.js to clear the cdn
LeafletImage = require("https://cdn.jsdelivr.net/gh/grahamsz/leaflet-image-layers/leaflet-image.js?2")
Insert cell
import {SankeyChart} from "88895dac303f3ed9"
Insert cell
Insert cell
Insert cell
import {d3Sankey} from "@d3/sankey"
Insert cell
Data = performAnalysis();
Insert cell
//d3 = require("d3@5")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more