@geo2france/api-dashboard 1.9.1 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,6 +24,7 @@ type dataset = {
24
24
  isFetching: boolean;
25
25
  isError: boolean;
26
26
  producers?: any[];
27
+ geojson?: any;
27
28
  };
28
29
  type ControlContextType = {
29
30
  values: Record<string, any>;
@@ -86,7 +86,7 @@ export const DSL_Dataset = ({ children, id, provider: provider_input, type: prov
86
86
  useEffect(() => {
87
87
  const finalData = data?.data && transformers.reduce((datat, fn) => fn(datat), data.data);
88
88
  if (datasetRegistryContext) {
89
- datasetRegistryContext({ id: id, resource: resource, data: finalData, isFetching: isFetching, isError: isError, producers: producers });
89
+ datasetRegistryContext({ id: id, resource: resource, data: finalData, isFetching: isFetching, isError: isError, producers: producers, geojson: data?.geojson });
90
90
  //Ajouter une info pour distinguer les erreurs du fourniseurs et celles des transformers ?
91
91
  }
92
92
  }, [resource, data, isFetching, someFetching, children]);
@@ -5,6 +5,7 @@ export declare const useDataset: (dataset_id?: string) => {
5
5
  isFetching: boolean;
6
6
  isError: boolean;
7
7
  producers?: any[];
8
+ geojson?: any;
8
9
  } | undefined;
9
10
  export declare const useDatasets: (dataset_ids?: string[]) => {
10
11
  id: string;
@@ -13,6 +14,7 @@ export declare const useDatasets: (dataset_ids?: string[]) => {
13
14
  isFetching: boolean;
14
15
  isError: boolean;
15
16
  producers?: any[];
17
+ geojson?: any;
16
18
  }[] | undefined;
17
19
  export declare const useAllDatasets: () => {
18
20
  id: string;
@@ -21,4 +23,5 @@ export declare const useAllDatasets: () => {
21
23
  isFetching: boolean;
22
24
  isError: boolean;
23
25
  producers?: any[];
26
+ geojson?: any;
24
27
  }[] | undefined;
@@ -0,0 +1,59 @@
1
+ import type { AnyLayer } from 'react-map-gl/maplibre';
2
+ import { AnyPaint } from 'mapbox-gl';
3
+ import React from 'react';
4
+ import 'maplibre-gl/dist/maplibre-gl.css';
5
+ type LayerType = AnyLayer["type"];
6
+ export declare const map_locale: {
7
+ 'CooperativeGesturesHandler.WindowsHelpText': string;
8
+ 'CooperativeGesturesHandler.MacHelpText': string;
9
+ 'CooperativeGesturesHandler.MobileHelpText': string;
10
+ };
11
+ /**
12
+ * Une carto simple avec un layer
13
+ *
14
+ * */
15
+ interface MapProps extends MapLayerProps {
16
+ /** Afficher une popup après un click sur la carte */
17
+ popup?: boolean;
18
+ /** Titre du graphique */
19
+ title?: string;
20
+ }
21
+ export declare const Map: React.FC<MapProps>;
22
+ interface MapLayerProps {
23
+ /** Identifiant du jeu de données */
24
+ dataset: string;
25
+ /** Couleur des symboles */
26
+ color?: string;
27
+ /** Layer Type */
28
+ type?: LayerType;
29
+ /** Les paint properties de maplibre cf. https://maplibre.org/maplibre-style-spec/layers/#paint */
30
+ paint?: AnyPaint;
31
+ /** Colonne contenant la variable à représenter */
32
+ categoryKey?: string;
33
+ /** Colonne contenant la coordonnée x / longitude */
34
+ xKey?: string;
35
+ /** Colonne contenant la coordonnées y / latitude */
36
+ yKey?: string;
37
+ }
38
+ /**
39
+ * Composant à utiliser comme enfant d'une <Map>
40
+ * Ajoute une couche (layer) à partir d'un dataset
41
+ *
42
+ * @param { MapLayerProps } props
43
+ * @returns { ReactElement }
44
+ */
45
+ export declare const MapLayer: React.FC<MapLayerProps>;
46
+ export interface IMapBaseLayerProps {
47
+ layer: 'osm' | 'ortho';
48
+ tileSize?: number;
49
+ }
50
+ /**
51
+ * Composant à utiliser comme enfant d'une <Map>
52
+ * Permet d'ajouter un fond de plan (OSM ou orthophoto)
53
+ * devnote : couches spécifiques Hauts-de-France
54
+ *
55
+ * @param { IMapBaseLayerProps } props
56
+ * @returns { ReactElement }
57
+ */
58
+ export declare const BaseLayer: React.FC<IMapBaseLayerProps>;
59
+ export {};
@@ -0,0 +1,147 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // Composant carto
3
+ import Maplibre, { Layer, Source, useMap, Popup } from 'react-map-gl/maplibre';
4
+ import { useEffect, useRef, useState } from "react";
5
+ import { useDataset } from '../Dataset/hooks';
6
+ import bbox from '@turf/bbox';
7
+ import { getType } from '@turf/invariant';
8
+ import { featureCollection, point } from '@turf/helpers';
9
+ import { usePalette } from '../Palette/Palette';
10
+ import { from, op } from 'arquero';
11
+ import 'maplibre-gl/dist/maplibre-gl.css';
12
+ import { LegendControl } from '../MapLegend/MapLegend';
13
+ import { useBlockConfig } from '../DashboardPage/Block';
14
+ import { parseNumber } from '../../utils/parsers';
15
+ export const map_locale = {
16
+ 'CooperativeGesturesHandler.WindowsHelpText': 'Utilisez Ctrl + molette pour zommer sur la carte.',
17
+ 'CooperativeGesturesHandler.MacHelpText': 'Utilisez ⌘ + molette pour zommer sur la carte.',
18
+ 'CooperativeGesturesHandler.MobileHelpText': 'Utilisez deux doights pour déplacer la carte.',
19
+ };
20
+ /** Construire un geojson a partir d'un tableau de données*/
21
+ const build_geojson = (params) => {
22
+ const { data, xKey, yKey } = params;
23
+ const features_collection = featureCollection(data.map((e) => {
24
+ const [x, y] = [parseNumber(e[xKey]), parseNumber(e[yKey])];
25
+ return point([x, y], { ...e });
26
+ }));
27
+ return features_collection;
28
+ };
29
+ export const Map = ({ dataset, color, type, paint, categoryKey, popup = false, title, xKey, yKey }) => {
30
+ const mapRef = useRef(null);
31
+ const [clickedFeature, setClickedFeature] = useState(undefined);
32
+ useBlockConfig({ title: title });
33
+ const onClickMap = (evt) => {
34
+ setClickedFeature({ ...evt.features[0], ...{ lngLat: evt.lngLat } });
35
+ };
36
+ const onMouseMoveMap = (evt) => {
37
+ if (!mapRef.current) {
38
+ return;
39
+ }
40
+ if (evt?.features.length > 0 && popup) {
41
+ mapRef.current.getCanvasContainer().style.cursor = 'pointer';
42
+ }
43
+ else {
44
+ mapRef.current.getCanvasContainer().style.cursor = 'grab';
45
+ }
46
+ };
47
+ return (_jsxs(Maplibre, { cooperativeGestures: true, locale: map_locale, ref: mapRef, interactiveLayerIds: [dataset], onClick: onClickMap, onMouseMove: onMouseMoveMap, style: { width: '100%', height: '500px' }, children: [_jsx(BaseLayer, { layer: "osm" }), _jsx(MapLayer, { dataset: dataset, color: color, type: type, paint: paint, categoryKey: categoryKey, xKey: xKey, yKey: yKey }), clickedFeature?.properties && categoryKey && popup &&
48
+ _jsxs(Popup, { longitude: clickedFeature.lngLat.lng, latitude: clickedFeature.lngLat.lat, onClose: () => { setClickedFeature(null); }, children: [_jsxs("div", { children: [" ", clickedFeature.properties[categoryKey], " "] }), " "] })] }));
49
+ };
50
+ /**
51
+ * Composant à utiliser comme enfant d'une <Map>
52
+ * Ajoute une couche (layer) à partir d'un dataset
53
+ *
54
+ * @param { MapLayerProps } props
55
+ * @returns { ReactElement }
56
+ */
57
+ export const MapLayer = ({ dataset, categoryKey, color = 'red', type = 'circle', paint, xKey, yKey }) => {
58
+ const { current: map } = useMap();
59
+ const data = useDataset(dataset);
60
+ // src (lib proj4 pour convertir)
61
+ // Si x et y sont definie, on construit le geojson
62
+ const geojson = xKey && yKey && data?.data ?
63
+ build_geojson({ data: data.data, xKey: xKey, yKey: yKey })
64
+ : data?.geojson; // Sinon on utilise le geojson (fournisseur wfs)
65
+ const geom_type = geojson?.features?.[0] && getType(geojson?.features?.[0]);
66
+ /** Type de données dans categoryKey (string ou number) */
67
+ const type_value = categoryKey && typeof (data?.data?.[0]?.[categoryKey]);
68
+ /** Valeurs distinctes (si type string) */
69
+ const values = (type_value === 'string') && categoryKey && data?.data && from(data?.data).rollup({ a: op.array_agg_distinct(categoryKey) }).get('a', 0) || undefined;
70
+ /** Couleurs de la palette */
71
+ const colors = usePalette({ nColors: Array.isArray(values) ? values?.length : 1 });
72
+ const match = Array.isArray(values) ? values?.map((v, i) => ({
73
+ val: v,
74
+ color: colors?.[i],
75
+ })) : undefined;
76
+ /** Expression mapLibre qui permet de mapper les valeurs et les couleurs de la palette */
77
+ const expression = match && categoryKey
78
+ ? [
79
+ "match",
80
+ ["get", categoryKey],
81
+ ...match?.flatMap((s) => [s.val, s.color]),
82
+ "purple" // fallback
83
+ ]
84
+ : undefined;
85
+ const legendItems = match?.map((e) => ({ color: e.color, label: e.val })).sort((a, b) => a.label.localeCompare(b.label)) || [];
86
+ const layers = [];
87
+ /** POINT */
88
+ if (geom_type === 'Point' || geom_type === 'MultiPoint') {
89
+ const default_paint = { "circle-color": expression ?? color ?? colors[0] };
90
+ type = 'circle';
91
+ layers.push(_jsx(Layer, { id: dataset, type: "circle", paint: (paint ?? default_paint) }, dataset));
92
+ }
93
+ /** POLYGON */
94
+ else if (geom_type === 'Polygon' || geom_type === 'MultiPolygon') {
95
+ const default_paint = { "fill-color": expression ?? color ?? colors[0] };
96
+ type = 'fill';
97
+ layers.push(_jsx(Layer, { id: dataset, type: "fill", paint: (paint ?? default_paint) }, dataset));
98
+ layers.push(_jsx(Layer, { id: dataset + '_line', type: 'line', paint: { "line-width": 0.5, "line-color": '#fff' } }, dataset + '_line'));
99
+ }
100
+ /** LINESTRING */
101
+ else if (geom_type === 'LineString' || geom_type === 'MultiLineString') {
102
+ const default_paint = { "line-color": expression ?? color ?? colors[0] };
103
+ type = 'line';
104
+ layers.push(_jsx(Layer, { id: dataset, type: "line", paint: (paint ?? default_paint) }, dataset));
105
+ }
106
+ //devnote : regarder la colonne contenant les valeurs pour proposer une représentation (catégorie ou choroplèthe)
107
+ useEffect(() => {
108
+ if (geojson && geojson.features.length > 0) { // do not fitbound if no features
109
+ const box = bbox(geojson).slice(0, 4);
110
+ map?.fitBounds(box, { padding: 20 });
111
+ }
112
+ }, [geojson, map]);
113
+ return (_jsxs(_Fragment, { children: [geojson &&
114
+ _jsx(Source, { type: "geojson", data: geojson, children: layers }), _jsx(LegendControl, { items: legendItems })] }));
115
+ };
116
+ /**
117
+ * Composant à utiliser comme enfant d'une <Map>
118
+ * Permet d'ajouter un fond de plan (OSM ou orthophoto)
119
+ * devnote : couches spécifiques Hauts-de-France
120
+ *
121
+ * @param { IMapBaseLayerProps } props
122
+ * @returns { ReactElement }
123
+ */
124
+ export const BaseLayer = ({ layer, tileSize = 256 }) => {
125
+ //TODO : ne pas utiliser par défaut le fond de plan geo2france ?
126
+ const t = (() => {
127
+ switch (layer) {
128
+ case 'osm':
129
+ return `https://osm.geo2france.fr/mapcache/?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image%2Fpng&TRANSPARENT=false&LAYERS=grey&TILED=true&WIDTH=${tileSize}&HEIGHT=${tileSize}&SRS=EPSG%3A3857&STYLES=&BBOX={bbox-epsg-3857}`;
130
+ case 'ortho':
131
+ return `https://www.geo2france.fr/geoserver/geo2france/ows/?bbox={bbox-epsg-3857}&format=image/png&service=WMS&version=1.3.0&request=GetMap&srs=EPSG:3857&transparent=true&width=${tileSize}&height=${tileSize}&layers=ortho_regionale_2018_rvb`;
132
+ }
133
+ })();
134
+ const source_raster = {
135
+ type: 'raster',
136
+ attribution: 'OpenStreetMap', //fixme
137
+ tiles: [
138
+ t
139
+ ],
140
+ tileSize: tileSize
141
+ };
142
+ const layer_raster = {
143
+ 'type': 'raster',
144
+ 'paint': {}
145
+ };
146
+ return (_jsx(Source, { ...source_raster, children: _jsx(Layer, { ...layer_raster }) }));
147
+ };
@@ -10,3 +10,9 @@ interface MapLegendProps {
10
10
  }
11
11
  declare const MapLegend: React.FC<MapLegendProps>;
12
12
  export default MapLegend;
13
+ interface LegendControlProps {
14
+ /** Elements de légende */
15
+ items: LegendItem[];
16
+ }
17
+ /** Un control pour Maplibre qui permet d'afficher une légende */
18
+ export declare const LegendControl: React.FC<LegendControlProps>;
@@ -1,4 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from "react";
3
+ import { createRoot } from "react-dom/client";
4
+ import { useControl } from "react-map-gl/maplibre";
2
5
  const default_style = {
3
6
  backgroundColor: 'rgba(256,256,256,0.8)',
4
7
  padding: '10px',
@@ -17,3 +20,29 @@ const MapLegend = ({ items, style }) => {
17
20
  } }), _jsx("span", { children: item.label })] }, index))) }));
