From 5e72711629b8c8ad8c40c2003edc0cfb06513a82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Demenech?= Date: Mon, 3 Jul 2023 18:55:43 -0300 Subject: [PATCH 1/2] [components,maps][l]: implements Leaflet map component -- #963 --- package-lock.json | 93 +++++++++- .../components/.storybook/preview-head.html | 19 ++ packages/components/package.json | 4 + packages/components/src/components/Map.tsx | 140 ++++++++++++++ packages/components/src/types/GeoJSON.tsx | 171 ++++++++++++++++++ packages/components/stories/Map.stories.ts | 55 ++++++ 6 files changed, 477 insertions(+), 5 deletions(-) create mode 100644 packages/components/.storybook/preview-head.html create mode 100644 packages/components/src/components/Map.tsx create mode 100644 packages/components/src/types/GeoJSON.tsx create mode 100644 packages/components/stories/Map.stories.ts diff --git a/package-lock.json b/package-lock.json index 6faef11f..e421b59d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8808,6 +8808,16 @@ "resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.2.0.tgz", "integrity": "sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==" }, + "node_modules/@react-leaflet/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", + "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -12696,6 +12706,15 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/leaflet": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.3.tgz", + "integrity": "sha512-Caa1lYOgKVqDkDZVWkto2Z5JtVo09spEaUt2S69LiugbBpoqQu92HYFMGUbYezZbnBkyOxMNPXHSgRrRY5UyIA==", + "dev": true, + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/lodash": { "version": "4.14.195", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", @@ -15525,6 +15544,11 @@ "node": ">=10" } }, + "node_modules/chroma-js": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", + "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" + }, "node_modules/chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -27579,6 +27603,11 @@ "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -32646,6 +32675,19 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-leaflet": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", + "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", + "dependencies": { + "@react-leaflet/core": "^2.1.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/react-query": { "version": "3.39.3", "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz", @@ -39254,17 +39296,20 @@ }, "packages/components": { "name": "@portaljs/components", - "version": "0.1.11", + "version": "0.1.12", "dependencies": { "@githubocto/flat-ui": "^0.14.1", "@heroicons/react": "^2.0.17", "@tanstack/react-table": "^8.8.5", + "chroma-js": "^2.4.2", "flexsearch": "0.7.21", + "leaflet": "^1.9.4", "next-mdx-remote": "^4.4.1", "papaparse": "^5.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.43.9", + "react-leaflet": "^4.2.1", "react-query": "^3.39.3", "react-vega": "^7.6.0", "rollup-plugin-re": "^1.0.7", @@ -39281,6 +39326,7 @@ "@storybook/react-vite": "^7.0.7", "@storybook/testing-library": "^0.0.14-next.2", "@types/flexsearch": "^0.7.3", + "@types/leaflet": "^1.9.3", "@types/papaparse": "^5.3.7", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", @@ -39627,7 +39673,7 @@ }, "packages/core": { "name": "@portaljs/core", - "version": "1.0.3", + "version": "1.0.6", "license": "MIT", "dependencies": { "@docsearch/react": "^3.3.3", @@ -39653,7 +39699,7 @@ }, "packages/remark-callouts": { "name": "@portaljs/remark-callouts", - "version": "1.0.4", + "version": "1.0.5", "license": "MIT", "dependencies": { "mdast-util-from-markdown": "^1.2.0", @@ -39663,7 +39709,7 @@ }, "packages/remark-embed": { "name": "@portaljs/remark-embed", - "version": "1.0.3", + "version": "1.0.4", "license": "MIT", "dependencies": { "unist-util-visit": "^4.1.1" @@ -39671,7 +39717,7 @@ }, "packages/remark-wiki-link": { "name": "@portaljs/remark-wiki-link", - "version": "1.0.3", + "version": "1.0.4", "license": "MIT", "dependencies": { "mdast-util-to-markdown": "^1.5.0", @@ -46416,6 +46462,7 @@ "@storybook/testing-library": "^0.0.14-next.2", "@tanstack/react-table": "^8.8.5", "@types/flexsearch": "^0.7.3", + "@types/leaflet": "^1.9.3", "@types/papaparse": "^5.3.7", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", @@ -46423,12 +46470,14 @@ "@typescript-eslint/parser": "^5.57.1", "@vitejs/plugin-react": "^4.0.0", "autoprefixer": "^10.4.14", + "chroma-js": "2.4.2", "eslint": "^8.38.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.3.4", "eslint-plugin-storybook": "^0.6.11", "flexsearch": "0.7.21", "json": "^11.0.0", + "leaflet": "^1.9.4", "next-mdx-remote": "^4.4.1", "papaparse": "^5.4.1", "postcss": "^8.4.23", @@ -46436,6 +46485,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.43.9", + "react-leaflet": "^4.2.1", "react-query": "^3.39.3", "react-vega": "^7.6.0", "rollup-plugin-re": "^1.0.7", @@ -46799,6 +46849,12 @@ "resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.2.0.tgz", "integrity": "sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==" }, + "@react-leaflet/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", + "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", + "requires": {} + }, "@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -49605,6 +49661,15 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "@types/leaflet": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.3.tgz", + "integrity": "sha512-Caa1lYOgKVqDkDZVWkto2Z5JtVo09spEaUt2S69LiugbBpoqQu92HYFMGUbYezZbnBkyOxMNPXHSgRrRY5UyIA==", + "dev": true, + "requires": { + "@types/geojson": "*" + } + }, "@types/lodash": { "version": "4.14.195", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", @@ -51793,6 +51858,11 @@ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "dev": true }, + "chroma-js": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", + "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" + }, "chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -61034,6 +61104,11 @@ } } }, + "leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -64650,6 +64725,14 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "react-leaflet": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", + "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", + "requires": { + "@react-leaflet/core": "^2.1.0" + } + }, "react-query": { "version": "3.39.3", "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz", diff --git a/packages/components/.storybook/preview-head.html b/packages/components/.storybook/preview-head.html new file mode 100644 index 00000000..3ccd28d1 --- /dev/null +++ b/packages/components/.storybook/preview-head.html @@ -0,0 +1,19 @@ + + + + + + diff --git a/packages/components/package.json b/packages/components/package.json index 93cad484..c2ed05ec 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -27,12 +27,15 @@ "@githubocto/flat-ui": "^0.14.1", "@heroicons/react": "^2.0.17", "@tanstack/react-table": "^8.8.5", + "chroma-js": "^2.4.2", "flexsearch": "0.7.21", + "leaflet": "^1.9.4", "next-mdx-remote": "^4.4.1", "papaparse": "^5.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.43.9", + "react-leaflet": "^4.2.1", "react-query": "^3.39.3", "react-vega": "^7.6.0", "rollup-plugin-re": "^1.0.7", @@ -49,6 +52,7 @@ "@storybook/react-vite": "^7.0.7", "@storybook/testing-library": "^0.0.14-next.2", "@types/flexsearch": "^0.7.3", + "@types/leaflet": "^1.9.3", "@types/papaparse": "^5.3.7", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", diff --git a/packages/components/src/components/Map.tsx b/packages/components/src/components/Map.tsx new file mode 100644 index 00000000..108c3eca --- /dev/null +++ b/packages/components/src/components/Map.tsx @@ -0,0 +1,140 @@ +import { useEffect, useState } from 'react'; +import LoadingSpinner from './LoadingSpinner'; +import loadData from '../lib/loadData'; +import chroma from 'chroma-js'; +import { + MapContainer, + TileLayer, + GeoJSON as GeoJSONLayer, + useMap, +} from 'react-leaflet'; + +import * as L from 'leaflet'; + +export type MapProps = { + data: string | GeoJSON.GeoJSON; + title?: string; + colorScale?: { + starting: string; + ending: string; + }; + center?: { latitude: number | undefined; longitude: number | undefined }; + zoom?: number; + tooltip?: { + prop: string; + }; +}; + +export function Map({ + data, + title = '', + colorScale = { starting: 'blue', ending: 'red' }, + center = { latitude: 45, longitude: 45 }, + zoom = 2, + tooltip = { + prop: '', + }, +}: MapProps) { + const [isLoading, setIsLoading] = useState(false); + + title; + // By default, assumes data is an Array... + const [geoJsonData, setGeoJsonData] = useState(null); + + useEffect(() => { + // If data is string, assume it's a URL + if (typeof data === 'string') { + setIsLoading(true); + + loadData(data).then((res: any) => { + const geoJsonObject = JSON.parse(res); + + const colorScaleAr = chroma + .scale([colorScale.starting, colorScale.ending]) + .mode('lch') + .colors(geoJsonObject.features.length); + + geoJsonObject.features.forEach((feature, i) => { + if (feature.color === undefined) { + feature.color = colorScaleAr[i]; + } + }); + + setGeoJsonData(geoJsonObject); + setIsLoading(false); + }); + } else { + setGeoJsonData(data); + } + }, []); + + const onEachFeature = (feature, layer) => { + const map = useMap(); + + const geometryType = feature.type; + + if (tooltip.prop) + layer.bindTooltip(feature.properties[tooltip.prop], { + direction: 'center', + }); + + layer.on({ + mouseover: (event) => { + if (['Polygon', 'MultiPolygon'].includes(geometryType)) { + event.target.setStyle({ + fillColor: '#B85042', + }); + } + }, + mouseout: (event) => { + if (['Polygon', 'MultiPolygon'].includes(geometryType)) { + event.target.setStyle({ + fillColor: '#A7BEAE', + }); + } + }, + }); + }; + + return isLoading || !geoJsonData ? ( +
+ +
+ ) : ( + { + map.target.scrollWheelZoom.enable(); + + var info = L.control(); + + info.onAdd = function (map) { + this._div = L.DomUtil.create('div', 'info'); + this.update(); + return this._div; + }; + + info.update = function (props) { + this._div.innerHTML = `

${title}

`; + }; + + if (title) info.addTo(map.target); + }} + > + { + return { color: geoJsonFeature.color }; + }} + onEachFeature={onEachFeature} + /> + +
+ ); +} diff --git a/packages/components/src/types/GeoJSON.tsx b/packages/components/src/types/GeoJSON.tsx new file mode 100644 index 00000000..8b751514 --- /dev/null +++ b/packages/components/src/types/GeoJSON.tsx @@ -0,0 +1,171 @@ +/** + * Typescript types for the GeoJSON RFC7946 specification. This is not fully RFC-compliant due to lack of support for + * ranged number data types. + * + * See https://tools.ietf.org/html/rfc7946 + */ +export declare namespace GeoJSON { + /** + * Inside this document, the term "geometry type" refers to seven case-sensitive strings: "Point", "MultiPoint", + * "LineString", "MultiLineString", "Polygon", "MultiPolygon", and "GeometryCollection". + */ + export type Geometry = Point | MultiPoint | LineString | MultiLineString | Polygon | MultiPolygon + | GeometryCollection; + export type GeometryType = Geometry["type"]; + + /** + * ...the term "GeoJSON types" refers to nine case-sensitive strings: "Feature", "FeatureCollection", and the + * geometry types listed above. + */ + export type GeoJson = Geometry | Feature | FeatureCollection; + export type GeoJsonType = GeoJson["type"]; + + // types + + /** + * A position is an array of numbers. There MUST be two or more elements. The first two elements are longitude and + * latitude, or easting and northing, precisely in that order and using decimal numbers. Altitude or elevation MAY + * be included as an optional third element. + * + * Implementations SHOULD NOT extend positions beyond three elements because the semantics of extra elements are + * unspecified and ambiguous. + */ + export type Position = [longitude: number, latitude: number, elevation?: number] + + export type Record = { [key in string | number]: unknown }; + + /** + * Properties inherit to all GeoJSON types + */ + export interface GeometryBase extends Record { + /** + * A GeoJSON object MAY have a member named "bbox" to include information on the coordinate range for its + * Geometries, Features, or FeatureCollections. The value of the bbox member MUST be an array of length 2*n + * where n is the number of dimensions represented in the contained geometries, with all axes of the most + * southwesterly point followed by all axes of the more northeasterly point. The axes order of a bbox follows + * the axes order of geometries. + */ + bbox?: number[]; + + /** + * A GeoJSON object MAY have other members. + * + * Members not described in this specification ("foreign members") MAY be used in a GeoJSON document. Note that + * support for foreign members can vary across implementations, and no normative processing model for foreign + * members is defined. + */ + } + + // geometry types + + export interface Point extends GeometryBase { + type: "Point"; + /** + * For type "Point", the "coordinates" member is a single position. + */ + coordinates: Position; + } + + export interface MultiPoint extends GeometryBase { + type: "MultiPoint"; + /** + * For type "MultiPoint", the "coordinates" member is an array of positions. + */ + coordinates: Position[]; + } + + export interface LineString extends GeometryBase { + type: "LineString"; + /** + * For type "LineString", the "coordinates" member is an array of two or more positions. + */ + coordinates: { 0: Position, 1: Position } & Position[] + } + + export interface MultiLineString extends GeometryBase { + type: "MultiLineString"; + /** + * For type "MultiLineString", the "coordinates" member is an array of LineString coordinate arrays. + */ + coordinates: LineString["coordinates"][]; + } + + /** + * To specify a constraint specific to Polygons, it is useful to introduce the concept of a linear ring: + * - A linear ring is a closed LineString with four or more positions. + * - The first and last positions are equivalent, and they MUST contain identical values; their representation + * SHOULD also be identical. + * - A linear ring is the boundary of a surface or the boundary of a hole in a surface. + * - A linear ring MUST follow the right-hand rule with respect to the area it bounds, i.e., exterior rings are + * counterclockwise, and holes are clockwise. + */ + export type LinearRing = { 0: Position, 1: Position, 2: Position, 3: Position } & Position[]; + + export interface Polygon extends GeometryBase { + type: "Polygon"; + /** + * For type "Polygon", the "coordinates" member MUST be an array of linear ring coordinate arrays. + * + * For Polygons with more than one of these rings, the first MUST be the exterior ring, and any others MUST be + * interior rings. The exterior ring bounds the surface, and the interior rings (if present) bound holes within + * the surface. + */ + coordinates: LinearRing[]; + } + + export interface MultiPolygon extends GeometryBase { + type: "MultiPolygon"; + /** + * For type "MultiPolygon", the "coordinates" member is an array of Polygon coordinate arrays. + */ + coordinates: Polygon["coordinates"][]; + } + + export interface GeometryCollection { + /** + * A GeoJSON object with type "GeometryCollection" is a Geometry object. + */ + type: "GeometryCollection"; + /** + * A GeometryCollection has a member with the name "geometries". The value of "geometries" is an array. Each + * element of this array is a GeoJSON Geometry object. It is possible for this array to be empty. + */ + geometries: Geometry[]; + } + + // GeoJSON types + + export interface Feature { + /** + * A Feature object has a "type" member with the value "Feature". + */ + type: "Feature"; + /** + * If a Feature has a commonly used identifier, that identifier SHOULD be included as a member of the Feature object + * with the name "id", and the value of this member is either a JSON string or number. + */ + id?: string | number; + /** + * A Feature object has a member with the name "geometry". The value of the geometry member SHALL be either a + * Geometry object as defined above or, in the case that the Feature is unlocated, a JSON null value. + */ + geometry: Geometry | null; + /** + * A Feature object has a member with the name "properties". The value of the properties member is an object + * (any JSON object or a JSON null value). + */ + properties: Record | null; + } + + export interface FeatureCollection { + /** + * A GeoJSON object with the type "FeatureCollection" is a FeatureCollection object. + */ + type: "FeatureCollection"; + /** + * A FeatureCollection object has a member with the name "features". The value of "features" is a JSON array. Each + * element of the array is a Feature object as defined above. It is possible for this array to be empty. + */ + features: Feature[]; + } +} diff --git a/packages/components/stories/Map.stories.ts b/packages/components/stories/Map.stories.ts new file mode 100644 index 00000000..c39cdc9e --- /dev/null +++ b/packages/components/stories/Map.stories.ts @@ -0,0 +1,55 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Map, MapProps } from '../src/components/Map'; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: 'Components/Map', + component: Map, + tags: ['autodocs'], + argTypes: { + data: { + description: + 'Data to be displayed.\n\n GeoJSON Object \n\nOR\n\n URL to GeoJSON Object', + }, + title: { + description: 'Title to display on the map. Optional.', + }, + center: { + description: 'Initial coordinates of the center of the map', + }, + zoom: { + description: 'Zoom level', + }, + tooltip: { + description: 'Tooltip settings' + } + }, +}; + +export default meta; + +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args +export const GeoJSONPolygons: Story = { + name: 'GeoJSON polygons map', + args: { + data: 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_geography_marine_polys.geojson', + title: 'Seas and Oceans Map', + center: { latitude: 45, longitude: 0 }, + zoom: 2, + tooltip: { prop: 'name' }, + }, +}; + +export const GeoJSONPoints: Story = { + name: 'GeoJSON points map', + args: { + data: 'https://opendata.arcgis.com/datasets/9c58741995174fbcb017cf46c8a42f4b_25.geojson', + title: 'Roads in York', + center: { latitude: 53.9614, longitude: -1.0739 }, + zoom: 12, + tooltip: { prop: 'Location' }, + }, +}; From 299477a717c41e96daac49e86b40f5b18e7e317a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Demenech?= Date: Thu, 6 Jul 2023 17:13:37 -0300 Subject: [PATCH 2/2] [openspending, maps][m]: fix build for leaflet map --- .../components/.storybook/preview-head.html | 23 ++++++------------- packages/components/.storybook/preview.ts | 1 + packages/components/package.json | 10 +++++--- packages/components/postcss.config.js | 5 +++- packages/components/scripts/fix-leaflet.cjs | 6 +++++ packages/components/src/components/Map.tsx | 21 ++++++++--------- packages/components/src/include.css | 6 +++++ packages/components/src/index.css | 15 +++++------- packages/components/src/index.ts | 1 + packages/components/tailwind.config.js | 4 +--- packages/components/vite.config.ts | 3 ++- 11 files changed, 51 insertions(+), 44 deletions(-) create mode 100644 packages/components/scripts/fix-leaflet.cjs create mode 100644 packages/components/src/include.css diff --git a/packages/components/.storybook/preview-head.html b/packages/components/.storybook/preview-head.html index 3ccd28d1..b43dc699 100644 --- a/packages/components/.storybook/preview-head.html +++ b/packages/components/.storybook/preview-head.html @@ -1,19 +1,10 @@ - - - - + diff --git a/packages/components/.storybook/preview.ts b/packages/components/.storybook/preview.ts index 45152b91..1ec353c5 100644 --- a/packages/components/.storybook/preview.ts +++ b/packages/components/.storybook/preview.ts @@ -1,6 +1,7 @@ import 'tailwindcss/tailwind.css' import '../src/index.css' + import type { Preview } from '@storybook/react'; const preview: Preview = { diff --git a/packages/components/package.json b/packages/components/package.json index c2ed05ec..4416c004 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -12,12 +12,14 @@ ], "scripts": { "dev": "npm run storybook", - "build": "tsc && vite build && npm run build-tailwind", + "example": "vite", + "build": "tsc && vite build && npm run build-tailwind && npm run fix-leaflet", "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", - "build-tailwind": "NODE_ENV=production npx tailwindcss -o ./dist/styles.css --minify", - "prepare": "npm run build" + "build-tailwind": "NODE_ENV=production npx tailwindcss --postcss -c tailwind.config.js -i src/index.css -o ./dist/styles.css --minify", + "prepare": "npm run build", + "fix-leaflet": "node ./scripts/fix-leaflet.cjs" }, "peerDependencies": { "react": "^18.2.0", @@ -66,6 +68,8 @@ "eslint-plugin-storybook": "^0.6.11", "json": "^11.0.0", "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-import-url": "^7.2.0", "prop-types": "^15.8.1", "storybook": "^7.0.7", "tailwindcss": "^3.3.2", diff --git a/packages/components/postcss.config.js b/packages/components/postcss.config.js index 2e7af2b7..e24f213a 100644 --- a/packages/components/postcss.config.js +++ b/packages/components/postcss.config.js @@ -1,6 +1,9 @@ +console.log('PostCSS') + export default { plugins: { + 'postcss-import': {}, tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/packages/components/scripts/fix-leaflet.cjs b/packages/components/scripts/fix-leaflet.cjs new file mode 100644 index 00000000..4878a51b --- /dev/null +++ b/packages/components/scripts/fix-leaflet.cjs @@ -0,0 +1,6 @@ +const fs = require('fs'); +const path = require('path'); + +const leafletPath = path.join(require.resolve('leaflet'), '../') + +fs.cpSync(`${leafletPath}images`,'./dist/images', { recursive: true }); diff --git a/packages/components/src/components/Map.tsx b/packages/components/src/components/Map.tsx index 108c3eca..e55b2769 100644 --- a/packages/components/src/components/Map.tsx +++ b/packages/components/src/components/Map.tsx @@ -6,7 +6,6 @@ import { MapContainer, TileLayer, GeoJSON as GeoJSONLayer, - useMap, } from 'react-leaflet'; import * as L from 'leaflet'; @@ -37,7 +36,6 @@ export function Map({ }: MapProps) { const [isLoading, setIsLoading] = useState(false); - title; // By default, assumes data is an Array... const [geoJsonData, setGeoJsonData] = useState(null); @@ -69,8 +67,6 @@ export function Map({ }, []); const onEachFeature = (feature, layer) => { - const map = useMap(); - const geometryType = feature.type; if (tooltip.prop) @@ -105,29 +101,32 @@ export function Map({ center={[center.latitude, center.longitude]} zoom={zoom} scrollWheelZoom={false} - className="h-[500px]" - whenReady={(map) => { + className="h-80 w-full" + // @ts-ignore + whenReady={(map: any) => { map.target.scrollWheelZoom.enable(); - var info = L.control(); + var info = new L.Control() as any; - info.onAdd = function (map) { + info.onAdd = function () { this._div = L.DomUtil.create('div', 'info'); this.update(); return this._div; }; - info.update = function (props) { + info.update = function () { this._div.innerHTML = `

${title}

`; }; if (title) info.addTo(map.target); + + setTimeout(() => map.target.invalidateSize(), 5000); }} > { - return { color: geoJsonFeature.color }; + style={(geoJsonFeature: any) => { + return { color: geoJsonFeature?.color }; }} onEachFeature={onEachFeature} /> diff --git a/packages/components/src/include.css b/packages/components/src/include.css new file mode 100644 index 00000000..c9bc585e --- /dev/null +++ b/packages/components/src/include.css @@ -0,0 +1,6 @@ +/* Temporary fix for a size issue with FlatUiTable loading indicator on Firefox */ +@layer base { + svg[tw^='animate-pulse w-12'] { + max-width: 100px; + } +} diff --git a/packages/components/src/index.css b/packages/components/src/index.css index c61c638e..7a21fb11 100644 --- a/packages/components/src/index.css +++ b/packages/components/src/index.css @@ -1,10 +1,7 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; -/* Temporary fix for a size issue with FlatUiTable loading indicator on Firefox */ -@layer base { - svg[tw^='animate-pulse w-12'] { - max-width: 100px; - } -} +@import "tailwindcss/base"; +@import "tailwindcss/components"; +@import "leaflet"; +@import "include"; +@import "tailwindcss/utilities"; + diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index b0e5e0b5..0c546556 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -4,3 +4,4 @@ export * from "./components/LineChart"; export * from "./components/Vega"; export * from "./components/VegaLite"; export * from "./components/FlatUiTable"; +export * from "./components/Map"; diff --git a/packages/components/tailwind.config.js b/packages/components/tailwind.config.js index d21f1cda..98fd9860 100644 --- a/packages/components/tailwind.config.js +++ b/packages/components/tailwind.config.js @@ -1,8 +1,6 @@ /** @type {import('tailwindcss').Config} */ export default { content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], - theme: { - extend: {}, - }, + theme: {}, plugins: [], }; diff --git a/packages/components/vite.config.ts b/packages/components/vite.config.ts index ddc2109f..1f62d18c 100644 --- a/packages/components/vite.config.ts +++ b/packages/components/vite.config.ts @@ -41,12 +41,13 @@ const app = async (): Promise => { fileName: (format) => `components.${format}.js`, }, rollupOptions: { - external: ['react', 'react-dom', 'tailwindcss', 'vega-lite', 'vega', 'react-vega'], + external: ['react', 'react-dom', 'tailwindcss', 'vega-lite', 'vega', 'react-vega', 'leaflet'], output: { globals: { react: 'React', 'react-dom': 'ReactDOM', tailwindcss: 'tailwindcss', + leaflet: 'leaflet' }, }, },