diff --git a/packages/visualizations-react/stories/Poi/PoiMap.stories.tsx b/packages/visualizations-react/stories/Poi/PoiMap.stories.tsx index 18cf127d..ff545617 100644 --- a/packages/visualizations-react/stories/Poi/PoiMap.stories.tsx +++ b/packages/visualizations-react/stories/Poi/PoiMap.stories.tsx @@ -3,6 +3,7 @@ import { BBox } from 'geojson'; import { CATEGORY_ITEM_VARIANT, POPUP_DISPLAY } from '@opendatasoft/visualizations'; import { ComponentMeta, ComponentStory } from '@storybook/react'; import type { + CategoryItem, Layer, PoiMapOptions, PopupDisplayTypes, @@ -16,8 +17,8 @@ import sources from './sources'; const BASE_STYLE = 'https://demotiles.maplibre.org/style.json'; -const layer1: Layer = { - id: 'layer-001', +const citiesLayer: Layer = { + id: 'layer-cities', source: 'cities', type: 'circle', color: 'black', @@ -33,8 +34,8 @@ const layer1: Layer = { }, }; -const layer2: Layer = { - id: 'layer-002', +const battlesLayer: Layer = { + id: 'layer-battles', source: 'battles', type: 'symbol', iconImageId: 'battle-icon', @@ -53,7 +54,16 @@ const layer2: Layer = { }, }; -const layers = [layer1, layer2]; +const moselleLayer: Layer = { + id: 'layer-moselle', + source: 'moselle', + type: 'fill', + color: '#6E0F12', + borderColor: 'white', + opacity: 0.3, +}; + +const layers = [citiesLayer, battlesLayer, moselleLayer]; const citiesColorMatch = { key: 'key', @@ -80,7 +90,7 @@ const battleImageMatch = { const bbox: BBox = [-6.855469, 41.343825, 11.645508, 51.37178]; -const legendCitiesItems = [ +const legendCitiesItems: CategoryItem[] = [ { label: 'Paris', color: citiesColorMatch.colors.Paris, @@ -107,7 +117,7 @@ const legendCitiesItems = [ }, ]; -const legendbattleItems = [ +const legendbattleItems: CategoryItem[] = [ { variant: CATEGORY_ITEM_VARIANT.Image, label: 'Battle of Verdun', @@ -120,10 +130,19 @@ const legendbattleItems = [ }, ]; +const legendMoselleItems: CategoryItem[] = [ + { + variant: CATEGORY_ITEM_VARIANT.Box, + label: 'Moselle', + color: '#6E0F12', + borderColor: 'white', + }, +]; + const legend = { type: 'category' as const, title: 'French cities and famous battles', - items: [...legendCitiesItems, ...legendbattleItems], + items: [...legendCitiesItems, ...legendbattleItems, ...legendMoselleItems], align: 'start' as const, }; @@ -196,8 +215,8 @@ const PoiMapMatchExpressionArgs = { data: { value: { layers: [ - { ...layer1, colorMatch: citiesColorMatch }, - { ...layer2, iconImageMatch: battleImageMatch }, + { ...citiesLayer, colorMatch: citiesColorMatch }, + { ...battlesLayer, iconImageMatch: battleImageMatch }, ], sources, }, @@ -214,8 +233,9 @@ const PoiMapLegendStartArgs = { data: { value: { layers: [ - { ...layer1, colorMatch: citiesColorMatch }, - { ...layer2, iconImageMatch: battleImageMatch }, + { ...citiesLayer, colorMatch: citiesColorMatch }, + { ...battlesLayer, iconImageMatch: battleImageMatch }, + moselleLayer, ], sources, }, @@ -232,8 +252,9 @@ const PoiMapLegendCenterArgs = { data: { value: { layers: [ - { ...layer1, colorMatch: citiesColorMatch }, - { ...layer2, iconImageMatch: battleImageMatch }, + { ...citiesLayer, colorMatch: citiesColorMatch }, + { ...battlesLayer, iconImageMatch: battleImageMatch }, + moselleLayer, ], sources, }, @@ -250,8 +271,9 @@ const PoiMapMinMaxZoomsArgs = { data: { value: { layers: [ - { ...layer1, colorMatch: citiesColorMatch }, - { ...layer2, iconImageMatch: battleImageMatch }, + { ...citiesLayer, colorMatch: citiesColorMatch }, + { ...battlesLayer, iconImageMatch: battleImageMatch }, + moselleLayer, ], sources, }, @@ -273,8 +295,9 @@ const PoiMapCooperativeGesturesArgs = { data: { value: { layers: [ - { ...layer1, colorMatch: citiesColorMatch }, - { ...layer2, iconImageMatch: battleImageMatch }, + { ...citiesLayer, colorMatch: citiesColorMatch }, + { ...battlesLayer, iconImageMatch: battleImageMatch }, + moselleLayer, ], sources, }, @@ -319,12 +342,18 @@ const StudioResponsiveUsageTemplate = () => { value: { layers: [ { - ...layer1, - popup: { ...(layer1.popup as PopupLayer), display: popupDisplay }, + ...citiesLayer, + popup: { + ...(citiesLayer.popup as PopupLayer), + display: popupDisplay, + }, }, { - ...layer2, - popup: { ...(layer2.popup as PopupLayer), display: popupDisplay }, + ...battlesLayer, + popup: { + ...(battlesLayer.popup as PopupLayer), + display: popupDisplay, + }, }, ], sources, diff --git a/packages/visualizations-react/stories/Poi/sources.ts b/packages/visualizations-react/stories/Poi/sources.ts index 22de7068..92cfe080 100644 --- a/packages/visualizations-react/stories/Poi/sources.ts +++ b/packages/visualizations-react/stories/Poi/sources.ts @@ -104,6 +104,10 @@ const sources : Required["value"]["sources"] = { ] } + }, + moselle: { + type: 'geojson', + data: 'https://france-geojson.gregoiredavid.fr/repo/departements/57-moselle/departement-57-moselle.geojson' } }; diff --git a/packages/visualizations-react/stories/Table/data.ts b/packages/visualizations-react/stories/Table/data.ts index 527627f3..814c7738 100644 --- a/packages/visualizations-react/stories/Table/data.ts +++ b/packages/visualizations-react/stories/Table/data.ts @@ -9,6 +9,7 @@ export default [ readingTime: 5.5, url: 'https://example.com/lorem-ipsum', geopoint: [2.357573,48.837904], + geoshape: 'centre-val-de-loire', }, { title: 'pellentesque nec blog post', @@ -21,6 +22,7 @@ export default [ readingTime: 3.8, url: 'https://example.com/pellentesque-nec', geopoint: [2.357573,48.837904], + geoshape: 'bretagne', }, { title: 'fusce sit amet blog post', @@ -33,6 +35,7 @@ export default [ readingTime: 7.2, url: 'https://example.com/fusce-sit-amet', geopoint: [2.357573,48.837904], + geoshape: 'nouvelle-aquitaine', }, { title: 'vestibulum nec blog post', @@ -44,6 +47,7 @@ export default [ readingTime: 4.5, url: 'https://example.com/vestibulum-nec', geopoint: [2.357573,48.837904], + geoshape: 'occitanie', }, { title: 'Cras At Blog Post', @@ -56,6 +60,7 @@ export default [ readingTime: 6.0, url: 'https://example.com/cras-at', geopoint: [2.357573,48.837904], + geoshape: 'provence-alpes-cote-d-azur', }, { title: 'Quisque A Blog Post', @@ -68,6 +73,7 @@ export default [ readingTime: 4.0, url: 'https://example.com/quisque-a', geopoint: [2.357573,48.837904], + geoshape: 'auvergne-rhone-alpes', }, { title: 'Ut Vitae Blog Post', @@ -80,6 +86,7 @@ export default [ readingTime: 5.0, url: 'https://example.com/ut-vitae', geopoint: [2.357573,48.837904], + geoshape: 'bourgogne-franche-comte', }, { title: 'Integer Id Blog Post', @@ -92,6 +99,7 @@ export default [ readingTime: 4.2, url: 'https://example.com/integer-id', geopoint: [2.357573,48.837904], + geoshape: 'grand-est', }, { title: 'Undefined row', @@ -103,6 +111,7 @@ export default [ readingTime: null, url: undefined, geopoint: undefined, + geoshape: undefined, }, { title: 'Empty row', diff --git a/packages/visualizations-react/stories/Table/options.ts b/packages/visualizations-react/stories/Table/options.ts index 1a360a97..f7da01da 100644 --- a/packages/visualizations-react/stories/Table/options.ts +++ b/packages/visualizations-react/stories/Table/options.ts @@ -108,6 +108,33 @@ export const columns: Column[] = [ }]) }, }, + { + title: 'Geo shapes', + key: 'geoshape', + dataFormat: 'geo', + options: { + mapOptions: { + style: 'https://demotiles.maplibre.org/style.json', + interactive: false, + bbox: [-6.855469, 41.343825, 11.645508, 51.37178], + zoom: 3, + }, + display: (v : unknown) => v as string, + sources: (v: unknown) => ({ + 'table-stories' : { + type: 'geojson', + data: `https://france-geojson.gregoiredavid.fr/repo/regions/${v}/region-${v}.geojson` + }, + }), + layers: () => ([{ + id:'table-stories-layer', + source: 'table-stories', + type: "fill", + color: 'black', + borderColor: 'white', + }]) + }, + }, ]; const options: TableOptions = { diff --git a/packages/visualizations/src/components/Map/WebGl/types.ts b/packages/visualizations/src/components/Map/WebGl/types.ts index 988e7c70..be110a8e 100644 --- a/packages/visualizations/src/components/Map/WebGl/types.ts +++ b/packages/visualizations/src/components/Map/WebGl/types.ts @@ -1,5 +1,6 @@ import type { CircleLayerSpecification, + FillLayerSpecification, GeoJSONFeature, GestureOptions, LngLatLike, @@ -56,7 +57,10 @@ export interface WebGlMapOptions { export type WebGlMapStyleOption = Partial>; // Supported layers -export type LayerSpecification = CircleLayerSpecification | SymbolLayerSpecification; +export type LayerSpecification = + | CircleLayerSpecification + | SymbolLayerSpecification + | FillLayerSpecification; type BaseLayer = { id: string; @@ -103,7 +107,14 @@ export type SymbolLayer = BaseLayer & { }; }; -export type Layer = CircleLayer | SymbolLayer; +export type FillLayer = BaseLayer & { + type: FillLayerSpecification['type']; + color: Color; + borderColor?: Color; + opacity?: number; +}; + +export type Layer = CircleLayer | SymbolLayer | FillLayer; export type GeoPoint = { lat: number; diff --git a/packages/visualizations/src/components/Map/WebGl/utils.ts b/packages/visualizations/src/components/Map/WebGl/utils.ts index e93331dd..a048775d 100644 --- a/packages/visualizations/src/components/Map/WebGl/utils.ts +++ b/packages/visualizations/src/components/Map/WebGl/utils.ts @@ -5,7 +5,7 @@ import type { StyleSpecification, ExpressionInputType, SymbolLayerSpecification, - FilterSpecification, + FillLayerSpecification, } from 'maplibre-gl'; import { isGroupByForMatchExpression, Color } from '../../types'; @@ -19,6 +19,7 @@ import type { PopupConfigurationByLayers, SymbolLayer, CenterZoomOptions, + FillLayer, } from './types'; import { DEFAULT_DARK_GREY, DEFAULT_BASEMAP_STYLE, DEFAULT_SORT_KEY_VALUE } from './constants'; @@ -34,12 +35,10 @@ export const getMapSources = (sources: WebGlMapData['sources']): StyleSpecificat const getBaseMapLayerConfiguration = (layer: Layer) => { const { id, source, sourceLayer } = layer; - const filter: FilterSpecification = ['==', ['geometry-type'], 'Point']; return { id, source, ...(sourceLayer ? { 'source-layer': sourceLayer } : null), - filter, }; }; @@ -87,6 +86,7 @@ const getMapCircleLayer = (layer: CircleLayer): CircleLayerSpecification => { } return { ...getBaseMapLayerConfiguration(layer), + filter: ['==', ['geometry-type'], 'Point'], type, paint: { 'circle-radius': circleRadius, @@ -117,6 +117,7 @@ const getMapSymbolLayer = (layer: SymbolLayer): SymbolLayerSpecification => { return { ...getBaseMapLayerConfiguration(layer), + filter: ['==', ['geometry-type'], 'Point'], type, layout: { 'icon-size': 1, @@ -126,7 +127,22 @@ const getMapSymbolLayer = (layer: SymbolLayer): SymbolLayerSpecification => { }, }; }; -// Only circle and symbol layers are supported + +const getMapFillLayer = (layer: FillLayer): FillLayerSpecification => { + const { type, color, borderColor, opacity } = layer; + + return { + ...getBaseMapLayerConfiguration(layer), + filter: ['==', ['geometry-type'], 'Polygon'], + type, + paint: { + 'fill-color': color, + ...(borderColor && { 'fill-outline-color': borderColor }), + ...(opacity && { 'fill-opacity': opacity }), + }, + }; +}; +// Circle, symbol and fill layers are supported export const getMapLayers = (layers?: Layer[]): LayerSpecification[] => { if (!layers) return []; return layers.map((layer) => { @@ -135,6 +151,8 @@ export const getMapLayers = (layers?: Layer[]): LayerSpecification[] => { return getMapCircleLayer(layer); case 'symbol': return getMapSymbolLayer(layer); + case 'fill': + return getMapFillLayer(layer); default: throw new Error(`Unexepected layer type for layer: ${layer}`); }