diff --git a/packages/visualizations/package-lock.json b/packages/visualizations/package-lock.json
index 2350d639..8a372c33 100644
--- a/packages/visualizations/package-lock.json
+++ b/packages/visualizations/package-lock.json
@@ -6,7 +6,7 @@
"packages": {
"": {
"name": "@opendatasoft/visualizations",
- "version": "0.8.2",
+ "version": "0.9.0",
"license": "MIT",
"dependencies": {
"@mapbox/geo-viewport": "^0.5.0",
@@ -34,8 +34,11 @@
"@rollup/plugin-replace": "^3.0.1",
"@rollup/plugin-typescript": "^6.1.0",
"@tsconfig/svelte": "^1.0.10",
+ "@types/chroma-js": "^2.1.4",
+ "@types/geojson": "^7946.0.10",
"@types/lodash": "^4.14.182",
"@types/luxon": "^2.0.5",
+ "@types/mapbox__geo-viewport": "^0.4.1",
"@types/markdown-it": "^12.0.1",
"@types/markdown-it-link-attributes": "^3.0.1",
"@types/rollup-plugin-visualizer": "^4.2.1",
@@ -2045,6 +2048,12 @@
"@babel/types": "^7.3.0"
}
},
+ "node_modules/@types/chroma-js": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.4.tgz",
+ "integrity": "sha512-l9hWzP7cp7yleJUI7P2acmpllTJNYf5uU6wh50JzSIZt3fFHe+w2FM6w9oZGBTYzjjm2qHdnQvI+fF/JF/E5jQ==",
+ "dev": true
+ },
"node_modules/@types/estree": {
"version": "0.0.39",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
@@ -2052,9 +2061,9 @@
"dev": true
},
"node_modules/@types/geojson": {
- "version": "7946.0.8",
- "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz",
- "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA=="
+ "version": "7946.0.10",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
+ "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA=="
},
"node_modules/@types/json-schema": {
"version": "7.0.9",
@@ -2086,6 +2095,12 @@
"integrity": "sha512-GKrG5v16BOs9XGpouu33hOkAFaiSDi3ZaDXG9F2yAoyzHRBtksZnI60VWY5aM/yAENCccBejrxw8jDY+9OVlxw==",
"dev": true
},
+ "node_modules/@types/mapbox__geo-viewport": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@types/mapbox__geo-viewport/-/mapbox__geo-viewport-0.4.1.tgz",
+ "integrity": "sha512-aW7Orm58KsT9KmpZb1sqM2l/KufsS1IUsL1RCplVLfUIbpTk3PYkpOat3CN7jA8KcO1w1TuNFapn0g+/rASWzQ==",
+ "dev": true
+ },
"node_modules/@types/mapbox__point-geometry": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.2.tgz",
@@ -9224,6 +9239,12 @@
"@babel/types": "^7.3.0"
}
},
+ "@types/chroma-js": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.4.tgz",
+ "integrity": "sha512-l9hWzP7cp7yleJUI7P2acmpllTJNYf5uU6wh50JzSIZt3fFHe+w2FM6w9oZGBTYzjjm2qHdnQvI+fF/JF/E5jQ==",
+ "dev": true
+ },
"@types/estree": {
"version": "0.0.39",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
@@ -9231,9 +9252,9 @@
"dev": true
},
"@types/geojson": {
- "version": "7946.0.8",
- "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz",
- "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA=="
+ "version": "7946.0.10",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
+ "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA=="
},
"@types/json-schema": {
"version": "7.0.9",
@@ -9265,6 +9286,12 @@
"integrity": "sha512-GKrG5v16BOs9XGpouu33hOkAFaiSDi3ZaDXG9F2yAoyzHRBtksZnI60VWY5aM/yAENCccBejrxw8jDY+9OVlxw==",
"dev": true
},
+ "@types/mapbox__geo-viewport": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@types/mapbox__geo-viewport/-/mapbox__geo-viewport-0.4.1.tgz",
+ "integrity": "sha512-aW7Orm58KsT9KmpZb1sqM2l/KufsS1IUsL1RCplVLfUIbpTk3PYkpOat3CN7jA8KcO1w1TuNFapn0g+/rASWzQ==",
+ "dev": true
+ },
"@types/mapbox__point-geometry": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.2.tgz",
diff --git a/packages/visualizations/package.json b/packages/visualizations/package.json
index c37c4887..8cd92b53 100644
--- a/packages/visualizations/package.json
+++ b/packages/visualizations/package.json
@@ -45,8 +45,11 @@
"@rollup/plugin-replace": "^3.0.1",
"@rollup/plugin-typescript": "^6.1.0",
"@tsconfig/svelte": "^1.0.10",
+ "@types/chroma-js": "^2.1.4",
+ "@types/geojson": "^7946.0.10",
"@types/lodash": "^4.14.182",
"@types/luxon": "^2.0.5",
+ "@types/mapbox__geo-viewport": "^0.4.1",
"@types/markdown-it": "^12.0.1",
"@types/markdown-it-link-attributes": "^3.0.1",
"@types/rollup-plugin-visualizer": "^4.2.1",
diff --git a/packages/visualizations/src/components/Map/Choropleth.svelte b/packages/visualizations/src/components/Map/Choropleth.svelte
index e94b720d..d5418cb2 100644
--- a/packages/visualizations/src/components/Map/Choropleth.svelte
+++ b/packages/visualizations/src/components/Map/Choropleth.svelte
@@ -1,18 +1,31 @@
-
diff --git a/packages/visualizations/src/components/Map/index.ts b/packages/visualizations/src/components/Map/index.ts
index 430d0b09..3eb64f31 100644
--- a/packages/visualizations/src/components/Map/index.ts
+++ b/packages/visualizations/src/components/Map/index.ts
@@ -1,4 +1,5 @@
-import type { ChoroplethOptions, DataFrame } from '../types';
+import type { DataFrame } from '../types';
+import type { ChoroplethOptions } from './types';
import ChoroplethImpl from './Choropleth.svelte';
import SvelteImpl from '../SvelteImpl';
diff --git a/packages/visualizations/src/components/Map/types.ts b/packages/visualizations/src/components/Map/types.ts
new file mode 100644
index 00000000..f3034b2b
--- /dev/null
+++ b/packages/visualizations/src/components/Map/types.ts
@@ -0,0 +1,71 @@
+import type { Feature, FeatureCollection, Position } from 'geojson';
+import type { FillLayerSpecification, Popup } from 'maplibre-gl';
+import type { Color, ColorsScale } from '../types';
+
+export interface ChoroplethOptions {
+ shapes: ChoroplethShapeValue;
+ colorsScale?: ColorsScale;
+ legend?: MapLegend;
+ aspectRatio: number;
+ activeShapes?: string[];
+ interactive?: boolean;
+ emptyValueColor: Color;
+ tooltip: { label: ChoroplethTooltipFormatter };
+}
+
+export interface MapLegend {
+ title?: string;
+}
+
+/** Function used to render an HTML Tooltip depending on the shape the user
+ * interacted with.
+ */
+export type ChoroplethTooltipFormatter = ({
+ value,
+ label,
+ key,
+}: {
+ /** Numeric value of the shape */
+ value?: number;
+ /** Label of the shape */
+ label: string;
+ /** Value of the key used to match shapes and numeric data */
+ key?: string;
+}) => string;
+
+/** Structure containing the numerical data used by the Choropleth to compute
+ * the legend and the color of the shapes it renders.
+ */
+export interface ChoroplethDataValue {
+ x: string;
+ y: number;
+}
+
+/** `ChoroplethShapeValue` implementation based on a GeoJSON FeatureCollection. */
+export interface ChoroplethShapeGeoJsonValue {
+ type: 'geojson';
+ geoJson: FeatureCollection | null;
+}
+
+/** `ChoroplethShapeValue` implementation based on a Vector Tiles source URL. */
+export interface ChoroplethShapeVectorTilesValue {
+ type: 'vtiles';
+ url: string;
+}
+
+/** Structure containing everything necessary for a Choropleth to render shapes visually.
+ * Supports different types of structures, such as GeoJSON features, or a Vector Tiles source.
+ */
+export type ChoroplethShapeValue = ChoroplethShapeGeoJsonValue | ChoroplethShapeVectorTilesValue;
+
+export interface ChoroplethFixedTooltipDescription {
+ center: Position;
+ description: string;
+ popup: Popup;
+}
+
+export type MapRenderTooltipFunction = (f: Feature) => string;
+
+export type ChoroplethLayer = Omit;
+
+export type MapLayer = ChoroplethLayer;
diff --git a/packages/visualizations/src/components/Map/utils.js b/packages/visualizations/src/components/Map/utils.ts
similarity index 69%
rename from packages/visualizations/src/components/Map/utils.js
rename to packages/visualizations/src/components/Map/utils.ts
index 7bbb8b4f..9bf7f1ae 100644
--- a/packages/visualizations/src/components/Map/utils.js
+++ b/packages/visualizations/src/components/Map/utils.ts
@@ -2,23 +2,42 @@ import chroma from 'chroma-js';
import turfBbox from '@turf/bbox';
import maplibregl from 'maplibre-gl';
import geoViewport from '@mapbox/geo-viewport';
-
-export const LIGHT_GREY = '#CBD2DB';
-export const DARK_GREY = '#515457';
-
-export const colorShapes = (geoJson, values, colorsScale, emptyValueColor) => {
+import type { FeatureCollection, Feature, Position, BBox } from 'geojson';
+import type { Scale } from 'chroma-js';
+import type { Color, ColorsScale } from '../types';
+import type {
+ ChoroplethDataValue,
+ ChoroplethFixedTooltipDescription,
+ MapRenderTooltipFunction,
+} from './types';
+
+export const LIGHT_GREY: Color = '#CBD2DB';
+export const DARK_GREY: Color = '#515457';
+
+export const colorShapes = (
+ geoJson: FeatureCollection,
+ values: ChoroplethDataValue[],
+ colorsScale: ColorsScale,
+ emptyValueColor: Color
+): {
+ geoJson: FeatureCollection;
+ bounds: {
+ min: number;
+ max: number;
+ };
+} => {
// Key in the values is "x"
// Key in the shapes is "key"
// We add a color property in the JSON
const rawValues = values.map((v) => v.y);
const min = Math.min(...rawValues);
const max = Math.max(...rawValues);
- let colorMin;
- let colorMax;
- let scale;
+ let colorMin: Color;
+ let colorMax: Color;
+ let scale: Scale;
if (colorsScale?.type === 'palette') {
- const thresholdArray = [];
+ const thresholdArray: number[] = [];
colorsScale.colors.forEach((_color, i) => {
if (i === 0) {
thresholdArray.push(min);
@@ -36,15 +55,15 @@ export const colorShapes = (geoJson, values, colorsScale, emptyValueColor) => {
scale = chroma.scale([colorMin, colorMax]).domain([min, max]);
}
- const dataMapping = {};
+ const dataMapping: { [key: ChoroplethDataValue['x']]: ChoroplethDataValue['y'] } = {};
values.forEach((v) => {
dataMapping[v.x] = v.y;
});
// Iterate shapes, compute color from matching value
const coloredFeatures = geoJson.features.map((feature) => {
- const shapeMapping = feature.properties.key;
- const value = dataMapping[shapeMapping]; // FIXME: beware of int/string differences in keys
+ const shapeMapping: string = feature.properties?.key;
+ const value: number = dataMapping[shapeMapping]; // FIXME: beware of int/string differences in keys
const color = value ? scale(value).hex() : emptyValueColor;
return {
@@ -67,31 +86,14 @@ export const colorShapes = (geoJson, values, colorsScale, emptyValueColor) => {
};
};
-export const mapKeyToColor = (values, colorScale) => {
- const rawValues = values.map((v) => v.y);
- const min = Math.min(...rawValues);
- const max = Math.max(...rawValues);
-
- const colorMin = chroma(colorScale).darken(4).hex();
- const colorMax = chroma(colorScale).brighten(4).hex();
-
- const scale = chroma.scale([colorMin, colorMax]).domain([min, max]);
-
- const mapping = {};
-
- values.forEach((v) => {
- mapping[v.x] = scale(v.y).hex();
- });
-
- return mapping;
-};
-
// This is a default bound that will be extended
-const VOID_BOUNDS = [180, 90, -180, -90];
+const VOID_BOUNDS: BBox = [180, 90, -180, -90];
-function computeBboxFromCoords(coordsPath, bbox) {
- return coordsPath.reduce(
- (current, coords) => [
+type CoordsPath = Position[];
+
+function computeBboxFromCoords(coordsPath: CoordsPath, bbox: BBox): BBox {
+ return coordsPath.reduce(
+ (current: BBox, coords: Position) => [
Math.min(coords[0], current[0]),
Math.min(coords[1], current[1]),
Math.max(coords[0], current[2]),
@@ -103,8 +105,12 @@ function computeBboxFromCoords(coordsPath, bbox) {
// The features given by querySourceFeatures are cut based on a tile representation
// but we need the bounding box of the features themselves, so we need to build them again
-function mergeBboxFromFeaturesWithSameKey(features) {
- const mergedBboxes = {};
+function mergeBboxFromFeaturesWithSameKey(features: Feature[]) {
+ const mergedBboxes: {
+ [key: string]: {
+ bbox: BBox;
+ };
+ } = {};
features.forEach((feature) => {
// FIXME: supports only shapes for now
if (feature.geometry.type === 'Polygon') {
@@ -113,14 +119,14 @@ function mergeBboxFromFeaturesWithSameKey(features) {
feature.geometry.coordinates.forEach((coordsPath) => {
bbox = computeBboxFromCoords(coordsPath, bbox);
});
- const id = feature.properties.key;
+ const id: string = feature.properties?.key;
if (!mergedBboxes[id]) {
mergedBboxes[id] = {
bbox,
};
} else {
const storedBbox = mergedBboxes[id].bbox;
- const mergedBbox = [
+ const mergedBbox: BBox = [
Math.min(bbox[0], storedBbox[0]),
Math.min(bbox[1], storedBbox[1]),
Math.max(bbox[2], storedBbox[2]),
@@ -137,10 +143,13 @@ function mergeBboxFromFeaturesWithSameKey(features) {
}
// We're calculating the maximum zoom required to fit the smallest feature we're displaying, to prevent people from zooming "too far" by accident
-export const computeMaxZoomFromGeoJsonFeatures = (mapContainer, features) => {
+export const computeMaxZoomFromGeoJsonFeatures = (
+ mapContainer: HTMLElement,
+ features: Feature[]
+): number => {
let maxZoom = 0; // maxZoom lowest value possible
const filteredBboxes = mergeBboxFromFeaturesWithSameKey(features);
- Object.values(filteredBboxes).forEach((value) => {
+ Object.values(filteredBboxes).forEach((value: any) => {
// Vtiles = 512 tilesize
maxZoom = Math.max(
geoViewport.viewport(
@@ -157,16 +166,20 @@ export const computeMaxZoomFromGeoJsonFeatures = (mapContainer, features) => {
return maxZoom;
};
-const getShapeCenter = (feature) => {
+const getShapeCenter = (feature: Feature) => {
const featureBbox = turfBbox(feature.geometry);
const centerLatitude = (featureBbox[1] + featureBbox[3]) / 2;
const centerLongitude = (featureBbox[0] + featureBbox[2]) / 2;
return [centerLongitude, centerLatitude];
};
-export const getFixedTooltips = (shapeKeys, features, renderTooltip) => {
+export const getFixedTooltips = (
+ shapeKeys: string[],
+ features: Feature[],
+ renderTooltip: MapRenderTooltipFunction
+): ChoroplethFixedTooltipDescription[] => {
const popups = shapeKeys.map((shapeKey) => {
- const matchedFeature = features.find((feature) => feature.properties.key === shapeKey);
+ const matchedFeature = features.find((feature) => feature.properties?.key === shapeKey);
if (matchedFeature) {
const center = getShapeCenter(matchedFeature);
const description = renderTooltip(matchedFeature);
@@ -180,5 +193,7 @@ export const getFixedTooltips = (shapeKeys, features, renderTooltip) => {
return null;
});
- return popups;
+ return popups.filter((item): item is NonNullable =>
+ Boolean(item)
+ ) as ChoroplethFixedTooltipDescription[];
};
diff --git a/packages/visualizations/src/components/types.ts b/packages/visualizations/src/components/types.ts
index dd8f593a..681ba925 100644
--- a/packages/visualizations/src/components/types.ts
+++ b/packages/visualizations/src/components/types.ts
@@ -234,8 +234,6 @@ export interface KpiCardOptions {
format?: (value: number) => string;
}
-export interface ChoroplethOptions {}
-
export interface DataBounds {
min: number;
max: number;
diff --git a/packages/visualizations/src/components/utils/ColorsLegend.svelte b/packages/visualizations/src/components/utils/ColorsLegend.svelte
index 388a0652..ee727cea 100644
--- a/packages/visualizations/src/components/utils/ColorsLegend.svelte
+++ b/packages/visualizations/src/components/utils/ColorsLegend.svelte
@@ -10,7 +10,7 @@
export let dataBounds: DataBounds;
export let colorsScale: ColorsScale;
export let variant: LegendVariant;
- export let title: string;
+ export let title: string | undefined;
// the part below is related to labels rotation
let legendWidth: number;
diff --git a/packages/visualizations/src/index.ts b/packages/visualizations/src/index.ts
index 510b3d7e..95e7d3ab 100644
--- a/packages/visualizations/src/index.ts
+++ b/packages/visualizations/src/index.ts
@@ -4,3 +4,4 @@ export { default as KpiCard } from './components/KpiCard';
export { default as Choropleth } from './components/Map';
export * from './types';
export * from './components/types';
+export * from './components/Map/types';