18
21
  };
19
22
  export default MapLegend;
23
+ /** Un control pour Maplibre qui permet d'afficher une légende */
24
+ export const LegendControl = ({ items }) => {
25
+ const rootRef = useRef(null);
26
+ useControl(() => {
27
+ const container = document.createElement("div");
28
+ //container.className = "maplibregl-ctrl"; // pour hériter du style par défaut
29
+ const root = createRoot(container);
30
+ rootRef.current = root;
31
+ const control = {
32
+ onAdd: (_map) => {
33
+ root.render(_jsx(MapLegend, { items: items }));
34
+ return container;
35
+ },
36
+ onRemove: () => {
37
+ container.parentNode?.removeChild(container);
38
+ },
39
+ };
40
+ return control;
41
+ }, { position: "top-right" });
42
+ useEffect(() => {
43
+ if (rootRef.current) {
44
+ rootRef.current.render(_jsx(MapLegend, { items: items }));
45
+ }
46
+ }, [items]);
47
+ return null;
48
+ };
@@ -18,4 +18,5 @@ import { Join } from "../components/Dataset/Join";
18
18
  import { ChartEcharts } from "../components/Charts/ChartEcharts";
19
19
  import { useBlockConfig } from "../components/DashboardPage/Block";
20
20
  import { Statistics, StatisticsCollection } from "../components/Charts/Statistics";
