@ndbx/runtime
Version:
The `@ndbx/runtime` package provides a runtime environment to embed NodeBox visualizations directly into React applications. NodeBox is a powerful tool for creating interactive and generative visualizations, and this runtime allows you to integrate those
231 lines (215 loc) • 8.28 kB
JavaScript
/**
* Generate a default map.
*
* Parameters:
* - Source CRS: This parameter provides the necessary information to interpret geographic coordinates.
* A CRS (Coordinate Reference System) is a system that defines how geographic data is mapped
* onto a flat surface. It includes three components:
* (1) The coordinate system defines how locations on Earth are described numerically.
* (e.g. geographical coordinates like latitude and longitude or
* projected Cartesian coordinates like X, Y coordinates)
* (2) The datum specifies the reference model of the Earth's shape (e.g., WGS84, NAD83)
* and its position relative to the Earth’s surface.
* (3) The projection is the method used to transform the Earth's curved surface into a flat,
* two-dimensional map.
* - Target projection:
* All source data will be reprojected to WGS84 (EPSG:4326) geographic coordinates.
* The selected projection transforms these geographic coordinates (latitude and longitude)
* into a new set of cartesian coordinates to plot on a flat 2D plane (X and Y coordinates).
*
* @category Geo
*/
import { debugPrint } from "project:Utilities";
import {
validateVegaSpec,
emptyMap,
proj4Defs,
scaleDefaults,
addParamAttr,
parse2vegaRef,
setScale,
applyScale,
reprojectCoordinates,
setMark,
plotGeodata,
setGraticule,
} from "project:Utilities";
import { parse as parseVega } from "https://esm.sh/vega@5";
import { min, max, ascending, descending, rollup, sum, bin, range as d3range } from "https://esm.sh/d3-array@3.2.4";
import {
scaleLinear,
scaleTime,
scaleUtc,
scalePow,
scaleSqrt,
scaleLog,
scaleSymlog,
scaleOrdinal,
scaleBand,
scalePoint,
scaleDiverging,
scaleQuantile,
scaleQuantize,
scaleThreshold,
} from "https://esm.sh/d3-scale@4.0.2";
export default function (node) {
const plotIn = node.specIn({ name: "plotSpec", label: "Plot spec" });
const dataIn = node.tableIn({ name: "dataIn", label: "Geodata" });
// Add parameters
node.pushSection({ name: "General" });
const dataNameIn = node.stringIn({
name: "dataName",
label: "Data name",
value: "geodata",
});
const markNameIn = node.stringIn({
name: "markName",
label: "Mark name",
value: "geoshape",
});
node.popSection();
node.pushSection({ name: "Source data" });
const coordSystemIn = node.stringIn({
name: "crs",
label: "Source CRS",
value: "EPSG4326",
choices: [
["EPSG4326", "WGS84 (EPSG:4326)"],
["EPSG3857", "Web Mercator (EPSG:3857)"],
["EPSG31370", "Belge Lambert 72 (EPSG:31370)"],
["EPSG4269", "NAD83 (EPSG:4269)"],
],
});
const formatIn = node.stringIn({
name: "format",
label: "Format",
value: "json",
choices: [
["json", "JSON"],
["geojson", "GeoJSON"],
["topojson", "TopoJSON"],
],
});
const featureIn = node.stringIn({
name: "feature",
label: "Feature",
});
node.popSection();
node.pushSection({ name: "Mark properties" });
const fillIn = node.stringIn({ name: "fillColor", label: "Fill color" });
const strokeIn = node.stringIn({ name: "strokeColor", label: "Stroke color" });
const sizeIn = node.numberIn({ name: "size", label: "Size" });
const strokeWidthIn = node.numberIn({ name: "strokeWidth", label: "Stroke width" });
const shapeIn = node.stringIn({ name: "shape", label: "Shape" });
const opacityIn = node.numberIn({ name: "opacity", label: "Opacity" });
const fillOpacityIn = node.numberIn({ name: "fillOpacity", label: "Fill opacity" });
const strokeOpacityIn = node.numberIn({ name: "strokeOpacity", label: "Stroke opacity" });
const strokeDashIn = node.stringIn({ name: "strokeDash", label: "Stroke dash" });
node.popSection();
const plotOut = node.specOut({ name: "plotSpecOut", label: "Plot spec" });
node.onRender = () => {
const dataName = dataNameIn.value;
let specOut = structuredClone(plotIn.value ? plotIn.value : emptyMap);
validateVegaSpec(specOut);
let specData = specOut.data.find((d) => d.name == dataName);
let geoData = dataIn.value ? structuredClone(dataIn.value) : specData ? specData.values : []; // check input data first, then plotSpec data
const sourceCRS = coordSystemIn.value;
// If geodata exists, apply the reprojection and plot it
if (geoData) {
// Reproject the coordinates based on the source CRS
const reprojectedGeoData = reprojectCoordinates(geoData, sourceCRS);
// Prepare parameters for plotGeodata
const plotParams = {
spec: specOut, // Current Vega spec
geodata: reprojectedGeoData, // Reprojected geodata
format: formatIn.value, // Data format,
feature: featureIn.value,
dataName: dataName,
markName: markNameIn.value,
};
// Plot the reprojected geodata using Vega specifications
specOut = plotGeodata(plotParams);
}
// SET SCALES
const scaleNames = scaleDefaults.map((d) => d.scaleName);
const scaleAttrs = scaleDefaults.map((d) => d.scaleAttr);
const scaleProps = scaleDefaults.map((d) => d.property);
const nodeParams = [
undefined,
undefined,
fillIn,
strokeIn,
sizeIn,
strokeWidthIn,
shapeIn,
opacityIn,
fillOpacityIn,
strokeOpacityIn,
strokeDashIn,
];
scaleNames.forEach((scaleName, i) => {
let domain = undefined;
const paramVal = nodeParams[i] ? nodeParams[i].value : undefined;
if (paramVal) {
const paramType = nodeParams[i].node.values[nodeParams[i].name]
? nodeParams[i].node.values[nodeParams[i].name].type
: undefined;
// ADD SCALE PARAMS
const scaleAttr = scaleAttrs[i];
const nodeParam = nodeParams[i];
// Add scaleAttr to data if expression, quoted string or single value (not list, array or object)
const objRegex = new RegExp("^\\{.*\\}$");
const arrRegex = new RegExp("^\\[.*\\]$");
const litRegex = new RegExp('^".*"$');
const commaRegex = new RegExp(",");
if (paramType === "EXPRESSION") {
addParamAttr(specOut, dataName, scaleAttr, nodeParam, true);
} else if (paramType === "VALUE" && typeof paramVal === "number") {
//addParamAttr(specOut, dataName, scaleAttr, nodeParam);
} else if (paramType === "VALUE" && paramVal.match(litRegex) && paramVal !== "") {
//addParamAttr(specOut, dataName, scaleAttr, nodeParam);
} else if (
paramType === "VALUE" &&
!paramVal.match(objRegex) &&
!paramVal.match(arrRegex) &&
!paramVal.match(commaRegex) &&
paramVal !== ""
) {
//addParamAttr(specOut, dataName, scaleAttr, nodeParam, true);
}
let result;
if ((paramType === "VALUE") & (typeof paramVal !== "number")) {
const regex = new RegExp('^".*"$');
result = paramVal.split(",").map((d) => (!isNaN(Number(d)) ? Number(d) : d.match(regex) ? JSON.parse(d) : d)); // Convert comma-separated value to array
if (!Array.isArray(result) || result.length === 1) result = result[0];
} else {
result = paramVal;
}
if (paramType === "VALUE" && !Array.isArray(result)) {
// Set mark if parameter is value
const scaleProp = scaleProps[i];
setMark({
mode: "update_all",
spec: specOut,
dataName: dataName,
[scaleProp]: parse2vegaRef(specOut, dataName, scaleAttrs[i], nodeParams[i]),
});
} else {
// Set and apply scale
//domain = parse2vegaRef(specOut, dataName, scaleAttrs[i], nodeParams[i]);
domain = parse2vegaRef(specOut, dataName, scaleAttrs[i], nodeParams[i]);
setScale({
spec: specOut,
dataName: dataName,
scaleName: scaleName,
plotType: undefined,
domain: undefined,
//typeof domain !== "object" ? domain : Object.keys(domain).includes("value") ? [domain.value] : domain,
});
applyScale({ spec: specOut, scaleName: scaleName, plotType: undefined });
}
}
});
plotOut.set(specOut);
};
}