From 023beb349fb518e0142721c808454a288be5ec77 Mon Sep 17 00:00:00 2001 From: Benoit Richter Date: Tue, 2 Aug 2022 13:56:47 +0200 Subject: [PATCH 1/7] Rewrite all map-related code to TypeScript --- packages/visualizations/package-lock.json | 41 ++++-- packages/visualizations/package.json | 3 + .../src/components/Map/Choropleth.svelte | 82 ++++++------ .../src/components/Map/MapRender.svelte | 118 ++++++++++-------- .../src/components/Map/index.ts | 3 +- .../src/components/Map/types.ts | 55 ++++++++ .../src/components/Map/{utils.js => utils.ts} | 93 +++++++------- .../visualizations/src/components/types.ts | 2 - .../src/components/utils/ColorsLegend.svelte | 2 +- packages/visualizations/src/index.ts | 1 + 10 files changed, 259 insertions(+), 141 deletions(-) create mode 100644 packages/visualizations/src/components/Map/types.ts rename packages/visualizations/src/components/Map/{utils.js => utils.ts} (71%) 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..7dc4b082 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..c7dcc429 --- /dev/null +++ b/packages/visualizations/src/components/Map/types.ts @@ -0,0 +1,55 @@ +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; +} + +export type ChoroplethTooltipFormatter = ({ + value, + label, + key, +}: { + value?: number; + label: string; + key?: string; +}) => string; + +export interface ChoroplethDataValue { + x: string; + y: number; +} + +export interface ChoroplethShapeGeoJsonValue { + type: 'geojson'; + geoJson: FeatureCollection | null; +} + +export interface ChoroplethShapeVectorTilesValue { + type: 'vtiles'; + url: string; +} + +export type ChoroplethShapeValue = ChoroplethShapeGeoJsonValue | ChoroplethShapeVectorTilesValue; + +export interface ChoroplethFixedTooltipDescription { + center: Position; + description: string; + popup: Popup; +} + +export type ChoroplethRenderTooltipFunction = (f: Feature) => string; + +export type ChoroplethLayer = Omit; diff --git a/packages/visualizations/src/components/Map/utils.js b/packages/visualizations/src/components/Map/utils.ts similarity index 71% rename from packages/visualizations/src/components/Map/utils.js rename to packages/visualizations/src/components/Map/utils.ts index 7bbb8b4f..2c616410 100644 --- a/packages/visualizations/src/components/Map/utils.js +++ b/packages/visualizations/src/components/Map/utils.ts @@ -2,24 +2,37 @@ 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, + ChoroplethRenderTooltipFunction, +} 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 +) => { // 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 = []; - colorsScale.colors.forEach((_color, i) => { + const thresholdArray: number[] = []; + colorsScale.colors.forEach((_color, i: number) => { if (i === 0) { thresholdArray.push(min); thresholdArray.push(min + (max - min) / colorsScale.colors.length); @@ -36,15 +49,15 @@ export const colorShapes = (geoJson, values, colorsScale, emptyValueColor) => { scale = chroma.scale([colorMin, colorMax]).domain([min, max]); } - const dataMapping = {}; + const dataMapping: any = {}; 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 +80,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]; + +type CoordsPath = Position[]; -function computeBboxFromCoords(coordsPath, bbox) { - return coordsPath.reduce( - (current, coords) => [ +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 +99,8 @@ 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: any = {}; features.forEach((feature) => { // FIXME: supports only shapes for now if (feature.geometry.type === 'Polygon') { @@ -113,7 +109,7 @@ 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, @@ -137,10 +133,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[] +) => { 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 +156,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: ChoroplethRenderTooltipFunction +): 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 +183,5 @@ export const getFixedTooltips = (shapeKeys, features, renderTooltip) => { return null; }); - return popups; + return popups.filter(Boolean) 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'; From d5361b5acf1bd0c8c5b793ead9530737568b1afe Mon Sep 17 00:00:00 2001 From: Benoit Richter Date: Thu, 4 Aug 2022 14:29:07 +0200 Subject: [PATCH 2/7] Document the less trivial types --- .../visualizations/src/components/Map/types.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/visualizations/src/components/Map/types.ts b/packages/visualizations/src/components/Map/types.ts index c7dcc429..d559765d 100644 --- a/packages/visualizations/src/components/Map/types.ts +++ b/packages/visualizations/src/components/Map/types.ts @@ -17,31 +17,45 @@ 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 { From 4ec004cae096ff25c5bb06f3bfce24ce014a0382 Mon Sep 17 00:00:00 2001 From: Benoit Richter Date: Thu, 4 Aug 2022 14:34:46 +0200 Subject: [PATCH 3/7] Keep generic types for MapRender, as it should be generic and not tied to Choropleth only --- .../src/components/Map/Choropleth.svelte | 4 ++-- .../src/components/Map/MapRender.svelte | 13 +++++++------ packages/visualizations/src/components/Map/types.ts | 4 +++- packages/visualizations/src/components/Map/utils.ts | 4 ++-- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/visualizations/src/components/Map/Choropleth.svelte b/packages/visualizations/src/components/Map/Choropleth.svelte index 7dc4b082..d5418cb2 100644 --- a/packages/visualizations/src/components/Map/Choropleth.svelte +++ b/packages/visualizations/src/components/Map/Choropleth.svelte @@ -13,7 +13,7 @@ ChoroplethDataValue, ChoroplethLayer, ChoroplethOptions, - ChoroplethRenderTooltipFunction, + MapRenderTooltipFunction, ChoroplethShapeValue, ChoroplethTooltipFormatter, MapLegend, @@ -36,7 +36,7 @@ const defaultEmptyValueColor = '#cccccc'; let aspectRatio: number; - let renderTooltip: ChoroplethRenderTooltipFunction; + let renderTooltip: MapRenderTooltipFunction; let bbox: BBox; let activeShapes: string[] | undefined; let interactive: boolean; diff --git a/packages/visualizations/src/components/Map/MapRender.svelte b/packages/visualizations/src/components/Map/MapRender.svelte index ad59b0d7..818d6dd1 100644 --- a/packages/visualizations/src/components/Map/MapRender.svelte +++ b/packages/visualizations/src/components/Map/MapRender.svelte @@ -20,8 +20,8 @@ import type { ColorsScale, DataBounds, LegendVariant } from '../types'; import type { ChoroplethFixedTooltipDescription, - ChoroplethLayer, - ChoroplethRenderTooltipFunction, + MapLayer, + MapRenderTooltipFunction, MapLegend, } from './types'; @@ -30,7 +30,7 @@ // maplibre source config export let source: SourceSpecification; // maplibre layer config - export let layer: ChoroplethLayer; + export let layer: MapLayer; // bounding box to start from, and restrict to it export let bbox: BBox; // option to disable map interactions @@ -40,7 +40,7 @@ export let colorsScale: ColorsScale; export let dataBounds: DataBounds; // Used to render tooltips on hover - export let renderTooltip: ChoroplethRenderTooltipFunction; + export let renderTooltip: MapRenderTooltipFunction; // Used to select shapes to activate a tooltip on render export let activeShapes: string[] | undefined; // aspect ratio based on width, by default equal to 1 @@ -51,6 +51,7 @@ $: legendVariant = clientWidth <= 375 ? 'fluid' : 'fixed'; // Used to store fixed tooltips displayed on render + // FIXME: This may not be useful anymore, and is very tied to Choropleth right now let fixedPopupsList: ChoroplethFixedTooltipDescription[] = []; $: cssVarStyles = `--aspect-ratio:${aspectRatio};`; @@ -166,7 +167,7 @@ function handleInteractivity( isInteractive: boolean, - tooltipRenderer?: ChoroplethRenderTooltipFunction + tooltipRenderer?: MapRenderTooltipFunction ) { if (isInteractive) { // Enable all user interaction handlers @@ -219,7 +220,7 @@ } } - function updateSourceAndLayer(newSource: SourceSpecification, newLayer: ChoroplethLayer) { + function updateSourceAndLayer(newSource: SourceSpecification, newLayer: MapLayer) { if (newSource && newLayer) { if (map.getLayer(layerId)) { map.removeLayer(layerId); diff --git a/packages/visualizations/src/components/Map/types.ts b/packages/visualizations/src/components/Map/types.ts index d559765d..f3034b2b 100644 --- a/packages/visualizations/src/components/Map/types.ts +++ b/packages/visualizations/src/components/Map/types.ts @@ -64,6 +64,8 @@ export interface ChoroplethFixedTooltipDescription { popup: Popup; } -export type ChoroplethRenderTooltipFunction = (f: Feature) => string; +export type MapRenderTooltipFunction = (f: Feature) => string; export type ChoroplethLayer = Omit; + +export type MapLayer = ChoroplethLayer; diff --git a/packages/visualizations/src/components/Map/utils.ts b/packages/visualizations/src/components/Map/utils.ts index 2c616410..1451e91f 100644 --- a/packages/visualizations/src/components/Map/utils.ts +++ b/packages/visualizations/src/components/Map/utils.ts @@ -8,7 +8,7 @@ import type { Color, ColorsScale } from '../types'; import type { ChoroplethDataValue, ChoroplethFixedTooltipDescription, - ChoroplethRenderTooltipFunction, + MapRenderTooltipFunction, } from './types'; export const LIGHT_GREY: Color = '#CBD2DB'; @@ -166,7 +166,7 @@ const getShapeCenter = (feature: Feature) => { export const getFixedTooltips = ( shapeKeys: string[], features: Feature[], - renderTooltip: ChoroplethRenderTooltipFunction + renderTooltip: MapRenderTooltipFunction ): ChoroplethFixedTooltipDescription[] => { const popups = shapeKeys.map((shapeKey) => { const matchedFeature = features.find((feature) => feature.properties?.key === shapeKey); From 03197e9d1deae3e66bdf216a019a18774327b044 Mon Sep 17 00:00:00 2001 From: Benoit Richter Date: Fri, 19 Aug 2022 09:58:21 +0200 Subject: [PATCH 4/7] Improved types after PR review --- .../visualizations/src/components/Map/utils.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/visualizations/src/components/Map/utils.ts b/packages/visualizations/src/components/Map/utils.ts index 1451e91f..c2c52562 100644 --- a/packages/visualizations/src/components/Map/utils.ts +++ b/packages/visualizations/src/components/Map/utils.ts @@ -32,7 +32,7 @@ export const colorShapes = ( if (colorsScale?.type === 'palette') { const thresholdArray: number[] = []; - colorsScale.colors.forEach((_color, i: number) => { + colorsScale.colors.forEach((_color, i) => { if (i === 0) { thresholdArray.push(min); thresholdArray.push(min + (max - min) / colorsScale.colors.length); @@ -49,7 +49,7 @@ export const colorShapes = ( scale = chroma.scale([colorMin, colorMax]).domain([min, max]); } - const dataMapping: any = {}; + const dataMapping: { [key: ChoroplethDataValue['x']]: ChoroplethDataValue['y'] } = {}; values.forEach((v) => { dataMapping[v.x] = v.y; }); @@ -100,7 +100,11 @@ function computeBboxFromCoords(coordsPath: CoordsPath, bbox: BBox): 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: Feature[]) { - const mergedBboxes: any = {}; + const mergedBboxes: { + [key: string]: { + bbox: BBox + } + } = {}; features.forEach((feature) => { // FIXME: supports only shapes for now if (feature.geometry.type === 'Polygon') { @@ -116,7 +120,7 @@ function mergeBboxFromFeaturesWithSameKey(features: Feature[]) { }; } 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]), @@ -183,5 +187,5 @@ export const getFixedTooltips = ( return null; }); - return popups.filter(Boolean) as ChoroplethFixedTooltipDescription[]; + return popups.filter((item): item is NonNullable => Boolean(item)) as ChoroplethFixedTooltipDescription[]; }; From 2a0a57eee58849732c59e5008153f643d71b5745 Mon Sep 17 00:00:00 2001 From: Benoit Richter Date: Fri, 19 Aug 2022 10:02:39 +0200 Subject: [PATCH 5/7] Linting --- packages/visualizations/src/components/Map/utils.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/visualizations/src/components/Map/utils.ts b/packages/visualizations/src/components/Map/utils.ts index c2c52562..90b0a913 100644 --- a/packages/visualizations/src/components/Map/utils.ts +++ b/packages/visualizations/src/components/Map/utils.ts @@ -102,8 +102,8 @@ function computeBboxFromCoords(coordsPath: CoordsPath, bbox: BBox): BBox { function mergeBboxFromFeaturesWithSameKey(features: Feature[]) { const mergedBboxes: { [key: string]: { - bbox: BBox - } + bbox: BBox; + }; } = {}; features.forEach((feature) => { // FIXME: supports only shapes for now @@ -187,5 +187,7 @@ export const getFixedTooltips = ( return null; }); - return popups.filter((item): item is NonNullable => Boolean(item)) as ChoroplethFixedTooltipDescription[]; + return popups.filter((item): item is NonNullable => + Boolean(item) + ) as ChoroplethFixedTooltipDescription[]; }; From 96d7281d2957c5e54bb359c55b00429ff8362e64 Mon Sep 17 00:00:00 2001 From: Benoit Richter Date: Fri, 19 Aug 2022 15:49:55 +0200 Subject: [PATCH 6/7] Improve typing after PR review --- .../visualizations/src/components/Map/MapRender.svelte | 8 +++++--- packages/visualizations/src/components/Map/utils.ts | 10 ++++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/visualizations/src/components/Map/MapRender.svelte b/packages/visualizations/src/components/Map/MapRender.svelte index 818d6dd1..2015033d 100644 --- a/packages/visualizations/src/components/Map/MapRender.svelte +++ b/packages/visualizations/src/components/Map/MapRender.svelte @@ -8,7 +8,7 @@ NavigationControl, LngLatBoundsLike, MapSourceDataEvent, - MapMouseEvent, + MapLayerMouseEvent, LngLatLike, } from 'maplibre-gl'; import { onMount } from 'svelte'; @@ -129,7 +129,9 @@ function sourceLoadingCallback(e: MapSourceDataEvent) { // sourceDataType can be "visibility" or "metadata", in which case it's not about the data itself if (e.isSourceLoaded && e.sourceId === sourceId && !e.sourceDataType) { - // @ts-ignore // The type forces you to pass a filter parameter in the option, but it's not required by the real code + // The type forces you to pass a filter parameter in the option, but it's not required by the real code + // https://github.com/maplibre/maplibre-gl-js/issues/1393 + // @ts-ignore const renderedFeatures = map.querySourceFeatures(sourceId, { sourceLayer: layerId }); if (renderedFeatures.length) { @@ -151,7 +153,7 @@ } } - function addTooltip(e: MapMouseEvent) { + function addTooltip(e: MapLayerMouseEvent) { // @ts-ignore // Somehow `features` isn't part of the type, but exists in the object at runtime const description = renderTooltip(e.features[0]); if (hoverPopup.isOpen()) { diff --git a/packages/visualizations/src/components/Map/utils.ts b/packages/visualizations/src/components/Map/utils.ts index 90b0a913..9bf7f1ae 100644 --- a/packages/visualizations/src/components/Map/utils.ts +++ b/packages/visualizations/src/components/Map/utils.ts @@ -19,7 +19,13 @@ export const colorShapes = ( 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 @@ -140,7 +146,7 @@ function mergeBboxFromFeaturesWithSameKey(features: Feature[]) { export const computeMaxZoomFromGeoJsonFeatures = ( mapContainer: HTMLElement, features: Feature[] -) => { +): number => { let maxZoom = 0; // maxZoom lowest value possible const filteredBboxes = mergeBboxFromFeaturesWithSameKey(features); Object.values(filteredBboxes).forEach((value: any) => { From 20727d3d1480fa6113464d7592f370e7d4f53d5a Mon Sep 17 00:00:00 2001 From: Benoit Richter Date: Fri, 19 Aug 2022 17:07:55 +0200 Subject: [PATCH 7/7] Replace ts-ignore with a simple value check --- .../src/components/Map/MapRender.svelte | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/visualizations/src/components/Map/MapRender.svelte b/packages/visualizations/src/components/Map/MapRender.svelte index 2015033d..32ad16d9 100644 --- a/packages/visualizations/src/components/Map/MapRender.svelte +++ b/packages/visualizations/src/components/Map/MapRender.svelte @@ -154,12 +154,13 @@ } function addTooltip(e: MapLayerMouseEvent) { - // @ts-ignore // Somehow `features` isn't part of the type, but exists in the object at runtime - const description = renderTooltip(e.features[0]); - if (hoverPopup.isOpen()) { - hoverPopup.setLngLat(e.lngLat).setHTML(description); - } else { - hoverPopup.setLngLat(e.lngLat).setHTML(description).addTo(map); + if (e.features) { + const description = renderTooltip(e.features[0]); + if (hoverPopup.isOpen()) { + hoverPopup.setLngLat(e.lngLat).setHTML(description); + } else { + hoverPopup.setLngLat(e.lngLat).setHTML(description).addTo(map); + } } }