21
- export { Dashboard, Dataset, Provider, Transform, Join, Filter, DataPreview, ChartEcharts, ChartPie, ChartYearSerie, Statistics, StatisticsCollection, useDataset, useDatasets, useAllDatasets, useBlockConfig, Producer, Control, useControl, useAllControls, Radio, Select, Input, Palette, usePalette, PalettePreview, Debug, };
21
+ import { MapLayer, Map } from "../components/Map/Map";
22
+ export { Dashboard, Dataset, Provider, Transform, Join, Filter, DataPreview, ChartEcharts, ChartPie, ChartYearSerie, Statistics, StatisticsCollection, useDataset, useDatasets, useAllDatasets, useBlockConfig, Producer, Control, useControl, useAllControls, Radio, Select, Input, Palette, usePalette, PalettePreview, Debug, Map, MapLayer };
package/dist/dsl/index.js CHANGED
@@ -18,4 +18,5 @@ import { Join } from "../components/Dataset/Join";
18
18
  import { ChartEcharts } from "../components/Charts/ChartEcharts";
19
19
  import { useBlockConfig } from "../components/DashboardPage/Block";
20
20
  import { Statistics, StatisticsCollection } from "../components/Charts/Statistics";
21
- export { Dashboard, Dataset, Provider, Transform, Join, Filter, DataPreview, ChartEcharts, ChartPie, ChartYearSerie, Statistics, StatisticsCollection, useDataset, useDatasets, useAllDatasets, useBlockConfig, Producer, Control, useControl, useAllControls, Radio, Select, Input, Palette, usePalette, PalettePreview, Debug, };
21
+ import { MapLayer, Map } from "../components/Map/Map";
22
+ export { Dashboard, Dataset, Provider, Transform, Join, Filter, DataPreview, ChartEcharts, ChartPie, ChartYearSerie, Statistics, StatisticsCollection, useDataset, useDatasets, useAllDatasets, useBlockConfig, Producer, Control, useControl, useAllControls, Radio, Select, Input, Palette, usePalette, PalettePreview, Debug, Map, MapLayer };
@@ -0,0 +1,2 @@
1
+ /** Parser automatiquement un nombre au format : 0.6, "0.6" ou "0,6" */
2
+ export declare const parseNumber: (value: string | number) => number;
@@ -0,0 +1,14 @@
1
+ /** Parser automatiquement un nombre au format : 0.6, "0.6" ou "0,6" */
2
+ export const parseNumber = (value) => {
3
+ if (typeof value === "number") {
4
+ return value;
5
+ }
6
+ if (typeof value === "string") {
7
+ const normalized = value.replace(",", ".");
8
+ const num = parseFloat(normalized);
9
+ if (!isNaN(num)) {
10
+ return num;
11
+ }
12
+ }
13
+ return NaN;
14
+ };
@@ -4,5 +4,8 @@ interface useMapControlProps {
4
4
  legendElement: React.ReactElement;
5
5
  position?: ControlPosition;
6
6
  }
7
+ /** A déprécier ? utiliser plutôt le composant LegendControl ?
8
+ * Uniquement pour legacy
9
+ */
7
10
  export declare const useMapControl: ({ mapRef, legendElement, position }: useMapControlProps) => void;
8
11
  export {};
@@ -1,6 +1,9 @@
1
1
  // Ajouter un control (element HTML) sur une carte
2
2
  import { useEffect } from "react";
3
3
  import { createRoot } from 'react-dom/client';
4
+ /** A déprécier ? utiliser plutôt le composant LegendControl ?
5
+ * Uniquement pour legacy
6
+ */
4
7
  export const useMapControl = ({ mapRef, legendElement, position = 'top-right' }) => {
5
8
  useEffect(() => {
6
9
  if (mapRef?.current) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geo2france/api-dashboard",
3
- "version": "1.9.1",
3
+ "version": "1.10.1",
4
4
  "private": false,
5
5
  "description": "Build dashboards with JSX/TSX",
6
6
  "main": "dist/index.js",
@@ -46,18 +46,22 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@iconify/react": "^6.0.1",
49
+ "@turf/bbox": "^7.2.0",
50
+ "@turf/helpers": "^7.2.0",
51
+ "@turf/invariant": "^7.2.0",
49
52
  "alasql": "^4.6.6",
50
53
  "arquero": "^8.0.3",
51
- "axios": "^1.7.4",
54
+ "axios": "^1.12.2",
52
55
  "chroma-js": "^3.1.2",
53
- "echarts": "^5.5.1",
54
- "echarts-for-react": "^3.0.2",
56
+ "echarts": "^6.0.0",
57
+ "echarts-for-react": "^3.0.4",
55
58
  "query-string": "~7.1.3",
56
59
  "react-helmet": "^6.1.0",
57
60
  "react-helmet-async": "^2.0.5",
58
61
  "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz"
59
62
  },
60
63
  "devDependencies": {
64
+ "@ant-design/icons": "^6.0.2",
61
65
  "@iconify/json": "^2.2.382",
62
66
  "@tanstack/react-query": "^5.51.11",
63
67
  "@testing-library/dom": "^10.4.0",
@@ -78,7 +82,9 @@
78
82
  "antd": "^5.24.3",
79
83
  "jest": "^29.7.0",
80
84
  "jest-environment-jsdom": "^29.7.0",
85
+ "maplibre-gl": "^4.7.1",
81
86
  "react": "^18.3.1",
87
+ "react-map-gl": "^7.1.9",
82
88
  "react-router-dom": "^6.25.1",
83
89
  "ts-jest": "^29.2.5",
84
90
  "ts-node": "^10.9.2",
@@ -88,11 +94,13 @@
88
94
  "vite-plugin-svgr": "^4.2.0"
89
95
  },
90
96
  "peerDependencies": {
97
+ "@ant-design/icons": "^6.0.2",
91
98
  "@tanstack/react-query": "^5.51.11",
92
99
  "antd": "^5.18.3",
100
+ "maplibre-gl": "^4.7.1",
93
101
  "react": "^18.3.1",
94
102
  "react-dom": "^18.3.1",
95
- "react-map-gl": "^7.1.7",
103
+ "react-map-gl": "^7.1.9",
96
104
  "react-router-dom": "^6.25.1"
97
105
  }
98
106
  }
@@ -1 +0,0 @@
1
- export declare const mapOperator: (operator: any) => string;
@@ -1,36 +0,0 @@
1
- export const mapOperator = (operator) => {
2
- switch (operator) {
3
- case "ne":
4
- return '<>';
5
- case "gte":
6
- return '>=';
7
- case "gt":
8
- return '>';
9
- case "lte":
10
- return `<=`;
11
- case "lt":
12
- return `<`;
13
- case "eq":
14
- return "=";
15
- case "contains":
16
- case "startswith":
17
- case "endswith":
18
- return "ilike";
19
- case "containss":
20
- case "startswiths":
21
- case "endswiths":
22
- return "like";
23
- case "ncontains":
24
- case "nstartswith":
25
- case "nendswith":
26
- return "not ilike";
27
- case "ncontainss":
28
- case "nstartswiths":
29
- case "nendswiths":
30
- return "not like";
31
- case "in":
32
- return operator;
33
- default:
34
- return "";
35
- }
36
- };
@@ -1,7 +0,0 @@
1
- import { SimpleRecord } from "../types";
2
- interface BlocConfig {
3
- title?: string;
4
- dataExport?: SimpleRecord[];
5
- }
6
- export declare const useBlockConfig: ({ title, dataExport }: BlocConfig) => void;
7
- export {};
@@ -1,9 +0,0 @@
1
- import { useContext, useEffect } from "react";
2
- import { ChartBlockContext } from "../components/DashboardPage/Block";
3
- export const useBlockConfig = ({ title, dataExport }) => {
4
- const blockContext = useContext(ChartBlockContext);
5
- useEffect(() => blockContext?.setConfig({
6
- title: title,
7
- dataExport: dataExport
8
- }), [title, dataExport]);
9
- };