diff --git a/packages/lesmis/package.json b/packages/lesmis/package.json index 21607161a1..0244681769 100755 --- a/packages/lesmis/package.json +++ b/packages/lesmis/package.json @@ -24,8 +24,9 @@ "start-lesmis-server": "yarn workspace @tupaia/lesmis-server start-dev", "test": "yarn package:test", "start-ui-components": "yarn workspace @tupaia/ui-components build -w", + "start-ui-map-components": "yarn workspace @tupaia/ui-map-components build -w", "start-ui-chart-components": "yarn workspace @tupaia/ui-chart-components build -w", - "start-frontend": "npm-run-all -c -l -p start-ui-components start-ui-chart-components start-dev", + "start-frontend": "npm-run-all -c -l -p start-ui-components start-ui-chart-components start-ui-map-components start-dev", "start-servers": "npm-run-all -c -l -p start-central-server start-entity-server start-report-server start-web-config-server start-lesmis-server" }, "browserslist": [ diff --git a/packages/ui-components/package.json b/packages/ui-components/package.json index e106c2ce00..34bda678c6 100644 --- a/packages/ui-components/package.json +++ b/packages/ui-components/package.json @@ -55,10 +55,10 @@ "@storybook/react": "^6.3.9", "@types/jest": "^29.5.1", "@types/lodash.throttle": "^4.1.7", - "@types/react-dom": "^18.2.4", + "@types/react-dom": "^16.9.18", "@types/react-router-dom": "^5.3.3", "@types/react-table": "^7.7.14", - "@types/styled-components": "^5.1.26", + "@types/styled-components": "^5.0.0", "faker": "^4.1.0", "fast-glob": "^3.2.5", "react-docgen-typescript-plugin": "^1.0.5", diff --git a/packages/ui-components/src/components/ReferenceTooltip.tsx b/packages/ui-components/src/components/ReferenceTooltip.tsx index 91e17d8091..f2d46bed03 100644 --- a/packages/ui-components/src/components/ReferenceTooltip.tsx +++ b/packages/ui-components/src/components/ReferenceTooltip.tsx @@ -46,13 +46,13 @@ const StyledToolTip = withStyles(theme => ({ }, }))(Tooltip); -interface Reference { +export interface ReferenceProps { text?: string; name?: string; link?: string; } -const Content = ({ text = '', name = '', link = '' }: Reference) => { +const Content = ({ text = '', name = '', link = '' }: ReferenceProps) => { if (text) { return ( @@ -72,7 +72,7 @@ const Content = ({ text = '', name = '', link = '' }: Reference) => { interface ReferenceTooltipProps { iconStyleOption?: string; - reference?: Reference; + reference?: ReferenceProps; } export const ReferenceTooltip = ({ diff --git a/packages/ui-map-components/.storybook/main.js b/packages/ui-map-components/.storybook/main.js index e731ebcce9..e32f7873ab 100644 --- a/packages/ui-map-components/.storybook/main.js +++ b/packages/ui-map-components/.storybook/main.js @@ -2,4 +2,7 @@ module.exports = { stories: ['../stories/**/*.stories.js'], addons: ['@storybook/addon-essentials'], + typescript: { + reactDocgen: 'react-docgen-typescript-plugin', + }, }; diff --git a/packages/ui-map-components/jest.config.js b/packages/ui-map-components/jest.config.js index 31ed09a0b5..404168ebec 100644 --- a/packages/ui-map-components/jest.config.js +++ b/packages/ui-map-components/jest.config.js @@ -2,16 +2,23 @@ * Tupaia * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd */ +const baseConfig = require('../../jest.config-ts.json'); module.exports = { - moduleDirectories: ['node_modules'], - collectCoverageFrom: ['**/src/components/**/*.js'], + ...baseConfig, + rootDir: '.', + moduleDirectories: ['node_modules', 'helpers'], + collectCoverageFrom: ['**/src/**/**'], + testMatch: ['/src/__tests__/**/**.test.**'], // handle static assets @see https://jestjs.io/docs/webpack#handling-static-assets moduleNameMapper: { '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|css)$': '/jestFileMock.js', '^file-loader': '/jestFileMock.js', }, - transformIgnorePatterns: ['/node_modules/'], + transform: { + '^.+\\.tsx?$': 'ts-jest', + '^.+\\.js$': 'babel-jest', + }, testTimeout: 30 * 1000, // 30 seconds. Needed for CI as some test take a while if CPU has high load }; diff --git a/packages/ui-map-components/package.json b/packages/ui-map-components/package.json index 09a277c2f1..f9a3db80ea 100644 --- a/packages/ui-map-components/package.json +++ b/packages/ui-map-components/package.json @@ -11,16 +11,16 @@ "author": "Beyond Essential Systems (https://beyondessential.com.au)", "source": "src/index.js", "main": "dist/index.js", + "types": "dist/index.d.ts", "scripts": { - "build": "rm -rf dist && npm run --prefix ../../ package:build:js", + "build": "rm -rf dist && yarn package:build:ts", "build-dev": "npm run build", "lint": "yarn package:lint:js", "lint:fix": "yarn lint --fix", "storybook": "start-storybook -s public -p 6006", "test": "yarn package:test --env=jsdom", "test:coverage": "yarn test --coverage", - "test:watch": "yarn test --watch", - "build-dev:watch": "yarn run package:build:js -w" + "test:watch": "yarn test --watch" }, "dependencies": { "@material-ui/core": "^4.9.8", @@ -38,6 +38,12 @@ }, "devDependencies": { "@material-ui/styles": "^4.9.10", - "@storybook/react": "^6.3.9" + "@mui/types": "^7.2.4", + "@storybook/react": "^6.3.9", + "@types/jest": "^29.5.1", + "@types/leaflet": "^1.7.1", + "@types/react-dom": "^16.9.18", + "@types/styled-components": "^5.1.26", + "react-docgen-typescript-plugin": "^1.0.5" } } diff --git a/packages/ui-map-components/src/__tests/markerColors.test.js b/packages/ui-map-components/src/__tests__/markerColors.test.js similarity index 100% rename from packages/ui-map-components/src/__tests/markerColors.test.js rename to packages/ui-map-components/src/__tests__/markerColors.test.js diff --git a/packages/ui-map-components/src/__tests/markerFormats.test.js b/packages/ui-map-components/src/__tests__/markerFormats.test.js similarity index 100% rename from packages/ui-map-components/src/__tests/markerFormats.test.js rename to packages/ui-map-components/src/__tests__/markerFormats.test.js diff --git a/packages/ui-map-components/src/components/ActivePolygon.js b/packages/ui-map-components/src/components/ActivePolygon.tsx similarity index 60% rename from packages/ui-map-components/src/components/ActivePolygon.js rename to packages/ui-map-components/src/components/ActivePolygon.tsx index 243199c5da..bc5cdbf3d1 100644 --- a/packages/ui-map-components/src/components/ActivePolygon.js +++ b/packages/ui-map-components/src/components/ActivePolygon.tsx @@ -4,14 +4,19 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; -import { Polygon } from 'react-leaflet'; +import { Polygon, PolygonProps as LeafletPolygonProps } from 'react-leaflet'; import styled from 'styled-components'; import { MAP_COLORS } from '../constants'; const { POLYGON_HIGHLIGHT } = MAP_COLORS; -const StyledPolygon = styled(Polygon)` +interface PolygonProps { + shade?: string; + hasChildren?: boolean; + hasShadedChildren?: boolean; +} + +const StyledPolygon = styled(Polygon)` stroke: ${props => props.shade || POLYGON_HIGHLIGHT}; opacity: 1; fill: ${props => props.shade || 'none'}; @@ -27,7 +32,15 @@ const StyledPolygon = styled(Polygon)` /** * ActivePolygon: The polygon that is selected on the map. This handles the style logic */ -const ActivePolygon = ({ coordinates, shade, hasChildren, hasShadedChildren }) => ( +interface ActivePolygonProps extends PolygonProps { + coordinates: LeafletPolygonProps['positions']; +} +const ActivePolygon = ({ + coordinates, + shade, + hasChildren = false, + hasShadedChildren = false, +}: ActivePolygonProps) => ( ); -ActivePolygon.propTypes = { - coordinates: PropTypes.arrayOf( - PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number))), - ).isRequired, - shade: PropTypes.string, - hasChildren: PropTypes.bool, - hasShadedChildren: PropTypes.bool, -}; - -ActivePolygon.defaultProps = { - shade: null, - hasChildren: false, - hasShadedChildren: false, -}; - export default ActivePolygon; diff --git a/packages/ui-map-components/src/components/AreaTooltip.js b/packages/ui-map-components/src/components/AreaTooltip.tsx similarity index 62% rename from packages/ui-map-components/src/components/AreaTooltip.js rename to packages/ui-map-components/src/components/AreaTooltip.tsx index cec7152976..31ec52ce95 100644 --- a/packages/ui-map-components/src/components/AreaTooltip.js +++ b/packages/ui-map-components/src/components/AreaTooltip.tsx @@ -5,12 +5,14 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; import { Tooltip } from 'react-leaflet'; import styled from 'styled-components'; import { PopupDataItemList } from './PopupDataItemList'; +import { MeasureData, Series } from '../types'; -const Heading = styled.span` +const Heading = styled.span<{ + hasMeasureValue: boolean; +}>` text-align: center; font-weight: ${props => (props.hasMeasureValue ? 'bold' : 'normal')}; `; @@ -19,15 +21,25 @@ const Grid = styled.div` display: grid; `; +interface AreaTooltipProps { + permanent?: boolean; + sticky?: boolean; + orgUnitName?: string; + hasMeasureValue?: boolean; + serieses?: Series[]; + orgUnitMeasureData?: MeasureData; + text?: string; +} + export const AreaTooltip = ({ - permanent, - sticky, + permanent = false, + sticky = false, orgUnitName, - hasMeasureValue, - serieses, - orgUnitMeasureData, + hasMeasureValue = false, + serieses = [], + orgUnitMeasureData = {} as MeasureData, text, -}) => { +}: AreaTooltipProps) => { return ( ); }; - -AreaTooltip.propTypes = { - permanent: PropTypes.bool, - sticky: PropTypes.bool, - hasMeasureValue: PropTypes.bool, - serieses: PropTypes.arrayOf(PropTypes.object), - orgUnitMeasureData: PropTypes.object, - orgUnitName: PropTypes.string, - text: PropTypes.string, -}; - -AreaTooltip.defaultProps = { - permanent: false, - sticky: false, - hasMeasureValue: false, - serieses: [], - orgUnitMeasureData: {}, - orgUnitName: null, - text: null, -}; diff --git a/packages/ui-map-components/src/components/EntityPolygon.js b/packages/ui-map-components/src/components/EntityPolygon.tsx similarity index 75% rename from packages/ui-map-components/src/components/EntityPolygon.js rename to packages/ui-map-components/src/components/EntityPolygon.tsx index c3a8099f34..a3d3cfab91 100644 --- a/packages/ui-map-components/src/components/EntityPolygon.js +++ b/packages/ui-map-components/src/components/EntityPolygon.tsx @@ -4,11 +4,11 @@ * */ import React from 'react'; -import PropTypes from 'prop-types'; import { Polygon as PolygonComponent } from 'react-leaflet'; import styled from 'styled-components'; import { blue } from '@material-ui/core/colors'; import { AreaTooltip } from './AreaTooltip'; +import { Entity } from '../types'; export const POLYGON_COLOR = '#EE6230'; @@ -23,7 +23,7 @@ const BasicPolygon = styled(PolygonComponent)` } `; -export const EntityPolygon = ({ entity }) => { +export const EntityPolygon = ({ entity }: { entity?: Entity }) => { if (!entity || !Array.isArray(entity.region)) { return null; } @@ -36,14 +36,3 @@ export const EntityPolygon = ({ entity }) => { ); }; - -EntityPolygon.propTypes = { - entity: PropTypes.shape({ - name: PropTypes.string, - region: PropTypes.array, - }), -}; - -EntityPolygon.defaultProps = { - entity: null, -}; diff --git a/packages/ui-map-components/src/components/InteractivePolygon.js b/packages/ui-map-components/src/components/InteractivePolygon.tsx similarity index 77% rename from packages/ui-map-components/src/components/InteractivePolygon.js rename to packages/ui-map-components/src/components/InteractivePolygon.tsx index d6c44290b2..44ca465b3a 100644 --- a/packages/ui-map-components/src/components/InteractivePolygon.js +++ b/packages/ui-map-components/src/components/InteractivePolygon.tsx @@ -4,12 +4,20 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; import { Polygon } from 'react-leaflet'; import styled from 'styled-components'; import { AreaTooltip } from './AreaTooltip'; import { MAP_COLORS, BREWER_PALETTE } from '../constants'; import ActivePolygon from './ActivePolygon'; +import { + Color, + ColorKey, + Entity, + GenericDataItem, + MeasureData, + OrgUnitCode, + Series, +} from '../types'; const { POLYGON_BLUE, POLYGON_HIGHLIGHT } = MAP_COLORS; @@ -39,12 +47,24 @@ const TransparentShadedPolygon = styled(Polygon)` } `; +type OrgUnit = GenericDataItem & { + isHidden?: boolean; +}; + +type ParsedPropsResult = { + shade?: Color; + isHidden?: boolean; + hasShadedChildren: boolean; + orgUnitMeasureData?: OrgUnit; + orgUnitMultiOverlayMeasureData?: GenericDataItem; +}; + const parseProps = ( - organisationUnitCode, - organisationUnitChildren, - measureOrgUnits, - multiOverlayMeasureData, -) => { + organisationUnitCode: OrgUnitCode = undefined, + organisationUnitChildren: OrgUnit[], + measureOrgUnits: OrgUnit[], + multiOverlayMeasureData: MeasureData[], +): ParsedPropsResult => { let shade; let isHidden; let orgUnitMeasureData; @@ -79,19 +99,32 @@ const parseProps = ( return { shade, isHidden, hasShadedChildren, orgUnitMeasureData, orgUnitMultiOverlayMeasureData }; }; +interface InteractivePolygonProps { + isChildArea?: boolean; + hasMeasureData?: boolean; + multiOverlaySerieses?: Series[]; + multiOverlayMeasureData?: GenericDataItem[]; + permanentLabels?: boolean; + onChangeOrgUnit?: (organisationUnitCode?: string) => void; + area: Entity; + isActive?: boolean; + measureOrgUnits?: OrgUnit[]; + organisationUnitChildren?: GenericDataItem[]; +} + export const InteractivePolygon = React.memo( ({ - isChildArea, - hasMeasureData, - multiOverlaySerieses, - multiOverlayMeasureData, - permanentLabels, - onChangeOrgUnit, + isChildArea = false, + hasMeasureData = false, + multiOverlaySerieses = [], + multiOverlayMeasureData = [], + permanentLabels = true, + onChangeOrgUnit = () => {}, area, - isActive, - measureOrgUnits, - organisationUnitChildren, - }) => { + isActive = false, + measureOrgUnits = [], + organisationUnitChildren = [], + }: InteractivePolygonProps) => { const { organisationUnitCode } = area; const coordinates = area.location?.region; const hasChildren = organisationUnitChildren && organisationUnitChildren.length > 0; @@ -135,7 +168,6 @@ export const InteractivePolygon = React.memo( // Render all measure data even it is not selected on switch button to display. return ( {tooltip}; }, ); - -InteractivePolygon.propTypes = { - area: PropTypes.shape({ - name: PropTypes.string, - type: PropTypes.string, - location: PropTypes.object, - organisationUnitCode: PropTypes.string, - }).isRequired, - isActive: PropTypes.bool, - permanentLabels: PropTypes.bool, - isChildArea: PropTypes.bool, - onChangeOrgUnit: PropTypes.func, - hasMeasureData: PropTypes.bool, - multiOverlaySerieses: PropTypes.arrayOf(PropTypes.object), - measureOrgUnits: PropTypes.arrayOf(PropTypes.object), - multiOverlayMeasureData: PropTypes.arrayOf(PropTypes.object), - organisationUnitChildren: PropTypes.arrayOf(PropTypes.object), -}; - -InteractivePolygon.defaultProps = { - isActive: false, - permanentLabels: true, - isChildArea: false, - onChangeOrgUnit: () => {}, - hasMeasureData: false, - multiOverlaySerieses: [], - organisationUnitChildren: [], - measureOrgUnits: [], - multiOverlayMeasureData: [], -}; diff --git a/packages/ui-map-components/src/components/InversePolygonMask.js b/packages/ui-map-components/src/components/InversePolygonMask.tsx similarity index 64% rename from packages/ui-map-components/src/components/InversePolygonMask.js rename to packages/ui-map-components/src/components/InversePolygonMask.tsx index 5deeaed6dd..0a72404be4 100644 --- a/packages/ui-map-components/src/components/InversePolygonMask.js +++ b/packages/ui-map-components/src/components/InversePolygonMask.tsx @@ -4,8 +4,8 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; -import { Polygon as PolygonComponent } from 'react-leaflet'; +import { Polygon as PolygonComponent, PolygonProps } from 'react-leaflet'; +import { LatLngExpression } from 'leaflet'; import styled from 'styled-components'; const BasicPolygon = styled(PolygonComponent)` @@ -14,7 +14,9 @@ const BasicPolygon = styled(PolygonComponent)` stroke-width: 0; `; -const getOuterPolygon = region => { +type Region = LatLngExpression[]; + +const getOuterPolygon = (region: Region): PolygonProps['positions'] => { return [ [ [90, -180], @@ -26,7 +28,7 @@ const getOuterPolygon = region => { ]; }; -export const InversePolygonMask = ({ region }) => { +export const InversePolygonMask = ({ region }: { region: Region | null }) => { if (!Array.isArray(region)) { return null; } @@ -35,11 +37,3 @@ export const InversePolygonMask = ({ region }) => { return ; }; - -InversePolygonMask.propTypes = { - region: PropTypes.array, -}; - -InversePolygonMask.defaultProps = { - region: null, -}; diff --git a/packages/ui-map-components/src/components/LeafletMap.js b/packages/ui-map-components/src/components/LeafletMap.tsx similarity index 67% rename from packages/ui-map-components/src/components/LeafletMap.js rename to packages/ui-map-components/src/components/LeafletMap.tsx index dfd390636a..b55b379023 100644 --- a/packages/ui-map-components/src/components/LeafletMap.js +++ b/packages/ui-map-components/src/components/LeafletMap.tsx @@ -16,18 +16,30 @@ * complexity. */ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { Component, ReactNode } from 'react'; import styled from 'styled-components'; -import { MapContainer as LeafletMapContainer } from 'react-leaflet'; +import { MapContainer as LeafletMapContainer, MapContainerProps } from 'react-leaflet'; +import { LatLngBoundsExpression, Map as LeafletMapInterface } from 'leaflet'; import { LeafletStyles } from './LeafletStyles'; import { DEFAULT_BOUNDS } from '../constants'; -const Map = styled(LeafletMapContainer)` +interface LeafletMapProps extends MapContainerProps { + onPositionChanged?: ( + center: MapContainerProps['center'], + bounds: MapContainerProps['bounds'], + zoom: MapContainerProps['zoom'], + ) => void; + shouldSnapToPosition?: boolean; +} + +type Bounds = LeafletMapProps['bounds']; + +const Map = styled(LeafletMapContainer)` ${LeafletStyles}; `; -function arePositionsEqual(a, b) { +// Don't use the Leaflet 'bounds' type here as we are referring to only array type expressions +function arePositionsEqual(a: number[], b: number[]) { if (a && b) { // if both centers are defined, equal if the position matches return a[0] === b[0] && a[1] === b[1]; @@ -36,7 +48,8 @@ function arePositionsEqual(a, b) { return a === b; } -function areBoundsEqual(a, b) { +// Don't use the Leaflet 'bounds' type here as we are referring to only array type expressions +function areBoundsEqual(a: number[][], b: number[][]) { if (a && b) { return arePositionsEqual(a[0], b[0]) && arePositionsEqual(a[1], b[1]); } @@ -44,12 +57,36 @@ function areBoundsEqual(a, b) { return a === b; } -function areBoundsValid(b) { +function areBoundsValid(b: Bounds) { return Array.isArray(b) && b.length === 2; } -export class LeafletMap extends Component { - constructor(props) { +export class LeafletMap extends Component { + map: LeafletMapInterface | null; + + zooming: boolean; + + moving: boolean; + + lat: number; + + lng: number; + + zoom: number; + + lastSentLat: number | undefined; + + lastSentLng: number | undefined; + + lastSentZoom: number | undefined; + + initialCenter: LeafletMapProps['center']; + + initialBounds: LeafletMapProps['bounds']; + + initialZoom: LeafletMapProps['zoom']; + + constructor(props: LeafletMapProps) { super(props); // Declare some member variables to help keep track of what's going on @@ -90,7 +127,7 @@ export class LeafletMap extends Component { window.addEventListener('resize', () => this.forceUpdate()); }; - componentDidUpdate = prevProps => { + componentDidUpdate = (prevProps: LeafletMapProps) => { const { center, bounds, zoom } = this.props; if (this.map && this.requiresMoveAnimation(prevProps)) { if (bounds) { @@ -101,17 +138,17 @@ export class LeafletMap extends Component { } }; - attachEvents = map => { - map.on('movestart', event => { - this.onMoveStart(event); + attachEvents = (map: LeafletMapInterface) => { + map.on('movestart', () => { + this.onMoveStart(); }); map.on('moveend', event => { this.onMoveEnd(event); }); - map.on('zoomstart', event => { - this.onZoomStart(event); + map.on('zoomstart', () => { + this.onZoomStart(); }); map.on('zoomend', event => { @@ -119,7 +156,7 @@ export class LeafletMap extends Component { }); }; - captureMap = map => { + captureMap = (map: LeafletMapInterface) => { this.map = map; const center = this.map.getCenter(); this.zoom = this.map.getZoom(); @@ -133,7 +170,7 @@ export class LeafletMap extends Component { this.zooming = true; }; - onZoomEnd = event => { + onZoomEnd = (event: any) => { this.zoom = event.target.getZoom(); this.zooming = false; @@ -144,7 +181,7 @@ export class LeafletMap extends Component { this.moving = true; }; - onMoveEnd = event => { + onMoveEnd = (event: any) => { const center = event.target.getCenter(); this.lat = center.lat; this.lng = center.lng; @@ -166,37 +203,38 @@ export class LeafletMap extends Component { const { onPositionChanged } = this.props; if (onPositionChanged) { - onPositionChanged({ lat, lng }, this.map.getBounds(), this.zoom); + onPositionChanged({ lat, lng }, this.map?.getBounds(), this.zoom); } } this.refreshLayers(); }; - flyToPoint = (center, zoom) => { + flyToPoint = (center: LeafletMapProps['center'], zoom: LeafletMapProps['zoom']) => { if (!center) return; - this.map.setView(center, zoom, { animate: true }); + this.map?.setView(center, zoom, { animate: true }); }; - flyToBounds = bounds => { + flyToBounds = (bounds: LatLngBoundsExpression) => { if (!areBoundsValid(bounds)) return; - this.map.fitBounds(bounds, { + this.map?.fitBounds(bounds, { animate: true, }); }; - requiresMoveAnimation = prevProps => { - const { bounds, center, zoom, shouldSnapToPosition } = this.props; + requiresMoveAnimation = (prevProps: LeafletMapProps) => { + const { bounds, center, zoom, shouldSnapToPosition = false } = this.props; if (shouldSnapToPosition) { if (prevProps.zoom !== zoom) { return true; } if (bounds) { - return !areBoundsEqual(bounds, prevProps.bounds); + // recast to number[][] to avoid typescript error, since the functions above are only handling array + return !areBoundsEqual(bounds as number[][], prevProps.bounds as number[][]); } if (center) { - return !arePositionsEqual(center, prevProps.center); + return !arePositionsEqual(center as number[], prevProps.center as number[]); } } return false; @@ -221,7 +259,7 @@ export class LeafletMap extends Component { { + whenCreated={(map: LeafletMapInterface) => { this.captureMap(map); this.attachEvents(map); if (this.props.whenCreated) { @@ -240,24 +278,3 @@ export class LeafletMap extends Component { ); }; } - -LeafletMap.propTypes = { - bounds: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - center: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - children: PropTypes.node.isRequired, - onClick: PropTypes.func, - onPositionChanged: PropTypes.func, - shouldSnapToPosition: PropTypes.bool, - zoom: PropTypes.number, - whenCreated: PropTypes.func, -}; - -LeafletMap.defaultProps = { - bounds: null, - center: null, - onClick: () => {}, - onPositionChanged: () => {}, - zoom: null, - shouldSnapToPosition: false, - whenCreated: null, -}; diff --git a/packages/ui-map-components/src/components/LeafletStyles.js b/packages/ui-map-components/src/components/LeafletStyles.tsx similarity index 100% rename from packages/ui-map-components/src/components/LeafletStyles.js rename to packages/ui-map-components/src/components/LeafletStyles.tsx diff --git a/packages/ui-map-components/src/components/Legend/Legend.js b/packages/ui-map-components/src/components/Legend/Legend.tsx similarity index 66% rename from packages/ui-map-components/src/components/Legend/Legend.js rename to packages/ui-map-components/src/components/Legend/Legend.tsx index 2fd4daa6f9..53d1acc678 100644 --- a/packages/ui-map-components/src/components/Legend/Legend.js +++ b/packages/ui-map-components/src/components/Legend/Legend.tsx @@ -3,21 +3,16 @@ * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd */ -import PropTypes from 'prop-types'; import React from 'react'; import styled from 'styled-components'; -import { - MEASURE_TYPE_COLOR, - MEASURE_TYPE_ICON, - MEASURE_TYPE_RADIUS, - MEASURE_TYPE_SHADED_SPECTRUM, - MEASURE_TYPE_SPECTRUM, - MEASURE_TYPE_POPUP_ONLY, -} from '../../utils'; import { MarkerLegend } from './MarkerLegend'; import { SpectrumLegend } from './SpectrumLegend'; +import { MEASURE_TYPES } from '../../constants'; +import { LegendProps as BaseLegendProps, Series, MeasureType } from '../../types'; -const LegendFrame = styled.div` +const LegendFrame = styled.div<{ + isDisplayed: boolean; +}>` display: flex; width: fit-content; padding: 0.6rem; @@ -39,37 +34,55 @@ const LegendName = styled.div` `; const coloredMeasureTypes = [ - MEASURE_TYPE_COLOR, - MEASURE_TYPE_SPECTRUM, - MEASURE_TYPE_SHADED_SPECTRUM, + MEASURE_TYPES.COLOR, + MEASURE_TYPES.SPECTRUM, + MEASURE_TYPES.SHADED_SPECTRUM, ]; +// This is a workaround for type errors we get when trying to use Array.includes with a subset of a union type. This solution comes from https://github.com/microsoft/TypeScript/issues/51881 +const checkMeasureType = (item: MeasureType, subset: MeasureType[]) => + (subset as ReadonlyArray).includes(item); + const NullLegend = () => null; -const getLegendComponent = measureType => { +const getLegendComponent = (measureType: MeasureType) => { switch (measureType) { - case MEASURE_TYPE_SHADED_SPECTRUM: - case MEASURE_TYPE_SPECTRUM: + case MEASURE_TYPES.SHADED_SPECTRUM: + case MEASURE_TYPES.SPECTRUM: return SpectrumLegend; - case MEASURE_TYPE_RADIUS: + case MEASURE_TYPES.RADIUS: return NullLegend; default: return MarkerLegend; } }; +interface LegendProps extends BaseLegendProps { + className?: string; + measureInfo: { + [mapOverlayCode: string]: { + [seriesesKey: string]: Series[]; + }; + }; + currentMapOverlayCodes: string[]; + displayedMapOverlayCodes?: string[]; + seriesesKey?: string; + SeriesContainer?: React.ComponentType; + SeriesDivider?: React.ComponentType; +} + export const Legend = React.memo( ({ className, measureInfo: baseMeasureInfo, setValueHidden, - hiddenValues, + hiddenValues = {}, currentMapOverlayCodes, displayedMapOverlayCodes, - seriesesKey, - SeriesContainer, + seriesesKey = 'serieses', + SeriesContainer = LegendFrame, SeriesDivider, - }) => { + }: LegendProps) => { if (Object.keys(baseMeasureInfo).length === 0) { return null; } @@ -78,13 +91,17 @@ export const Legend = React.memo( // measure info for mapOverlayCode may not exist when location changes. const baseSerieses = baseMeasureInfo[mapOverlayCode]?.[seriesesKey] || []; const serieses = baseSerieses.filter( - ({ type, hideFromLegend, values = [], min, max, noDataColour }) => { + ({ type, hideFromLegend, values = [], min, max, noDataColour }: Series) => { + const seriesType = type as MeasureType; // if type is radius or popup-only, don't create a legend - if ([MEASURE_TYPE_RADIUS, MEASURE_TYPE_POPUP_ONLY].includes(type)) return false; + if (checkMeasureType(seriesType, [MEASURE_TYPES.RADIUS, MEASURE_TYPES.POPUP_ONLY])) + return false; + // if hideFromLegend is true, don't create a legend if (hideFromLegend) return false; + // if type is spectrum or shaded-spectrum, only create a legend if min and max are set OR noDataColour is set. If noDataColour is not set, that means hideNullFromLegend has been set as true in the map overlay config. Spectrum legends 'values' property will always be [] - if ([MEASURE_TYPE_SHADED_SPECTRUM, MEASURE_TYPE_SPECTRUM].includes(type)) + if (checkMeasureType(seriesType, [MEASURE_TYPES.SHADED_SPECTRUM, MEASURE_TYPES.SPECTRUM])) return noDataColour ? true : !(min === null || min === undefined || max === null || max === undefined); @@ -92,7 +109,11 @@ export const Legend = React.memo( }, ); return { ...results, [mapOverlayCode]: { serieses } }; - }, {}); + }, {}) as { + [mapOverlayCode: string]: { + serieses: Series[]; + }; + }; const legendTypes = currentMapOverlayCodes .map(mapOverlayCode => measureInfo[mapOverlayCode].serieses) @@ -105,17 +126,19 @@ export const Legend = React.memo( {currentMapOverlayCodes.map(mapOverlayCode => { const { serieses } = measureInfo[mapOverlayCode]; const baseSerieses = baseMeasureInfo[mapOverlayCode]?.[seriesesKey] || []; - const hasIconLayer = baseSerieses.some(l => l.type === MEASURE_TYPE_ICON); - const hasRadiusLayer = baseSerieses.some(l => l.type === MEASURE_TYPE_RADIUS); - const hasColorLayer = baseSerieses.some(l => coloredMeasureTypes.includes(l.type)); + const hasIconLayer = baseSerieses.some(l => l.type === MEASURE_TYPES.ICON); + const hasRadiusLayer = baseSerieses.some(l => l.type === MEASURE_TYPES.RADIUS); + const hasColorLayer = baseSerieses.some(l => + checkMeasureType(l.type as MeasureType, coloredMeasureTypes), + ); const isDisplayed = !displayedMapOverlayCodes || displayedMapOverlayCodes.includes(mapOverlayCode); return serieses - .sort(a => (a.type === MEASURE_TYPE_COLOR ? -1 : 1)) // color series should sit at the top + .sort(a => (a.type === MEASURE_TYPES.COLOR ? -1 : 1)) // color series should sit at the top .map((series, index) => { const { type } = series; - const LegendComponent = getLegendComponent(type); + const LegendComponent = getLegendComponent(type as MeasureType); return ( <> @@ -139,25 +162,3 @@ export const Legend = React.memo( ); }, ); - -Legend.propTypes = { - measureInfo: PropTypes.object.isRequired, - className: PropTypes.string, - hiddenValues: PropTypes.object, - setValueHidden: PropTypes.func, - displayedMapOverlayCodes: PropTypes.arrayOf(PropTypes.string), - currentMapOverlayCodes: PropTypes.arrayOf(PropTypes.string).isRequired, - seriesesKey: PropTypes.string, - SeriesContainer: PropTypes.node, - SeriesDivider: PropTypes.node, -}; - -Legend.defaultProps = { - className: null, - displayedMapOverlayCodes: null, - hiddenValues: {}, - setValueHidden: null, - seriesesKey: 'serieses', - SeriesContainer: LegendFrame, - SeriesDivider: null, -}; diff --git a/packages/ui-map-components/src/components/Legend/LegendEntry.js b/packages/ui-map-components/src/components/Legend/LegendEntry.tsx similarity index 63% rename from packages/ui-map-components/src/components/Legend/LegendEntry.js rename to packages/ui-map-components/src/components/Legend/LegendEntry.tsx index 317744f23d..9212c56788 100644 --- a/packages/ui-map-components/src/components/Legend/LegendEntry.js +++ b/packages/ui-map-components/src/components/Legend/LegendEntry.tsx @@ -4,9 +4,9 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; import styled from 'styled-components'; import MuiButton from '@material-ui/core/Button'; +import { LegendProps, Value } from '../../types'; const Button = styled(MuiButton)` display: flex; @@ -34,9 +34,29 @@ const Label = styled.div` } `; +interface LegendEntryProps { + marker: React.ReactNode; + label: string; + value: Value; + dataKey?: string; + onClick?: LegendProps['setValueHidden']; + hiddenValues?: LegendProps['hiddenValues']; + unClickable?: boolean; +} + export const LegendEntry = React.memo( - ({ marker, label, value, dataKey, onClick, hiddenValues, unClickable }) => { - const hidden = (hiddenValues[dataKey] || {})[value]; + ({ + marker, + label, + value, + dataKey, + onClick, + hiddenValues = {}, + unClickable = false, + }: LegendEntryProps) => { + const hidden = dataKey + ? (hiddenValues[dataKey] || {})[value as keyof typeof hiddenValues] + : false; const handleClick = () => { if (!unClickable && onClick) { @@ -52,21 +72,3 @@ export const LegendEntry = React.memo( ); }, ); - -LegendEntry.propTypes = { - marker: PropTypes.element.isRequired, - label: PropTypes.string.isRequired, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]), - dataKey: PropTypes.string, - hiddenValues: PropTypes.object, - onClick: PropTypes.func, - unClickable: PropTypes.bool, -}; - -LegendEntry.defaultProps = { - hiddenValues: {}, - unClickable: false, - onClick: null, - value: null, - dataKey: null, -}; diff --git a/packages/ui-map-components/src/components/Legend/MarkerLegend.js b/packages/ui-map-components/src/components/Legend/MarkerLegend.tsx similarity index 84% rename from packages/ui-map-components/src/components/Legend/MarkerLegend.js rename to packages/ui-map-components/src/components/Legend/MarkerLegend.tsx index 2d1dd31136..fab17bc81e 100644 --- a/packages/ui-map-components/src/components/Legend/MarkerLegend.js +++ b/packages/ui-map-components/src/components/Legend/MarkerLegend.tsx @@ -3,7 +3,6 @@ * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd */ -import PropTypes from 'prop-types'; import React from 'react'; import styled from 'styled-components'; import { useTheme } from '@material-ui/core/styles'; @@ -12,6 +11,7 @@ import { UNKNOWN_COLOR } from '../../constants'; import { DEFAULT_ICON, HIDDEN_ICON, + IconKey, LEGEND_COLOR_ICON, LEGEND_RADIUS_ICON, LEGEND_SHADING_ICON, @@ -25,6 +25,7 @@ import { MEASURE_VALUE_OTHER, } from '../../utils'; import { LegendEntry } from './LegendEntry'; +import { SeriesValue, MarkerLegendProps, MarkerSeries, Value } from '../../types'; const Container = styled(MuiBox)` display: flex; @@ -45,11 +46,11 @@ const Container = styled(MuiBox)` * and hide the radius as well. But, if the hidden icon is 'other', go ahead and add it to * the legend and show the radius. */ -const isHiddenOtherIcon = ({ value, icon }) => { +const isHiddenOtherIcon = ({ value, icon }: SeriesValue) => { return value === MEASURE_VALUE_OTHER && icon === HIDDEN_ICON; }; -const getMarkerColor = (value, type, hasColorLayer) => { +const getMarkerColor = (value: SeriesValue, type: MarkerSeries['type'], hasColorLayer: boolean) => { const theme = useTheme(); if (type === MEASURE_TYPE_COLOR) { @@ -66,12 +67,12 @@ const getMarkerColor = (value, type, hasColorLayer) => { }; const getLegendMarkerForValue = ( - value, - type, - hasIconLayer, - hasRadiusLayer, - hasColorLayer, - defaultIcon = null, + value: SeriesValue, + type: MarkerSeries['type'], + hasIconLayer: MarkerLegendProps['hasIconLayer'], + hasRadiusLayer: MarkerLegendProps['hasRadiusLayer'], + hasColorLayer: MarkerLegendProps['hasColorLayer'], + defaultIcon: IconKey | null = null, ) => { const { icon } = value; const color = getMarkerColor(value, type, hasColorLayer); @@ -109,7 +110,14 @@ const getLegendMarkerForValue = ( }; export const MarkerLegend = React.memo( - ({ series, setValueHidden, hiddenValues, hasIconLayer, hasRadiusLayer, hasColorLayer }) => { + ({ + series, + setValueHidden, + hiddenValues = {}, + hasIconLayer, + hasRadiusLayer, + hasColorLayer, + }: MarkerLegendProps) => { const { type, values, key: dataKey, valueMapping, icon } = series; const keys = values @@ -131,7 +139,7 @@ export const MarkerLegend = React.memo( dataKey={dataKey} marker={marker} label={v.name} - value={v.value} + value={v.value as Value} hiddenValues={hiddenValues} onClick={setValueHidden} /> @@ -149,7 +157,7 @@ export const MarkerLegend = React.memo( }); let nullKey = null; - const nullItem = valueMapping.null; + const nullItem = valueMapping?.null; if (!hasGroupedLegendIncludingNull && nullItem && !nullItem.hideFromLegend) { nullKey = ( @@ -179,23 +187,3 @@ export const MarkerLegend = React.memo( ); }, ); - -MarkerLegend.propTypes = { - series: PropTypes.shape({ - name: PropTypes.string, - key: PropTypes.string, - type: PropTypes.string, - values: PropTypes.array, - valueMapping: PropTypes.object, - }).isRequired, - hasIconLayer: PropTypes.bool.isRequired, - setValueHidden: PropTypes.func, - hiddenValues: PropTypes.object, - hasRadiusLayer: PropTypes.bool.isRequired, - hasColorLayer: PropTypes.bool.isRequired, -}; - -MarkerLegend.defaultProps = { - hiddenValues: {}, - setValueHidden: null, -}; diff --git a/packages/ui-map-components/src/components/Legend/SpectrumLegend.js b/packages/ui-map-components/src/components/Legend/SpectrumLegend.tsx similarity index 67% rename from packages/ui-map-components/src/components/Legend/SpectrumLegend.js rename to packages/ui-map-components/src/components/Legend/SpectrumLegend.tsx index 76f62b58c0..a9cad99b9e 100644 --- a/packages/ui-map-components/src/components/Legend/SpectrumLegend.js +++ b/packages/ui-map-components/src/components/Legend/SpectrumLegend.tsx @@ -5,7 +5,6 @@ import moment from 'moment'; import React from 'react'; -import PropTypes from 'prop-types'; import MuiBox from '@material-ui/core/Box'; import styled from 'styled-components'; import { formatDataValueByType } from '@tupaia/utils'; @@ -13,6 +12,7 @@ import { resolveSpectrumColour } from '../../utils'; import { LEGEND_SHADING_ICON, getMarkerForOption } from '../Markers/markerIcons'; import { SCALE_TYPES } from '../../constants'; import { LegendEntry } from './LegendEntry'; +import { ScaleType, SpectrumLegendProps, SpectrumSeries, Value } from '../../types'; const FlexCenter = styled(MuiBox)` display: flex; @@ -20,7 +20,9 @@ const FlexCenter = styled(MuiBox)` justify-content: center; `; -const SpectrumContainer = styled.div` +const SpectrumContainer = styled.div<{ + $hasNoData: boolean; +}>` max-width: ${p => (p.$hasNoData ? '70%' : '100%')}; `; @@ -43,7 +45,15 @@ const LabelRight = styled.div` margin-left: 0.625rem; `; -const getSpectrumLabels = (scaleType, min, max, valueType) => { +const getSpectrumLabels = ( + scaleType: ScaleType, + min: number, + max: number, + valueType?: SpectrumSeries['valueType'], +): { + left: string; + right: string; +} => { switch (scaleType) { case SCALE_TYPES.GPI: case SCALE_TYPES.PERFORMANCE: @@ -60,13 +70,14 @@ const getSpectrumLabels = (scaleType, min, max, valueType) => { } }; -const renderSpectrum = ({ min, max, scaleType, scaleColorScheme, valueType }) => { +const renderSpectrum = ({ min, max, scaleType, scaleColorScheme, valueType }: SpectrumSeries) => { if (min == null || max == null) return null; const spectrumDivs = []; if (min === max) { // There will only be a single value displayed, let's just default it to the middle color (50 % of the way from 0 to 1): + const colour = resolveSpectrumColour(scaleType, scaleColorScheme, 0.5, 0, 1); const { left: label } = getSpectrumLabels(scaleType, min, min, valueType); @@ -111,55 +122,43 @@ const renderSpectrum = ({ min, max, scaleType, scaleColorScheme, valueType }) => ); }; -export const SpectrumLegend = React.memo(({ series, setValueHidden, hiddenValues }) => { - const { - valueMapping, - noDataColour, - min, - max, - scaleType, - scaleColorScheme, - valueType, - key: dataKey, - } = series; - - const { value } = valueMapping.null; +export const SpectrumLegend = React.memo( + ({ series, setValueHidden, hiddenValues = {} }: SpectrumLegendProps) => { + const { + valueMapping, + noDataColour, + min, + max, + scaleType, + scaleColorScheme, + valueType, + key: dataKey, + } = series; + + const { value } = valueMapping.null; - return ( - - - {renderSpectrum({ min, max, scaleType, scaleColorScheme, valueType })} - - {noDataColour && ( - - )} - - ); -}); - -SpectrumLegend.propTypes = { - series: PropTypes.shape({ - valueMapping: PropTypes.object, - scaleColorScheme: PropTypes.object, - min: PropTypes.number, - max: PropTypes.number, - scaleType: PropTypes.string, - valueType: PropTypes.string, - dataKey: PropTypes.string, - noDataColour: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - }).isRequired, - setValueHidden: PropTypes.func, - hiddenValues: PropTypes.object, -}; - -SpectrumLegend.defaultProps = { - hiddenValues: {}, - setValueHidden: null, -}; + return ( + + + {renderSpectrum({ + min, + max, + scaleType, + scaleColorScheme, + valueType, + } as SpectrumSeries)} + + {noDataColour && ( + + )} + + ); + }, +); diff --git a/packages/ui-map-components/src/components/Legend/index.js b/packages/ui-map-components/src/components/Legend/index.ts similarity index 100% rename from packages/ui-map-components/src/components/Legend/index.js rename to packages/ui-map-components/src/components/Legend/index.ts diff --git a/packages/ui-map-components/src/components/MarkerLayer.js b/packages/ui-map-components/src/components/MarkerLayer.tsx similarity index 77% rename from packages/ui-map-components/src/components/MarkerLayer.js rename to packages/ui-map-components/src/components/MarkerLayer.tsx index 03a88925b0..ba61a5b698 100644 --- a/packages/ui-map-components/src/components/MarkerLayer.js +++ b/packages/ui-map-components/src/components/MarkerLayer.tsx @@ -5,11 +5,11 @@ */ import React from 'react'; import styled from 'styled-components'; -import PropTypes from 'prop-types'; import { LayerGroup, Polygon } from 'react-leaflet'; import { MeasureMarker, MeasurePopup } from './Markers'; import { AreaTooltip } from './AreaTooltip'; import { getSingleFormattedValue, MEASURE_TYPE_RADIUS } from '../utils'; +import { GenericDataItem, MeasureData, Series } from '../types'; const ShadedPolygon = styled(Polygon)` fill-opacity: 0.5; @@ -19,11 +19,11 @@ const ShadedPolygon = styled(Polygon)` `; // remove name from the measure data as it's not expected in getSingleFormattedValue -const getTooltipText = ({ name, ...markerData }, serieses) => +const getTooltipText = ({ name, ...markerData }: GenericDataItem, serieses: Series[]) => `${name}: ${getSingleFormattedValue(markerData, serieses)}`; // Filter hidden and invalid values and sort measure data -const processData = (measureData, serieses) => { +const processData = (measureData: MeasureData[], serieses: Series[]): MeasureData[] => { const data = measureData .filter(({ coordinates, region }) => region || (coordinates && coordinates.length === 2)) .filter(({ isHidden }) => !isHidden); @@ -36,25 +36,33 @@ const processData = (measureData, serieses) => { return data; }; +interface MarkerLayerProps { + measureData: MeasureData[]; + serieses: Series[]; + multiOverlayMeasureData: GenericDataItem[]; + multiOverlaySerieses: Series[]; + onSeeOrgUnitDashboard: (organisationUnitCode?: string) => void; +} + export const MarkerLayer = ({ measureData, serieses, multiOverlayMeasureData, multiOverlaySerieses, onSeeOrgUnitDashboard, -}) => { +}: MarkerLayerProps) => { if (!measureData || !serieses) return null; const data = processData(measureData, serieses); return ( - {data.map(measure => { + {data.map((measure: MeasureData) => { if (measure.region) { return ( ); }; - -MarkerLayer.propTypes = { - measureData: PropTypes.array, - multiOverlayMeasureData: PropTypes.array, - serieses: PropTypes.arrayOf( - PropTypes.shape({ - name: PropTypes.string, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - }), - ), - multiOverlaySerieses: PropTypes.array, - onSeeOrgUnitDashboard: PropTypes.func, -}; - -MarkerLayer.defaultProps = { - measureData: null, - multiOverlayMeasureData: null, - serieses: null, - multiOverlaySerieses: null, - onSeeOrgUnitDashboard: null, -}; diff --git a/packages/ui-map-components/src/components/Markers/CircleProportionMarker.js b/packages/ui-map-components/src/components/Markers/CircleProportionMarker.js deleted file mode 100644 index abbdf9c187..0000000000 --- a/packages/ui-map-components/src/components/Markers/CircleProportionMarker.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Tupaia - * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd - * - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { CircleMarker } from 'react-leaflet'; -import styled from 'styled-components'; -import { getColor } from '../../utils'; - -const HoverCircle = styled(CircleMarker)` - &:hover { - fill-opacity: 0.5; - } -`; - -export const CircleProportionMarker = React.memo(({ radius, children, coordinates, color }) => { - if (coordinates?.length !== 2) return null; - - const AREA_MULTIPLIER = 100; // just tuned by hand - const numberValue = parseFloat(radius) || 0; - const area = Math.max(numberValue, 1) * AREA_MULTIPLIER; - - const displayRadius = Math.sqrt(area / Math.PI); - const colorValue = getColor(color); - return ( - - {children} - - ); -}); - -CircleProportionMarker.propTypes = { - coordinates: PropTypes.arrayOf(PropTypes.number).isRequired, - children: PropTypes.node, - radius: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, - color: PropTypes.string.isRequired, -}; - -CircleProportionMarker.defaultProps = { - children: null, -}; diff --git a/packages/ui-map-components/src/components/Markers/CircleProportionMarker.tsx b/packages/ui-map-components/src/components/Markers/CircleProportionMarker.tsx new file mode 100644 index 0000000000..8dd895de49 --- /dev/null +++ b/packages/ui-map-components/src/components/Markers/CircleProportionMarker.tsx @@ -0,0 +1,42 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + * + */ + +import React from 'react'; +import { CircleMarker } from 'react-leaflet'; +import styled from 'styled-components'; +import { getColor } from '../../utils'; +import { Color, MarkerProps } from '../../types'; + +const HoverCircle = styled(CircleMarker)` + &:hover { + fill-opacity: 0.5; + } +`; + +export const CircleProportionMarker = React.memo( + ({ radius, children, coordinates, color }: MarkerProps) => { + if ((coordinates as number[])?.length !== 2) return null; + + const AREA_MULTIPLIER = 100; // just tuned by hand + const numberValue = parseFloat(String(radius)) || 0; + const area = Math.max(numberValue, 1) * AREA_MULTIPLIER; + + const displayRadius = Math.sqrt(area / Math.PI); + const colorValue = getColor(color as Color); + return ( + + {children} + + ); + }, +); diff --git a/packages/ui-map-components/src/components/Markers/IconContainer.js b/packages/ui-map-components/src/components/Markers/IconContainer.tsx similarity index 54% rename from packages/ui-map-components/src/components/Markers/IconContainer.js rename to packages/ui-map-components/src/components/Markers/IconContainer.tsx index 297cf64c95..e3521be5ff 100644 --- a/packages/ui-map-components/src/components/Markers/IconContainer.js +++ b/packages/ui-map-components/src/components/Markers/IconContainer.tsx @@ -3,11 +3,15 @@ * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd * */ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { SVGProps, ReactNode } from 'react'; import { ICON_BASE_SIZE } from './constants'; -export const IconContainer = ({ children, scale, ...props }) => ( +interface IconContainerProps extends SVGProps { + children?: ReactNode; + scale?: number; +} + +export const IconContainer = ({ children, scale = 1, ...props }: IconContainerProps) => ( ( {children} ); - -IconContainer.propTypes = { - children: PropTypes.node, - scale: PropTypes.number, -}; - -IconContainer.defaultProps = { - children: null, - scale: 1, -}; diff --git a/packages/ui-map-components/src/components/Markers/IconMarker.js b/packages/ui-map-components/src/components/Markers/IconMarker.js deleted file mode 100644 index 045d62e92f..0000000000 --- a/packages/ui-map-components/src/components/Markers/IconMarker.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Tupaia - * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd - * - */ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Marker } from 'react-leaflet'; -import { getMarkerForValue, ICON_VALUES } from './markerIcons'; - -export const IconMarker = React.memo( - ({ icon, color, children, coordinates, scale, handleClick }) => ( - - {children} - - ), -); - -IconMarker.propTypes = { - coordinates: PropTypes.arrayOf(PropTypes.number).isRequired, - children: PropTypes.node, - icon: PropTypes.oneOf(ICON_VALUES), - color: PropTypes.string.isRequired, - scale: PropTypes.number, - handleClick: PropTypes.func, -}; - -IconMarker.defaultProps = { - scale: 1, - children: null, - handleClick: () => {}, - icon: 'pin', -}; diff --git a/packages/ui-map-components/src/components/Markers/IconMarker.tsx b/packages/ui-map-components/src/components/Markers/IconMarker.tsx new file mode 100644 index 0000000000..97f2ce2d47 --- /dev/null +++ b/packages/ui-map-components/src/components/Markers/IconMarker.tsx @@ -0,0 +1,30 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + * + */ +import React from 'react'; +import { Marker } from 'react-leaflet'; +import { getMarkerForValue } from './markerIcons'; +import { MarkerProps } from '../../types'; + +export const IconMarker = React.memo( + ({ + icon = 'pin', + color, + children = null, + coordinates, + scale = 1, + handleClick = () => {}, + }: MarkerProps) => ( + + {children} + + ), +); diff --git a/packages/ui-map-components/src/components/Markers/MeasureMarker.js b/packages/ui-map-components/src/components/Markers/MeasureMarker.tsx similarity index 64% rename from packages/ui-map-components/src/components/Markers/MeasureMarker.js rename to packages/ui-map-components/src/components/Markers/MeasureMarker.tsx index 2eeb799c80..d8cd282aec 100644 --- a/packages/ui-map-components/src/components/Markers/MeasureMarker.js +++ b/packages/ui-map-components/src/components/Markers/MeasureMarker.tsx @@ -4,21 +4,21 @@ * */ import React from 'react'; -import PropTypes from 'prop-types'; import { IconMarker } from './IconMarker'; import { CircleProportionMarker } from './CircleProportionMarker'; +import { MarkerProps } from '../../types'; -export const MeasureMarker = React.memo(props => { - const { icon, radius } = props; +export const MeasureMarker = React.memo((props: MarkerProps) => { + const { icon, radius = 0 } = props; - if (parseInt(radius, 10) === 0) { + if (radius && parseInt(String(radius), 10) === 0) { if (icon) { // we have an icon, so don't render the radius at all return ; } // we have no icon and zero radius -- use minimum radius instead - return ; + return ; } if (radius && icon) { @@ -36,13 +36,3 @@ export const MeasureMarker = React.memo(props => { return ; }); - -MeasureMarker.propTypes = { - icon: PropTypes.string, - radius: PropTypes.number, -}; - -MeasureMarker.defaultProps = { - icon: null, - radius: null, -}; diff --git a/packages/ui-map-components/src/components/Markers/MeasurePopup.js b/packages/ui-map-components/src/components/Markers/MeasurePopup.js deleted file mode 100644 index add6160120..0000000000 --- a/packages/ui-map-components/src/components/Markers/MeasurePopup.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Tupaia - * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd - * - */ -import React from 'react'; -import PropTypes from 'prop-types'; -import { PopupDataItemList } from '../PopupDataItemList'; -import { PopupMarker } from './PopupMarker'; - -const buildHeaderText = (markerData, popupHeaderFormat) => { - const { organisationUnitCode, name } = markerData; - const replacements = { - code: organisationUnitCode, - name, - }; - return Object.entries(replacements).reduce( - (text, [key, value]) => text.replace(`{${key}}`, value), - popupHeaderFormat, - ); -}; - -export const MeasurePopup = React.memo( - ({ markerData, serieses, onSeeOrgUnitDashboard, multiOverlaySerieses }) => { - const { coordinates, organisationUnitCode } = markerData; - const { popupHeaderFormat = '{name}' } = serieses.reduce((all, mo) => ({ ...all, ...mo }), {}); - return ( - onSeeOrgUnitDashboard(organisationUnitCode) : null - } - > - - - ); - }, -); - -MeasurePopup.propTypes = { - markerData: PropTypes.shape({ - coordinates: PropTypes.arrayOf(PropTypes.number), - code: PropTypes.string, - name: PropTypes.string, - organisationUnitCode: PropTypes.string, - photoUrl: PropTypes.string, - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - }).isRequired, - serieses: PropTypes.arrayOf( - PropTypes.shape({ - name: PropTypes.string, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - }), - ).isRequired, - multiOverlaySerieses: PropTypes.array, - onSeeOrgUnitDashboard: PropTypes.func, -}; - -MeasurePopup.defaultProps = { - onSeeOrgUnitDashboard: null, - multiOverlaySerieses: null, -}; diff --git a/packages/ui-map-components/src/components/Markers/MeasurePopup.tsx b/packages/ui-map-components/src/components/Markers/MeasurePopup.tsx new file mode 100644 index 0000000000..930be0b9e2 --- /dev/null +++ b/packages/ui-map-components/src/components/Markers/MeasurePopup.tsx @@ -0,0 +1,54 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + * + */ +import React from 'react'; +import { PopupDataItemList } from '../PopupDataItemList'; +import { PopupMarker } from './PopupMarker'; +import { MeasureData, Series } from '../../types'; + +const buildHeaderText = (markerData: MeasureData, popupHeaderFormat: string): string => { + const { organisationUnitCode, name } = markerData; + const replacements = { + code: organisationUnitCode, + name, + }; + return Object.entries(replacements).reduce( + (text, [key, value]) => text.replace(`{${key}}`, value || ''), + popupHeaderFormat, + ); +}; + +interface MeasurePopupProps { + markerData: MeasureData; + serieses: Series[]; + onSeeOrgUnitDashboard?: (organisationUnitCode?: string) => void; + multiOverlaySerieses?: Series[]; +} + +export const MeasurePopup = React.memo( + ({ markerData, serieses, onSeeOrgUnitDashboard, multiOverlaySerieses }: MeasurePopupProps) => { + const { coordinates, organisationUnitCode } = markerData; + const { popupHeaderFormat = '{name}' } = serieses.reduce( + (all, mo) => ({ ...all, ...mo }), + {} as Series, + ); + + let onDetailButtonClick; + if (onSeeOrgUnitDashboard) { + onDetailButtonClick = () => onSeeOrgUnitDashboard(organisationUnitCode!); + } + + return ( + + + + ); + }, +); diff --git a/packages/ui-map-components/src/components/Markers/PopupMarker.js b/packages/ui-map-components/src/components/Markers/PopupMarker.tsx similarity index 76% rename from packages/ui-map-components/src/components/Markers/PopupMarker.js rename to packages/ui-map-components/src/components/Markers/PopupMarker.tsx index fd4707483c..46e7e7ce18 100644 --- a/packages/ui-map-components/src/components/Markers/PopupMarker.js +++ b/packages/ui-map-components/src/components/Markers/PopupMarker.tsx @@ -5,10 +5,10 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; import Typography from '@material-ui/core/Typography'; import styled from 'styled-components'; import { Popup } from 'react-leaflet'; +import { MeasureData } from '../../types'; const TOP_BAR_HEIGHT = 60; const DARK_BACKGROUND_COLOR = 'rgb(43,45,56)'; @@ -60,19 +60,31 @@ const Button = styled.button` cursor: pointer; `; +interface PopupMarkerProps { + coordinates: MeasureData['coordinates']; + headerText: string; + buttonText?: string; + children?: React.ReactNode; + onDetailButtonClick?: () => void; + onOpen?: () => void; + onClose?: () => void; + popupRef?: React.Ref; + sidePanelWidth?: number; +} + export const PopupMarker = React.memo( ({ onDetailButtonClick, - onOpen, - onClose, - sidePanelWidth, + onOpen = () => null, + onClose = () => null, + sidePanelWidth = 0, buttonText, headerText, coordinates, children, popupRef, - }) => { - const displayCoordinates = coordinates.map(c => c.toFixed(5)).join(', '); + }: PopupMarkerProps) => { + const displayCoordinates = coordinates?.map((c: any) => c.toFixed(5)).join(', '); return ( null, - onClose: () => null, - popupRef: () => null, - onDetailButtonClick: null, - sidePanelWidth: 0, - coordinates: null, - children: null, - buttonText: null, -}; diff --git a/packages/ui-map-components/src/components/Markers/arrowIcons/DownArrow.js b/packages/ui-map-components/src/components/Markers/arrowIcons/DownArrow.tsx similarity index 83% rename from packages/ui-map-components/src/components/Markers/arrowIcons/DownArrow.js rename to packages/ui-map-components/src/components/Markers/arrowIcons/DownArrow.tsx index 309feaab29..fab87b80af 100644 --- a/packages/ui-map-components/src/components/Markers/arrowIcons/DownArrow.js +++ b/packages/ui-map-components/src/components/Markers/arrowIcons/DownArrow.tsx @@ -3,10 +3,9 @@ * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd */ import React from 'react'; -import PropTypes from 'prop-types'; import { IconContainer } from '../IconContainer'; -export const DownArrow = ({ scale }) => ( +export const DownArrow = (scale: number = 1) => ( ( /> ); - -DownArrow.propTypes = { - scale: PropTypes.number, -}; -DownArrow.defaultProps = { - scale: 1, -}; diff --git a/packages/ui-map-components/src/components/Markers/arrowIcons/RightArrow.js b/packages/ui-map-components/src/components/Markers/arrowIcons/RightArrow.tsx similarity index 82% rename from packages/ui-map-components/src/components/Markers/arrowIcons/RightArrow.js rename to packages/ui-map-components/src/components/Markers/arrowIcons/RightArrow.tsx index fc0e8f4f65..6be9d4d99e 100644 --- a/packages/ui-map-components/src/components/Markers/arrowIcons/RightArrow.js +++ b/packages/ui-map-components/src/components/Markers/arrowIcons/RightArrow.tsx @@ -3,10 +3,9 @@ * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd */ import React from 'react'; -import PropTypes from 'prop-types'; import { IconContainer } from '../IconContainer'; -export const RightArrow = ({ scale }) => ( +export const RightArrow = (scale: number = 1) => ( ( /> ); - -RightArrow.propTypes = { - scale: PropTypes.number, -}; -RightArrow.defaultProps = { - scale: 1, -}; diff --git a/packages/ui-map-components/src/components/Markers/arrowIcons/UpArrow.js b/packages/ui-map-components/src/components/Markers/arrowIcons/UpArrow.tsx similarity index 83% rename from packages/ui-map-components/src/components/Markers/arrowIcons/UpArrow.js rename to packages/ui-map-components/src/components/Markers/arrowIcons/UpArrow.tsx index fc0e122e9e..15a99c30cd 100644 --- a/packages/ui-map-components/src/components/Markers/arrowIcons/UpArrow.js +++ b/packages/ui-map-components/src/components/Markers/arrowIcons/UpArrow.tsx @@ -3,10 +3,9 @@ * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd */ import React from 'react'; -import PropTypes from 'prop-types'; import { IconContainer } from '../IconContainer'; -export const UpArrow = ({ scale }) => ( +export const UpArrow = (scale: number = 1) => ( ( /> ); - -UpArrow.propTypes = { - scale: PropTypes.number, -}; -UpArrow.defaultProps = { - scale: 1, -}; diff --git a/packages/ui-map-components/src/components/Markers/arrowIcons/index.js b/packages/ui-map-components/src/components/Markers/arrowIcons/index.ts similarity index 100% rename from packages/ui-map-components/src/components/Markers/arrowIcons/index.js rename to packages/ui-map-components/src/components/Markers/arrowIcons/index.ts diff --git a/packages/ui-map-components/src/components/Markers/constants.js b/packages/ui-map-components/src/components/Markers/constants.ts similarity index 100% rename from packages/ui-map-components/src/components/Markers/constants.js rename to packages/ui-map-components/src/components/Markers/constants.ts diff --git a/packages/ui-map-components/src/components/Markers/disasterIcons/Cyclone.js b/packages/ui-map-components/src/components/Markers/disasterIcons/Cyclone.js deleted file mode 100644 index 91fc7a40c9..0000000000 --- a/packages/ui-map-components/src/components/Markers/disasterIcons/Cyclone.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Tupaia - * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd - */ -import React from 'react'; - -export const Cyclone = props => ( - - - -); diff --git a/packages/ui-map-components/src/components/Markers/disasterIcons/Earthquake.js b/packages/ui-map-components/src/components/Markers/disasterIcons/Earthquake.js deleted file mode 100644 index 7354bce54e..0000000000 --- a/packages/ui-map-components/src/components/Markers/disasterIcons/Earthquake.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Tupaia - * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd - */ -import React from 'react'; - -export const Earthquake = props => ( - - - - - - - -); diff --git a/packages/ui-map-components/src/components/Markers/disasterIcons/Flood.js b/packages/ui-map-components/src/components/Markers/disasterIcons/Flood.js deleted file mode 100644 index 7f935f5588..0000000000 --- a/packages/ui-map-components/src/components/Markers/disasterIcons/Flood.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Tupaia - * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd - */ -import React from 'react'; - -export const Flood = props => ( - - - - -); diff --git a/packages/ui-map-components/src/components/Markers/disasterIcons/Tsunami.js b/packages/ui-map-components/src/components/Markers/disasterIcons/Tsunami.js deleted file mode 100644 index 59c14465c4..0000000000 --- a/packages/ui-map-components/src/components/Markers/disasterIcons/Tsunami.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Tupaia - * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd - */ -import React from 'react'; - -export const Tsunami = props => ( - - - -); diff --git a/packages/ui-map-components/src/components/Markers/disasterIcons/Volcano.js b/packages/ui-map-components/src/components/Markers/disasterIcons/Volcano.js deleted file mode 100644 index 5c125fd828..0000000000 --- a/packages/ui-map-components/src/components/Markers/disasterIcons/Volcano.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Tupaia - * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd - */ -import React from 'react'; - -export const Volcano = props => ( - - - - -); diff --git a/packages/ui-map-components/src/components/Markers/disasterIcons/index.js b/packages/ui-map-components/src/components/Markers/disasterIcons/index.js deleted file mode 100644 index 71e1dbc319..0000000000 --- a/packages/ui-map-components/src/components/Markers/disasterIcons/index.js +++ /dev/null @@ -1,5 +0,0 @@ -export * from './Cyclone'; -export * from './Earthquake'; -export * from './Flood'; -export * from './Tsunami'; -export * from './Volcano'; diff --git a/packages/ui-map-components/src/components/Markers/index.js b/packages/ui-map-components/src/components/Markers/index.ts similarity index 100% rename from packages/ui-map-components/src/components/Markers/index.js rename to packages/ui-map-components/src/components/Markers/index.ts diff --git a/packages/ui-map-components/src/components/Markers/markerIcons.js b/packages/ui-map-components/src/components/Markers/markerIcons.tsx similarity index 73% rename from packages/ui-map-components/src/components/Markers/markerIcons.js rename to packages/ui-map-components/src/components/Markers/markerIcons.tsx index bf00e67524..acf61007f5 100644 --- a/packages/ui-map-components/src/components/Markers/markerIcons.js +++ b/packages/ui-map-components/src/components/Markers/markerIcons.tsx @@ -7,22 +7,26 @@ /* eslint-disable react/prop-types */ import L from 'leaflet'; -import React from 'react'; +import React, { ElementType } from 'react'; import PropTypes from 'prop-types'; import ReactDOMServer from 'react-dom/server'; import styled from 'styled-components'; import Warning from '@material-ui/icons/Warning'; import Help from '@material-ui/icons/Help'; import CheckBox from '@material-ui/icons/CheckBox'; +import { PointExpression } from 'leaflet'; import { ICON_BASE_SIZE } from './constants'; // from https://thenounproject.com/ochavisual/collection/ocha-humanitarian-icons/ -import { Cyclone, Earthquake, Tsunami, Volcano, Flood } from './disasterIcons'; import { UpArrow, DownArrow, RightArrow } from './arrowIcons'; import { BREWER_PALETTE, WHITE } from '../../constants'; import { IconContainer } from './IconContainer'; +import { Color, ColorKey } from '../../types'; +import { CssColor } from '@tupaia/types'; // allows passing a color to a material icon & scales it down a bit -const wrapMaterialIcon = Base => ({ color }) => ; +const wrapMaterialIcon = (Base: ElementType) => ({ color }: { color: Color }) => ( + +); const StyledSvgWrapper = styled.span` * { @@ -30,13 +34,21 @@ const StyledSvgWrapper = styled.span` } `; -const wrapSvgIcon = Base => ({ color }) => ( +const wrapSvgIcon = (Base: ElementType) => ({ color }: { color: Color }) => ( ); -const PinIcon = ({ color, scale }) => { +interface ScaleIconProps { + scale?: number; +} + +interface IconProps extends ScaleIconProps { + color: Color; +} + +const PinIcon = ({ color, scale = 1 }: IconProps) => { return ( { ); }; -const UpArrowIcon = ({ scale }) => { +const UpArrowIcon = ({ scale = 1 }: ScaleIconProps) => { return UpArrow(scale); }; -const RightArrowIcon = ({ scale }) => { +const RightArrowIcon = ({ scale = 1 }: ScaleIconProps) => { return RightArrow(scale); }; -const DownArrowIcon = ({ scale }) => { +const DownArrowIcon = ({ scale = 1 }: ScaleIconProps) => { return DownArrow(scale); }; -const HealthPinIcon = ({ color, scale }) => ( +const HealthPinIcon = ({ color, scale = 1 }: IconProps) => ( @@ -72,13 +84,13 @@ const HealthPinIcon = ({ color, scale }) => ( ); -const CircleIcon = ({ color, scale }) => ( +const CircleIcon = ({ color, scale = 1 }: IconProps) => ( ); -const RadiusIcon = ({ color, scale }) => ( +const RadiusIcon = ({ color, scale = 1 }: IconProps) => ( ( ); -const DottedCircle = ({ color, scale }) => ( +const DottedCircle = ({ color, scale = 1 }: IconProps) => ( ( ); -const SquareIcon = ({ color, scale, stroke = color }) => ( +const SquareIcon = ({ + color, + scale = 1, + stroke = color, +}: IconProps & { + stroke?: string; +}) => ( ( ); -const RingIcon = ({ color, scale }) => ( +const RingIcon = ({ color, scale = 1 }: IconProps) => ( ); const trianglePath = makePolygon(3, 3); -const TriangleIcon = ({ color, scale }) => ( +const TriangleIcon = ({ color, scale = 1 }: IconProps) => ( ); -const XIcon = ({ color, scale }) => ( +const XIcon = ({ color, scale = 1 }: IconProps) => ( ( ); -const HIcon = ({ color, h = 0.24, v = 0.2, scale }) => ( +const HIcon = ({ + color, + h = 0.24, + v = 0.2, + scale = 1, +}: IconProps & { + h?: number; + v?: number; +}) => ( ( ); -const FadedCircle = ({ color, scale }) => { +const FadedCircle = ({ color, scale = 1 }: IconProps) => { // Each color needs its own id - even though we're defining a new svg, the // ids we refer to them by are in document scope, not svg scope. // We replace non-word characters (punctuation and spaces) with '-' so that @@ -288,27 +314,20 @@ const icons = { checkbox: { Component: wrapMaterialIcon(CheckBox), }, - earthquake: { - Component: wrapSvgIcon(Earthquake), - }, - tsunami: { - Component: wrapSvgIcon(Tsunami), - }, - eruption: { - Component: wrapSvgIcon(Volcano), - }, - cyclone: { - Component: wrapSvgIcon(Cyclone), - }, - flood: { - Component: wrapSvgIcon(Flood), - }, hidden: { Component: () => null, }, }; -export const ICON_VALUES = Object.keys(icons); +type IconType = { + Component: ElementType; + iconAnchor?: number[]; + popupAnchor?: number[]; + [key: string]: unknown; +}; + +export type IconKey = keyof typeof icons; + export const SPECTRUM_ICON = 'fade'; export const UNKNOWN_ICON = 'empty'; export const DEFAULT_ICON = 'healthPin'; @@ -317,7 +336,7 @@ export const LEGEND_SHADING_ICON = 'square'; export const LEGEND_RADIUS_ICON = 'radius'; export const HIDDEN_ICON = 'hidden'; -function toLeaflet(icon, color, scale) { +function toLeaflet(icon: IconType, color?: string, scale: number = 1): L.DivIcon { const { Component, iconAnchor = [0.5 * ICON_BASE_SIZE, 0.5 * ICON_BASE_SIZE], // default to center point @@ -330,8 +349,8 @@ function toLeaflet(icon, color, scale) { return L.divIcon({ iconSize: [scaledIconSize, scaledIconSize], - iconAnchor: scaledIconAnchor, - popupAnchor: scaledPopupAnchor, + iconAnchor: scaledIconAnchor as PointExpression, + popupAnchor: scaledPopupAnchor as PointExpression, className: 'tupaia-simple', ...params, html: ReactDOMServer.renderToStaticMarkup(), @@ -339,62 +358,23 @@ function toLeaflet(icon, color, scale) { } // Returns jsx version of marker (for Legend rendering) -export function getMarkerForOption(iconKey, colorName, stroke) { - const icon = icons[iconKey] || icons.pin; - const color = BREWER_PALETTE[colorName] || colorName; +export function getMarkerForOption( + iconKey: IconKey | undefined, + colorName: Color, + stroke?: CssColor, +) { + const icon = icons[iconKey as IconKey] || icons.pin; + const color = BREWER_PALETTE[colorName as ColorKey] || colorName; return ; } // Return html version of marker (for Map rendering) -export function getMarkerForValue(iconKey, colorName, scale = 1) { - const icon = icons[iconKey] || icons.pin; - const color = BREWER_PALETTE[colorName] || colorName; +export function getMarkerForValue( + iconKey: IconKey | undefined, + colorName?: Color, + scale: number = 1, +) { + const icon = icons[iconKey as IconKey] || icons.pin; + const color = BREWER_PALETTE[colorName as ColorKey] || colorName; return toLeaflet(icon, color, scale); } - -const iconPropTypes = { - color: PropTypes.string.isRequired, - scale: PropTypes.number, -}; -const iconDefaultProps = { - scale: 1, -}; - -PinIcon.propTypes = iconPropTypes; -PinIcon.defaultProps = iconDefaultProps; -UpArrowIcon.propTypes = { - scale: PropTypes.number, -}; -UpArrowIcon.defaultProps = { - scale: 1, -}; -DownArrowIcon.propTypes = { - scale: PropTypes.number, -}; -DownArrowIcon.defaultProps = { - scale: 1, -}; -RightArrowIcon.propTypes = { - scale: PropTypes.number, -}; -RightArrowIcon.defaultProps = { - scale: 1, -}; -HealthPinIcon.propTypes = iconPropTypes; -HealthPinIcon.defaultProps = iconDefaultProps; -CircleIcon.propTypes = iconPropTypes; -CircleIcon.defaultProps = iconDefaultProps; -TriangleIcon.propTypes = iconPropTypes; -TriangleIcon.defaultProps = iconDefaultProps; -SquareIcon.propTypes = iconPropTypes; -SquareIcon.defaultProps = iconDefaultProps; -PentagonIcon.propTypes = iconPropTypes; -PentagonIcon.defaultProps = iconDefaultProps; -RingIcon.propTypes = iconPropTypes; -RingIcon.defaultProps = iconDefaultProps; -XIcon.propTypes = iconPropTypes; -XIcon.defaultProps = iconDefaultProps; -DottedCircle.propTypes = iconPropTypes; -DottedCircle.defaultProps = iconDefaultProps; -FadedCircle.propTypes = iconPropTypes; -FadedCircle.defaultProps = iconDefaultProps; diff --git a/packages/ui-map-components/src/components/PolygonLayer.js b/packages/ui-map-components/src/components/PolygonLayer.js deleted file mode 100644 index 4fe8a2a284..0000000000 --- a/packages/ui-map-components/src/components/PolygonLayer.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Tupaia - * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd - * - */ -import React from 'react'; -import PropTypes from 'prop-types'; -import { EntityPolygon } from './EntityPolygon'; - -export const PolygonLayer = ({ entities, Polygon }) => { - if (!entities) return null; - - return entities - .filter(e => Array.isArray(e.region)) - .map(e => ); -}; - -PolygonLayer.propTypes = { - Polygon: PropTypes.func, - entities: PropTypes.arrayOf( - PropTypes.shape({ - code: PropTypes.string, - name: PropTypes.string, - region: PropTypes.array, - }), - ), -}; - -PolygonLayer.defaultProps = { - entities: [], - Polygon: EntityPolygon, -}; diff --git a/packages/ui-map-components/src/components/PolygonLayer.tsx b/packages/ui-map-components/src/components/PolygonLayer.tsx new file mode 100644 index 0000000000..dfda64a1cb --- /dev/null +++ b/packages/ui-map-components/src/components/PolygonLayer.tsx @@ -0,0 +1,21 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + * + */ +import React from 'react'; +import { EntityPolygon } from './EntityPolygon'; +import { Entity } from '../types'; + +interface PolygonLayerProps { + entities?: Entity[]; + Polygon?: React.ComponentType<{ entity: Entity }>; +} + +export const PolygonLayer = ({ entities = [], Polygon = EntityPolygon }: PolygonLayerProps) => { + if (!entities) return null; + + return entities + .filter(e => Array.isArray(e.region)) + .map(e => ); +}; diff --git a/packages/ui-map-components/src/components/PopupDataItemList.js b/packages/ui-map-components/src/components/PopupDataItemList.js deleted file mode 100644 index 05e4eab251..0000000000 --- a/packages/ui-map-components/src/components/PopupDataItemList.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Tupaia - * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd - * - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { getFormattedInfo } from '../utils'; - -const getMetadata = (data, key) => { - if (data.metadata) { - return data.metadata; - } - const metadataKeys = Object.keys(data).filter(k => k.includes(`${key}_metadata`)); - return Object.fromEntries(metadataKeys.map(k => [k.replace(`${key}_metadata`, ''), data[k]])); -}; - -const PopupDataItem = ({ measureName, value }) => ( -
- {`${measureName}: `} - {value} -
-); - -PopupDataItem.propTypes = { - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - measureName: PropTypes.string.isRequired, -}; - -export const PopupDataItemList = ({ serieses, data }) => { - return serieses - .filter(series => !series.hideFromPopup) - .sort((measure1, measure2) => measure1.sortOrder - measure2.sortOrder) - .map(series => { - const { key, name, organisationUnit, ...otherConfigs } = series; - const metadata = getMetadata(data, key); - const { formattedValue, valueInfo } = getFormattedInfo(data, series, { - key, - metadata, - ...otherConfigs, - }); - return valueInfo.hideFromPopup ? null : ( - - ); - }) - .filter(popupItem => popupItem !== null); -}; - -PopupDataItemList.propTypes = { - data: PropTypes.object.isRequired, - serieses: PropTypes.arrayOf( - PropTypes.shape({ - key: PropTypes.string.isRequired, - name: PropTypes.string, - hideFromPopup: PropTypes.bool, - metadata: PropTypes.object, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - }), - ).isRequired, -}; - -PopupDataItemList.defaultTypes = { data: {} }; diff --git a/packages/ui-map-components/src/components/PopupDataItemList.tsx b/packages/ui-map-components/src/components/PopupDataItemList.tsx new file mode 100644 index 0000000000..6b6a6c4fa2 --- /dev/null +++ b/packages/ui-map-components/src/components/PopupDataItemList.tsx @@ -0,0 +1,47 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + * + */ + +import React from 'react'; +import { getFormattedInfo } from '../utils'; +import { Series, MeasureData } from '../types'; + +interface PopupDataItemProps { + measureName: Series['name']; + value: string; +} + +const PopupDataItem = ({ measureName, value }: PopupDataItemProps) => ( +
+ {`${measureName}: `} + {value} +
+); + +interface PopupDataItemListProps { + serieses: Series[]; + data?: MeasureData; +} + +export const PopupDataItemList = ({ + serieses, + data = {} as MeasureData, +}: PopupDataItemListProps) => { + return ( + <> + {serieses + .filter(series => !series.hideFromPopup) + .sort((measure1, measure2) => measure1.sortOrder - measure2.sortOrder) + .map(series => { + const { name } = series; + const { formattedValue, valueInfo } = getFormattedInfo(data, series); + return valueInfo.hideFromPopup ? null : ( + + ); + }) + .filter(popupItem => popupItem !== null)} + + ); +}; diff --git a/packages/ui-map-components/src/components/Table/MapTable.js b/packages/ui-map-components/src/components/Table/MapTable.js deleted file mode 100644 index ed9a11446d..0000000000 --- a/packages/ui-map-components/src/components/Table/MapTable.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Tupaia - * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd - */ -import React from 'react'; -import PropTypes from 'prop-types'; -import { DataTable } from '@tupaia/ui-components'; -import { getMapTableData } from './getMapTableData'; - -export const MapTable = ({ serieses, measureData, className }) => { - const { columns, data } = getMapTableData(serieses, measureData); - - return ; -}; - -MapTable.propTypes = { - measureData: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.string, - }), - ), - serieses: PropTypes.arrayOf( - PropTypes.shape({ - name: PropTypes.string, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - }), - ), - className: PropTypes.string, -}; - -MapTable.defaultProps = { - measureData: [], - serieses: [], - className: null, -}; diff --git a/packages/ui-map-components/src/components/Table/MapTable.tsx b/packages/ui-map-components/src/components/Table/MapTable.tsx new file mode 100644 index 0000000000..bf00b60299 --- /dev/null +++ b/packages/ui-map-components/src/components/Table/MapTable.tsx @@ -0,0 +1,20 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ +import React from 'react'; +import { DataTable } from '@tupaia/ui-components'; +import { getMapTableData } from './getMapTableData'; +import { MeasureData, Series } from '../../types'; + +interface MapTableProps { + serieses: Series[]; + measureData: MeasureData[]; + className?: string; +} + +export const MapTable = ({ serieses = [], measureData = [], className }: MapTableProps) => { + const { columns, data } = getMapTableData(serieses, measureData); + + return ; +}; diff --git a/packages/ui-map-components/src/components/Table/getMapTableData.js b/packages/ui-map-components/src/components/Table/getMapTableData.tsx similarity index 76% rename from packages/ui-map-components/src/components/Table/getMapTableData.js rename to packages/ui-map-components/src/components/Table/getMapTableData.tsx index 76afe10dea..c59f79e727 100644 --- a/packages/ui-map-components/src/components/Table/getMapTableData.js +++ b/packages/ui-map-components/src/components/Table/getMapTableData.tsx @@ -5,13 +5,14 @@ import React, { useMemo } from 'react'; import styled from 'styled-components'; import { getFormattedInfo } from '../../utils'; +import { MeasureData, Series } from '../../types'; const FirstColumnCell = styled.span` font-weight: 500; text-align: left; `; -const processColumns = serieses => { +const processColumns = (serieses: Series[]) => { if (!serieses) { return []; } @@ -25,14 +26,16 @@ const processColumns = serieses => { Header: 'Name', accessor: 'name', // eslint-disable-next-line react/prop-types - Cell: ({ value }) => {String(value)}, + Cell: ({ value }: { value: string | number | boolean | undefined }) => ( + {String(value)} + ), }, ...configColumns, { Header: 'Most Recent Data Date', accessor: 'submissionDate' }, ]; }; -const processData = (serieses, measureData) => { +const processData = (serieses: Series[], measureData: MeasureData[]) => { if (!measureData || !serieses) { return []; } @@ -51,7 +54,7 @@ const processData = (serieses, measureData) => { }); }; -export const getMapTableData = (serieses, measureData) => { +export const getMapTableData = (serieses: Series[], measureData: MeasureData[]) => { const columns = useMemo(() => processColumns(serieses), [JSON.stringify(serieses)]); const data = useMemo(() => processData(serieses, measureData), [ JSON.stringify(serieses), diff --git a/packages/ui-map-components/src/components/Table/index.js b/packages/ui-map-components/src/components/Table/index.ts similarity index 100% rename from packages/ui-map-components/src/components/Table/index.js rename to packages/ui-map-components/src/components/Table/index.ts diff --git a/packages/ui-map-components/src/components/Table/useMapDataExport.js b/packages/ui-map-components/src/components/Table/useMapDataExport.tsx similarity index 59% rename from packages/ui-map-components/src/components/Table/useMapDataExport.js rename to packages/ui-map-components/src/components/Table/useMapDataExport.tsx index 3bed1d2908..fef3969166 100644 --- a/packages/ui-map-components/src/components/Table/useMapDataExport.js +++ b/packages/ui-map-components/src/components/Table/useMapDataExport.tsx @@ -5,8 +5,15 @@ import { useDataTableExport } from '@tupaia/ui-components'; import { getMapTableData } from './getMapTableData'; +import { Series, MeasureData } from '../../types'; -export const useMapDataExport = (serieses, measureData, title, startDate, endDate) => { +export const useMapDataExport = ( + serieses: Series[], + measureData: MeasureData[], + title: string, + startDate: Series['startDate'], + endDate: Series['endDate'], +) => { const { columns, data } = getMapTableData(serieses, measureData); return useDataTableExport(columns, data, title, startDate, endDate); }; diff --git a/packages/ui-map-components/src/components/TileLayer.js b/packages/ui-map-components/src/components/TileLayer.tsx similarity index 57% rename from packages/ui-map-components/src/components/TileLayer.js rename to packages/ui-map-components/src/components/TileLayer.tsx index 2a4154bc6b..23a041a9fd 100644 --- a/packages/ui-map-components/src/components/TileLayer.js +++ b/packages/ui-map-components/src/components/TileLayer.tsx @@ -5,40 +5,38 @@ */ import React, { useRef, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import { TileLayer as LeafletTileLayer, LayerGroup, AttributionControl } from 'react-leaflet'; +import { TileLayer as LeafletTileLayer } from 'leaflet'; +import { TileLayer as ReactLeafletTileLayer, LayerGroup, AttributionControl } from 'react-leaflet'; // Taken from https://www.mapbox.com/help/how-attribution-works/#other-mapping-frameworks. const attribution = 'Leaflet © Mapbox © OpenStreetMap Improve this map'; -export const TileLayer = ({ tileSetUrl, showAttribution }) => { - const tileLayer = useRef(null); +interface TileLayerProps { + showAttribution?: boolean; + tileSetUrl?: string; +} + +export const TileLayer = ({ + tileSetUrl = 'http://{s}.tile.osm.org/{z}/{x}/{y}.png', + showAttribution = true, +}: TileLayerProps) => { + const tileLayer = useRef(null); useEffect(() => { if (tileLayer) { - tileLayer.current.setUrl(tileSetUrl); + tileLayer?.current?.setUrl(tileSetUrl); } }, [tileSetUrl]); return ( - ); }; - -TileLayer.propTypes = { - showAttribution: PropTypes.bool, - tileSetUrl: PropTypes.string, -}; - -TileLayer.defaultProps = { - showAttribution: true, - tileSetUrl: 'http://{s}.tile.osm.org/{z}/{x}/{y}.png', -}; diff --git a/packages/ui-map-components/src/components/TilePicker/TileButton.js b/packages/ui-map-components/src/components/TilePicker/TileButton.tsx similarity index 80% rename from packages/ui-map-components/src/components/TilePicker/TileButton.js rename to packages/ui-map-components/src/components/TilePicker/TileButton.tsx index 7e897b6440..d260a88a0e 100644 --- a/packages/ui-map-components/src/components/TilePicker/TileButton.js +++ b/packages/ui-map-components/src/components/TilePicker/TileButton.tsx @@ -6,11 +6,10 @@ import React from 'react'; import styled from 'styled-components'; -import PropTypes from 'prop-types'; -import { ReferenceTooltip } from '@tupaia/ui-components'; +import { ReferenceProps, ReferenceTooltip } from '@tupaia/ui-components'; import Typography from '@material-ui/core/Typography'; import Button from '@material-ui/core/Button'; -import { tileSetShape } from './constants'; +import { SeriesValue } from '../../types'; const StyledButton = styled(Button)` position: relative; @@ -80,7 +79,22 @@ const TileLabel = styled(Typography)` padding: 0.5rem 0.75rem; `; -export const TileButton = React.memo(({ tileSet, isActive, onChange }) => ( +// Types for a tileset +export type TileSet = { + key: string; + label: string; + thumbnail: string; + reference?: ReferenceProps; + legendItems?: SeriesValue[]; +}; + +interface TileButtonProps { + tileSet: TileSet; + isActive?: boolean; + onChange: (tileSetKey: string) => void; +} + +export const TileButton = React.memo(({ tileSet, isActive = false, onChange }: TileButtonProps) => ( onChange(tileSet.key)} className={isActive ? 'active' : ''}> @@ -91,13 +105,3 @@ export const TileButton = React.memo(({ tileSet, isActive, onChange }) => ( )); - -TileButton.propTypes = { - tileSet: PropTypes.shape(tileSetShape).isRequired, - isActive: PropTypes.bool, - onChange: PropTypes.func.isRequired, -}; - -TileButton.defaultProps = { - isActive: false, -}; diff --git a/packages/ui-map-components/src/components/TilePicker/TileControl.js b/packages/ui-map-components/src/components/TilePicker/TileControl.tsx similarity index 71% rename from packages/ui-map-components/src/components/TilePicker/TileControl.js rename to packages/ui-map-components/src/components/TilePicker/TileControl.tsx index f136f83ad3..ad05d92818 100644 --- a/packages/ui-map-components/src/components/TilePicker/TileControl.js +++ b/packages/ui-map-components/src/components/TilePicker/TileControl.tsx @@ -6,14 +6,15 @@ import React from 'react'; import styled from 'styled-components'; -import PropTypes from 'prop-types'; import Button from '@material-ui/core/Button'; import Box from '@material-ui/core/Box'; import Typography from '@material-ui/core/Typography'; import RightIcon from '@material-ui/icons/KeyboardArrowRight'; -import { tileSetShape } from './constants'; +import { TileSet } from './TileButton'; -const StyledButton = styled(Button)` +const StyledButton = styled(Button)<{ + active: string; +}>` display: block; pointer-events: auto; box-shadow: none; @@ -91,33 +92,32 @@ const LegendLabel = styled(Typography)` margin-bottom: 0.3rem; `; -export const TileControl = React.memo(({ tileSet, isActive, ...props }) => ( - - - tile - - - - - - {tileSet.legendItems && ( - - {tileSet.legendItems.map(item => ( - - - {item.label} - - ))} - - )} - -)); - -TileControl.propTypes = { - tileSet: PropTypes.shape(tileSetShape).isRequired, - isActive: PropTypes.bool, -}; +interface TileControlProps { + tileSet: TileSet; + isActive?: boolean; + [key: string]: any; +} -TileControl.defaultProps = { - isActive: false, -}; +export const TileControl = React.memo( + ({ tileSet, isActive = false, ...props }: TileControlProps) => ( + + + tile + + + + + + {tileSet.legendItems && ( + + {tileSet.legendItems.map(item => ( + + + {item.label} + + ))} + + )} + + ), +); diff --git a/packages/ui-map-components/src/components/TilePicker/TilePicker.js b/packages/ui-map-components/src/components/TilePicker/TilePicker.tsx similarity index 53% rename from packages/ui-map-components/src/components/TilePicker/TilePicker.js rename to packages/ui-map-components/src/components/TilePicker/TilePicker.tsx index 4ac7cb9fdb..d4c57d90a5 100644 --- a/packages/ui-map-components/src/components/TilePicker/TilePicker.js +++ b/packages/ui-map-components/src/components/TilePicker/TilePicker.tsx @@ -5,11 +5,9 @@ import React, { useState } from 'react'; import styled from 'styled-components'; -import PropTypes from 'prop-types'; import ClickAwayListener from '@material-ui/core/ClickAwayListener'; -import { TileButton } from './TileButton'; +import { TileButton, TileSet } from './TileButton'; import { TileControl } from './TileControl'; -import { tileSetShape } from './constants'; import { createScaleKeyFrameAnimation } from './keyFrames'; const Container = styled.div` @@ -65,40 +63,38 @@ const TileList = styled.div` } `; -export const TilePicker = React.memo(({ tileSets, activeTileSet, onChange, className }) => { - const [open, setOpen] = useState(false); - return ( - setOpen(false)}> - - - setOpen(current => !current)} - /> - - - {tileSets.map(tileSet => ( - - ))} - - - - ); -}); - -TilePicker.propTypes = { - tileSets: PropTypes.arrayOf(PropTypes.shape(tileSetShape)).isRequired, - activeTileSet: PropTypes.shape(tileSetShape).isRequired, - onChange: PropTypes.func.isRequired, - className: PropTypes.string, -}; +interface TilePickerProps { + tileSets: TileSet[]; + activeTileSet: TileSet; + onChange: (tileSetKey: TileSet['key']) => void; + className?: string; +} -TilePicker.defaultProps = { - className: null, -}; +export const TilePicker = React.memo( + ({ tileSets, activeTileSet, onChange, className }: TilePickerProps) => { + const [open, setOpen] = useState(false); + return ( + setOpen(false)}> + + + setOpen(current => !current)} + /> + + + {tileSets.map(tileSet => ( + + ))} + + + + ); + }, +); diff --git a/packages/ui-map-components/src/components/TilePicker/constants.js b/packages/ui-map-components/src/components/TilePicker/constants.ts similarity index 100% rename from packages/ui-map-components/src/components/TilePicker/constants.js rename to packages/ui-map-components/src/components/TilePicker/constants.ts diff --git a/packages/ui-map-components/src/components/TilePicker/index.js b/packages/ui-map-components/src/components/TilePicker/index.ts similarity index 100% rename from packages/ui-map-components/src/components/TilePicker/index.js rename to packages/ui-map-components/src/components/TilePicker/index.ts diff --git a/packages/ui-map-components/src/components/TilePicker/keyFrames.js b/packages/ui-map-components/src/components/TilePicker/keyFrames.ts similarity index 96% rename from packages/ui-map-components/src/components/TilePicker/keyFrames.js rename to packages/ui-map-components/src/components/TilePicker/keyFrames.ts index 7259fb3399..5eb3f5e3d1 100644 --- a/packages/ui-map-components/src/components/TilePicker/keyFrames.js +++ b/packages/ui-map-components/src/components/TilePicker/keyFrames.ts @@ -4,7 +4,7 @@ * */ -function ease(v, pow = 3) { +function ease(v: number, pow = 3) { return 1 - Math.pow(1 - v, pow); } diff --git a/packages/ui-map-components/src/components/index.js b/packages/ui-map-components/src/components/index.ts similarity index 100% rename from packages/ui-map-components/src/components/index.js rename to packages/ui-map-components/src/components/index.ts diff --git a/packages/ui-map-components/src/constants/colors.js b/packages/ui-map-components/src/constants/colors.ts similarity index 100% rename from packages/ui-map-components/src/constants/colors.js rename to packages/ui-map-components/src/constants/colors.ts diff --git a/packages/ui-map-components/src/constants/constants.js b/packages/ui-map-components/src/constants/constants.ts similarity index 69% rename from packages/ui-map-components/src/constants/constants.js rename to packages/ui-map-components/src/constants/constants.ts index b1b62f4c7b..568a9a3344 100644 --- a/packages/ui-map-components/src/constants/constants.js +++ b/packages/ui-map-components/src/constants/constants.ts @@ -3,6 +3,7 @@ * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd * */ +import { LatLngBoundsLiteral } from 'leaflet'; // Note: There's a little bit of a hack going on here, the bounds[0] for explore are actually [6.5, 110] // However in order to trigger the map to re-render we set them slightly adjusted as [6.5001, 110] @@ -10,7 +11,7 @@ export const DEFAULT_BOUNDS = [ [6.5001, 110], [-40, 204.5], -]; +] as LatLngBoundsLiteral; export const MAPBOX_TOKEN = 'pk.eyJ1Ijoic3Vzc29sIiwiYSI6ImNqNHMwOW02MzFhaGIycXRjMnZ1dXFlN2gifQ.1sAg5w7hYU7e3LtJM0-hSg'; @@ -31,11 +32,21 @@ export const TILE_SETS = [ }, ]; -export const SCALE_TYPES = { - PERFORMANCE: 'performance', - PERFORMANCE_DESC: 'performanceDesc', - NEUTRAL: 'neutral', - NEUTRAL_REVERSE: 'neutralReverse', - TIME: 'time', - GPI: 'gpi', -}; +export enum SCALE_TYPES { + PERFORMANCE = 'performance', + PERFORMANCE_DESC = 'performanceDesc', + NEUTRAL = 'neutral', + NEUTRAL_REVERSE = 'neutralReverse', + TIME = 'time', + GPI = 'gpi', +} + +export enum MEASURE_TYPES { + ICON = 'icon', + COLOR = 'color', + RADIUS = 'radius', + SPECTRUM = 'spectrum', + SHADING = 'shading', + SHADED_SPECTRUM = 'shaded-spectrum', + POPUP_ONLY = 'popup-only', +} diff --git a/packages/ui-map-components/src/constants/index.js b/packages/ui-map-components/src/constants/index.ts similarity index 100% rename from packages/ui-map-components/src/constants/index.js rename to packages/ui-map-components/src/constants/index.ts diff --git a/packages/ui-map-components/src/index.js b/packages/ui-map-components/src/index.ts similarity index 100% rename from packages/ui-map-components/src/index.js rename to packages/ui-map-components/src/index.ts diff --git a/packages/ui-map-components/src/types/helpers.ts b/packages/ui-map-components/src/types/helpers.ts new file mode 100644 index 0000000000..c9cbd71921 --- /dev/null +++ b/packages/ui-map-components/src/types/helpers.ts @@ -0,0 +1,6 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ + +export type ValueOf = T[keyof T]; diff --git a/packages/ui-map-components/src/types/index.d.ts b/packages/ui-map-components/src/types/index.d.ts new file mode 100644 index 0000000000..d4141560ad --- /dev/null +++ b/packages/ui-map-components/src/types/index.d.ts @@ -0,0 +1,5 @@ +export * from './helpers'; +export * from './leaflet-config'; +export * from './legend'; +export * from './series'; +export * from './types'; diff --git a/packages/ui-map-components/src/types/leaflet-config.d.ts b/packages/ui-map-components/src/types/leaflet-config.d.ts new file mode 100644 index 0000000000..02152179bd --- /dev/null +++ b/packages/ui-map-components/src/types/leaflet-config.d.ts @@ -0,0 +1,19 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ + +import { Layer as LeafletLayer, LayerOptions as LeafletLayerOptions } from 'leaflet'; + +/** + * Overrides to leaflet types to handle anything not covered by the built in types + */ +declare module 'leaflet' { + export interface LayerOptions extends LeafletLayerOptions { + direction?: string; + } + + export interface Layer extends LeafletLayer { + update: () => void; + } +} diff --git a/packages/ui-map-components/src/types/legend.d.ts b/packages/ui-map-components/src/types/legend.d.ts new file mode 100644 index 0000000000..d0b106c6d6 --- /dev/null +++ b/packages/ui-map-components/src/types/legend.d.ts @@ -0,0 +1,22 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ + +import { MarkerSeries, SpectrumSeries, Value } from './series'; + +export type LegendProps = { + setValueHidden: (dataKey?: string, value: Value, hidden: boolean) => void; + hiddenValues: Record>; +}; + +export type MarkerLegendProps = LegendProps & { + series: MarkerSeries; + hasIconLayer: boolean; + hasRadiusLayer: boolean; + hasColorLayer: boolean; +}; + +export type SpectrumLegendProps = LegendProps & { + series: SpectrumSeries; +}; diff --git a/packages/ui-map-components/src/types/react-leaflet-config.d.ts b/packages/ui-map-components/src/types/react-leaflet-config.d.ts new file mode 100644 index 0000000000..a3950238fd --- /dev/null +++ b/packages/ui-map-components/src/types/react-leaflet-config.d.ts @@ -0,0 +1,15 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ + +import { MapContainerProps as ReactLeafletMapContainerProps } from 'react-leaflet'; + +/** + * Overrides to react-leaflet types to handle anything not covered by the built in types + */ +declare module 'react-leaflet' { + export interface MapContainerProps extends ReactLeafletMapContainerProps { + onClick?: (event: any) => void; + } +} diff --git a/packages/ui-map-components/src/types/series.d.ts b/packages/ui-map-components/src/types/series.d.ts new file mode 100644 index 0000000000..f30fa3d634 --- /dev/null +++ b/packages/ui-map-components/src/types/series.d.ts @@ -0,0 +1,62 @@ +import { VALUE_TYPES } from '@tupaia/utils'; +import { IconKey } from '../components'; +import { ColorScheme } from '../utils'; +import { MeasureType, OrgUnitCode, ScaleType } from './types'; + +const ValueTypes = { ...VALUE_TYPES } as const; +export type Value = string | number | null | undefined; + +export type SeriesValue = { + value: Value | Value[]; + name: string; + hideFromLegend?: boolean; + icon?: IconKey; + color: string; + label?: string; + hideFromPopup?: boolean; +}; + +export type SeriesValueMapping = { + null: SeriesValue; + [key: string]: SeriesValue; +}; + +export type BaseSeries = { + name: string; + key: string; + values: SeriesValue[]; + valueMapping: SeriesValueMapping; + hideFromLegend?: boolean; + type: MeasureType; + hideByDefault?: Record; + displayedValueKey?: string; + color: string; + radius?: number; + hideFromPopup?: boolean; + metadata: object; + organisationUnit?: OrgUnitCode; + sortOrder: number; + popupHeaderFormat?: string; + valueType?: ValueOf; + startDate: string; + endDate: string; +}; + +export type MarkerSeries = BaseSeries & { + icon?: IconKey; +}; + +export type SpectrumSeries = BaseSeries & { + scaleColorScheme: ColorScheme; + min: number; + max: number; + scaleType: ScaleType; + dataKey?: string; + noDataColour?: string; + scaleBounds?: { + left: number; + right: number; + }; +}; + +export type Series = MarkerSeries & SpectrumSeries; diff --git a/packages/ui-map-components/src/types/types.d.ts b/packages/ui-map-components/src/types/types.d.ts new file mode 100644 index 0000000000..73c590b6e1 --- /dev/null +++ b/packages/ui-map-components/src/types/types.d.ts @@ -0,0 +1,67 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ + +import { CircleMarkerProps, PolygonProps } from 'react-leaflet'; +import { LatLngExpression, LatLngBoundsExpression } from 'leaflet'; +import { Entity as TupaiaEntity, CssColor } from '@tupaia/types'; +import { ReferenceProps } from '@tupaia/ui-components'; +import { VALUE_TYPES } from '@tupaia/utils'; +import { MEASURE_TYPES, SCALE_TYPES, BREWER_PALETTE } from '../constants'; +import { Color } from './types'; +import { MarkerProps } from './marker-types'; +import { DataValue } from './legend'; +import { ValueOf } from './helpers'; +import { ReactNode } from 'react'; +import { IconKey } from '../components'; + +export type ColorKey = keyof typeof BREWER_PALETTE; +export type Color = ColorKey | 'transparent' | CssColor; + +export type ScaleType = `${SCALE_TYPES}`; +export type MeasureType = `${MEASURE_TYPES}`; +export type OrgUnitCode = string | undefined; + +export type Location = { + bounds: LatLngBoundsExpression; + type?: string | null; + point?: LatLngExpression; + region: PolygonProps['positions']; +}; + +// A generic data item for anything that has an 'organisationUnitCode' property +export type GenericDataItem = { + [key: string]: any; + organisationUnitCode: OrgUnitCode; +}; + +// Extend the base TupaiaEntity type with more details about the entity, including leaflet specific formatting +export type Entity = TupaiaEntity & + GenericDataItem & { + region?: PolygonProps['positions']; + location?: Location; + }; + +// Types for markers +export type MarkerProps = { + radius?: number | string; + color?: Color; + children?: ReactNode; + coordinates: CircleMarkerProps['center']; + scale?: number; + handleClick?: (e: any) => void; + icon?: IconKey; +}; + +// Types for general measure data, with geometry +export type MeasureData = Omit & + MarkerProps & + Entity & { + isHidden?: boolean; + icon?: string; + photoUrl?: string; + value?: number | string; + submissionDate?: string | Date; + positions?: PolygonProps['positions']; //allow this to be optional because of the loose types of measure data + }; diff --git a/packages/ui-map-components/src/utils/index.js b/packages/ui-map-components/src/utils/index.ts similarity index 100% rename from packages/ui-map-components/src/utils/index.js rename to packages/ui-map-components/src/utils/index.ts diff --git a/packages/ui-map-components/src/utils/markerColors.js b/packages/ui-map-components/src/utils/markerColors.ts similarity index 66% rename from packages/ui-map-components/src/utils/markerColors.js rename to packages/ui-map-components/src/utils/markerColors.ts index 55d1c3582e..b136591851 100644 --- a/packages/ui-map-components/src/utils/markerColors.js +++ b/packages/ui-map-components/src/utils/markerColors.ts @@ -16,6 +16,7 @@ import { TIME_COLOR_SCHEME, GPI_COLOR_SCHEME, } from '../constants'; +import { Color, ColorKey, ScaleType } from '../types'; const COLOR_SCHEME_TO_FUNCTION = { [DEFAULT_COLOR_SCHEME]: getHeatmapColor, @@ -34,34 +35,42 @@ const SCALE_TYPE_TO_COLOR_SCHEME = { [SCALE_TYPES.GPI]: GPI_COLOR_SCHEME, }; +export type ColorScheme = keyof typeof COLOR_SCHEME_TO_FUNCTION; + /** * Helper function just to point the spectrum type to the correct colours * - * @param {constant} scaleType one of the SCALE_TYPE* constants - * @param {number | string} value a number in range [0..1] representing percentage or - * a string of a date within a range specified by [min, max] - * @param {number | string} min the lowest number or a string representing earliest date in a range - * @param {number | string} max the highest number or a string representing lastest date in a range - * @param {string} noDataColour css hsl string, e.g. `hsl(value, 100%, 50%)` for null value - * @returns {style} css hsl string, e.g. `hsl(value, 100%, 50%)` */ -export function resolveSpectrumColour(scaleType, scaleColorScheme, value, min, max, noDataColour) { +export function resolveSpectrumColour( + scaleType: ScaleType, + scaleColorScheme: ColorScheme, + value: number | null, // a number in range [0..1] representing percentage or a string of a date within a range specified by [min, max] + min: number | string, // the lowest number or a string representing earliest date in a range + max: number | string, // the highest number or a string representing latest date in a range + noDataColour?: string, // css hsl string, e.g. `hsl(value, 100%, 50%)` for null value +): string { if (value === null || (isNaN(value) && scaleType !== SCALE_TYPES.TIME)) return noDataColour || HEATMAP_UNKNOWN_COLOR; - const valueToColor = - COLOR_SCHEME_TO_FUNCTION[scaleColorScheme] || - COLOR_SCHEME_TO_FUNCTION[SCALE_TYPE_TO_COLOR_SCHEME[scaleType]] || - COLOR_SCHEME_TO_FUNCTION[DEFAULT_COLOR_SCHEME]; + const valueToColor = (COLOR_SCHEME_TO_FUNCTION[scaleColorScheme] || + COLOR_SCHEME_TO_FUNCTION[SCALE_TYPE_TO_COLOR_SCHEME[scaleType] as ColorScheme] || + COLOR_SCHEME_TO_FUNCTION[DEFAULT_COLOR_SCHEME]) as ( + value: number | null, + ...args: any[] + ) => string; switch (scaleType) { case SCALE_TYPES.PERFORMANCE_DESC: { - const percentage = value || value === 0 ? 1 - normaliseToPercentage(value, min, max) : null; - return valueToColor(percentage); + const percentage = + value || value === 0 + ? 1 - normaliseToPercentage(value, min as number, max as number) + : null; + return valueToColor(percentage as number); } case SCALE_TYPES.TIME: // if the value passed is a date locate it in the [min, max] range - if (isNaN(value)) return valueToColor(getTimeProportion(value, min, max), noDataColour); + if (isNaN(value)) + return valueToColor(getTimeProportion(value, min as string, max as string), noDataColour); return valueToColor(value, noDataColour); case SCALE_TYPES.GPI: @@ -70,11 +79,13 @@ export function resolveSpectrumColour(scaleType, scaleColorScheme, value, min, m case SCALE_TYPES.NEUTRAL: case SCALE_TYPES.NEUTRAL_REVERSE: default: - return valueToColor((value || value === 0) && normaliseToPercentage(value, min, max)); + return valueToColor( + value || value === 0 ? normaliseToPercentage(value, min as number, max as number) : null, + ); } } -const normaliseToPercentage = (value, min = 0, max = 1) => { +const normaliseToPercentage = (value: number, min: number = 0, max: number = 1) => { // Always clamp the result between 0 and 1 if (value < min) return 0; if (value > max) return 1; @@ -84,15 +95,14 @@ const normaliseToPercentage = (value, min = 0, max = 1) => { }; /** * Takes a value and return a hsl color string for use as a style - * - * @param {number} value Number in range [0..1] representing percentage - * @returns {style} css hsl string, e.g. `hsl(value, 100%, 50%)` */ -export function getPerformanceHeatmapColor(value) { +export function getPerformanceHeatmapColor( + value: number, // Number in range [0..1] representing percentage +): string { return `hsl(${Math.floor(value * 100)}, 100%, 50%)`; } -const getTimeProportion = (value, min, max) => { +const getTimeProportion = (value: number, min: string, max: string) => { if (!value || !isNaN(value)) return null; const range = moment(max).diff(min, 'days'); const valueAsMoment = moment(value); @@ -102,11 +112,11 @@ const getTimeProportion = (value, min, max) => { /** * Takes a value and return a hsl color string for use as a style - * - * @param {number} value Number in range [0..1] representing percentage - * @returns {style} css hsl string, e.g. `hsl(value, 100%, 50%)` */ -export function getTimeHeatmapColor(value, noDataColour) { +export function getTimeHeatmapColor( + value: number, // Number in range [0..1] representing percentage + noDataColour?: string, +): string { if (value === null || isNaN(value)) return noDataColour || HEATMAP_UNKNOWN_COLOR; return `hsl(${100 - Math.floor(value * 100)}, 100%, 50%)`; } @@ -127,30 +137,34 @@ const HEATMAP_DEFAULT_RGB_SET = [ * Takes a value and returns color string for use as a style * to match the map/legend style shown here: * https://commons.wikimedia.org/wiki/File:South_Africa_2011_population_density_map.svg - * - * @param {number} value A value in the range [0..1] representing a percentage - * @param {default} if default is true, return lightest to darkest colour, otherwise return darkest to lightest - * @returns {style} css rgb string, e.g. `rgb(0,0,0)` */ -function getHeatmapColorByOrder({ value, swapColor = false, colorSet = HEATMAP_DEFAULT_RGB_SET }) { +function getHeatmapColorByOrder({ + value, + swapColor = false, + colorSet = HEATMAP_DEFAULT_RGB_SET, +}: { + value: number; + swapColor?: boolean; + colorSet?: string[]; +}): string { const difference = value - 0.15; const index = difference < 0 ? 0 : Math.floor(difference / 0.1) + 1; const indexInRange = index > colorSet.length - 1 ? colorSet.length - 1 : index; return !swapColor ? colorSet[indexInRange] : colorSet[colorSet.length - indexInRange - 1]; } -export function getHeatmapColor(value) { +export function getHeatmapColor(value: number) { return getHeatmapColorByOrder({ value, swapColor: false }); } -export function getReverseHeatmapColor(value) { +export function getReverseHeatmapColor(value: number) { return getHeatmapColorByOrder({ value, swapColor: true }); } // Use a palette color if named, otherwise just return the name. // This allows measures to still use hex codes and named colors not in the palette. -export function getColor(colorName) { - return BREWER_PALETTE[colorName] || colorName; +export function getColor(colorName: Color) { + return BREWER_PALETTE[colorName as ColorKey] || colorName; } /** @@ -184,15 +198,14 @@ const RED_COLOR_SET = [ * * A GPI between 0.97 - 1.03 is considered gender parity */ -const GPI_PARITY_UPPER_LIMIT = '1.03'; -const GPI_PARITY_LOWER_LIMIT = '0.97'; +const GPI_PARITY_UPPER_LIMIT = 1.03; +const GPI_PARITY_LOWER_LIMIT = 0.97; /** * - * @param value - GPI value type is number between 0 and 2. * @returns {string} */ -export function getGPIColor(value, min, max) { +export function getGPIColor(value: number, min?: number, max?: number): string { if (value > GPI_PARITY_UPPER_LIMIT) { const normalisedValue = normaliseToPercentage(Math.min(value, 2), GPI_PARITY_UPPER_LIMIT, max); return getHeatmapColorByOrder({ value: normalisedValue, colorSet: RED_COLOR_SET }); diff --git a/packages/ui-map-components/src/utils/markerFormats.js b/packages/ui-map-components/src/utils/markerFormats.ts similarity index 75% rename from packages/ui-map-components/src/utils/markerFormats.js rename to packages/ui-map-components/src/utils/markerFormats.ts index 7119541949..f893065a03 100644 --- a/packages/ui-map-components/src/utils/markerFormats.js +++ b/packages/ui-map-components/src/utils/markerFormats.ts @@ -4,7 +4,7 @@ * */ -import { formatDataValueByType } from '@tupaia/utils'; +import { VALUE_TYPES, formatDataValueByType } from '@tupaia/utils'; import { resolveSpectrumColour } from './markerColors'; import { YES_COLOR, @@ -15,6 +15,16 @@ import { SCALE_TYPES, } from '../constants'; import { SPECTRUM_ICON, DEFAULT_ICON, UNKNOWN_ICON } from '../components/Markers/markerIcons'; +import { + SeriesValue, + LegendProps, + MeasureData, + ScaleType, + Series, + SeriesValueMapping, + Value, + MeasureType, +} from '../types'; export const MEASURE_TYPE_ICON = 'icon'; export const MEASURE_TYPE_COLOR = 'color'; @@ -33,11 +43,11 @@ export const SPECTRUM_MEASURE_TYPES = [MEASURE_TYPE_SPECTRUM, MEASURE_TYPE_SHADE const SPECTRUM_SCALE_DEFAULT = { left: {}, right: {} }; const PERCENTAGE_SPECTRUM_SCALE_DEFAULT = { left: { max: 0 }, right: { min: 1 } }; -export function autoAssignColors(values) { +export function autoAssignColors(values: SeriesValue[]) { if (!values) return []; let autoIndex = 0; - const getColor = valueObject => { + const getColor = (valueObject: SeriesValue) => { if (!valueObject.name) { return BREWER_AUTO[autoIndex++]; } @@ -58,18 +68,18 @@ export function autoAssignColors(values) { })); } -export function createValueMapping(valueObjects, type) { - const mapping = {}; +export function createValueMapping(valueObjects: SeriesValue[], type: string) { + const mapping = {} as SeriesValueMapping; valueObjects.forEach(valueObject => { const { value } = valueObject; if (Array.isArray(value)) { value.forEach(v => { - mapping[v] = valueObject; + mapping[v as string] = valueObject; }); } else { - mapping[value] = valueObject; + mapping[value as string] = valueObject; } }); @@ -82,11 +92,11 @@ export function createValueMapping(valueObjects, type) { return mapping; } -const getNullValueMapping = type => { +const getNullValueMapping = (type: string) => { const baseMapping = { name: 'No data', value: MEASURE_VALUE_NULL, - }; + } as SeriesValue; switch (type) { case MEASURE_TYPE_ICON: @@ -103,7 +113,14 @@ const getNullValueMapping = type => { return baseMapping; }; -function getFormattedValue(value, type, valueInfo, scaleType, valueType, submissionDate) { +function getFormattedValue( + value: Value, + type: MeasureType, + valueInfo: SeriesValue, + scaleType: ScaleType, + valueType: Series['valueType'], + submissionDate: MeasureData['submissionDate'], +) { switch (type) { case MEASURE_TYPE_SPECTRUM: case MEASURE_TYPE_SHADED_SPECTRUM: @@ -124,7 +141,7 @@ function getFormattedValue(value, type, valueInfo, scaleType, valueType, submiss } } -export const getSpectrumScaleValues = (measureData, series) => { +export const getSpectrumScaleValues = (measureData: MeasureData[], series: Series) => { const { key, scaleType, startDate, endDate } = series; if (scaleType === SCALE_TYPES.TIME) { @@ -143,14 +160,20 @@ export const getSpectrumScaleValues = (measureData, series) => { return { min, max }; }; -const clampScaleValues = (dataBounds, series) => { - const { valueType, scaleBounds = {} } = series; +const clampScaleValues = ( + dataBounds: { + min: number; + max: number; + }, + series: Series, +) => { + const { valueType, scaleBounds } = series; const defaultScale = valueType === 'percentage' ? PERCENTAGE_SPECTRUM_SCALE_DEFAULT : SPECTRUM_SCALE_DEFAULT; - const leftBoundConfig = scaleBounds.left || defaultScale.left; - const rightBoundConfig = scaleBounds.right || defaultScale.right; + const leftBoundConfig = scaleBounds?.left || defaultScale.left; + const rightBoundConfig = scaleBounds?.right || defaultScale.right; const { min: minDataValue, max: maxDataValue } = dataBounds; return { @@ -159,7 +182,13 @@ const clampScaleValues = (dataBounds, series) => { }; }; -const clampValue = (value, config) => { +const clampValue = ( + value: number, + config: { + min?: number | 'auto'; + max?: number | 'auto'; + }, +) => { const { min, max } = config; let clampedValue = value; @@ -169,12 +198,19 @@ const clampValue = (value, config) => { return clampedValue; }; -export function flattenMeasureHierarchy(mapOverlayHierarchy) { - const results = []; - const flattenGroupedMeasure = ({ children }) => { +type Measure = { + children?: object[]; +}; + +interface MapOverlayHierarchyItem { + children: Measure[] | MapOverlayHierarchyItem[]; +} +export function flattenMeasureHierarchy(mapOverlayHierarchy: MapOverlayHierarchyItem[]) { + const results = [] as object[]; + const flattenGroupedMeasure = ({ children }: MapOverlayHierarchyItem) => { children.forEach(childObject => { if (childObject.children && childObject.children.length) { - flattenGroupedMeasure(childObject); + flattenGroupedMeasure(childObject as MapOverlayHierarchyItem); } else { results.push(childObject); } @@ -191,7 +227,11 @@ export function flattenMeasureHierarchy(mapOverlayHierarchy) { return results; } -const getIsHidden = (measureData, serieses, allHiddenValues) => +const getIsHidden = ( + measureData: MeasureData, + serieses: Series[], + allHiddenValues: Record>, +) => serieses .map(({ key, valueMapping, hideByDefault }) => { const value = measureData[key]; @@ -202,7 +242,7 @@ const getIsHidden = (measureData, serieses, allHiddenValues) => // use 'no data' value if value is null and there is a null mapping defined if (!value && typeof value !== 'number' && valueMapping.null) { - return hiddenValues.null || hiddenValues[valueMapping.null.value]; + return hiddenValues.null || hiddenValues[valueMapping.null.value as string]; } const matchedValue = valueMapping[value]; @@ -212,11 +252,11 @@ const getIsHidden = (measureData, serieses, allHiddenValues) => return hiddenValues.other; } - return hiddenValues[matchedValue.value]; + return hiddenValues[matchedValue.value as string]; }) .some(isHidden => isHidden); -function getValueInfo(value, valueMapping) { +function getValueInfo(value: Value, valueMapping: SeriesValueMapping) { // use 'no data' value if value is null and there is a null mapping defined if (!value && typeof value !== 'number' && valueMapping.null) { return { @@ -224,7 +264,7 @@ function getValueInfo(value, valueMapping) { }; } - const matchedValue = valueMapping[value]; + const matchedValue = valueMapping[value as string]; if (!matchedValue) { // use 'other' value @@ -238,13 +278,12 @@ function getValueInfo(value, valueMapping) { ...matchedValue, }; } - // For situations where we can only show one value, just show the value // of the first measure. -export const getSingleFormattedValue = (markerData, series) => +export const getSingleFormattedValue = (markerData: MeasureData, series: Series[]) => getFormattedInfo(markerData, series[0]).formattedValue; -export function getFormattedInfo(markerData, series) { +export function getFormattedInfo(markerData: MeasureData, series: Series) { const { key, valueMapping, type, displayedValueKey, scaleType, valueType } = series; const value = markerData[key]; const valueInfo = getValueInfo(value, valueMapping); @@ -278,17 +317,23 @@ export function getFormattedInfo(markerData, series) { } export function getMeasureDisplayInfo( - measureData = {}, - serieses, - hiddenValues = {}, - radiusScaleFactor = 1, + measureData: MeasureData = {}, + serieses: Series[], + hiddenValues: LegendProps['hiddenValues'] = {}, + radiusScaleFactor: number = 1, ) { const isHidden = getIsHidden(measureData, serieses, hiddenValues); const displayInfo = { isHidden, + } as { + color?: string; + icon?: string; + radius?: number; + isHidden: boolean; + originalValue?: SeriesValue['value']; }; - serieses.forEach(({ color, icon, radius }) => { + serieses.forEach(({ color, icon, radius }: Series) => { if (color) { displayInfo.color = color; } @@ -312,7 +357,7 @@ export function getMeasureDisplayInfo( displayInfo.color = displayInfo.color || valueInfo.color; break; case MEASURE_TYPE_RADIUS: - displayInfo.radius = valueInfo.value * radiusScaleFactor || 0; + displayInfo.radius = (valueInfo.value as number) * radiusScaleFactor || 0; displayInfo.color = displayInfo.color || valueInfo.color; break; case MEASURE_TYPE_SPECTRUM: @@ -322,7 +367,7 @@ export function getMeasureDisplayInfo( displayInfo.color = resolveSpectrumColour( scaleType, scaleColorScheme, - valueInfo.value || (valueInfo.value === 0 ? 0 : null), + (valueInfo.value as number) || (valueInfo.value === 0 ? 0 : null), min, max, noDataColour, @@ -355,19 +400,19 @@ export function getMeasureDisplayInfo( const MAX_ALLOWED_RADIUS = 1000; -export const calculateRadiusScaleFactor = measureData => { +export const calculateRadiusScaleFactor = (measureData: MeasureData[]) => { // Check if any of the radii in the dataset are larger than the max allowed // radius, and scale everything down proportionally if so. // (this needs to happen here instead of inside the circle marker component // because it needs to operate on the dataset level, not the datapoint level) const maxRadius = measureData - .map(d => parseInt(d.radius, 10) || 1) + .map(d => parseInt(d.radius as string, 10) || 1) .reduce((state, current) => Math.max(state, current), 0); return maxRadius < MAX_ALLOWED_RADIUS ? 1 : (1 / maxRadius) * MAX_ALLOWED_RADIUS; }; // Take a measureData array where the [key]: value is a number // and filters NaN values (e.g. undefined). -export function flattenNumericalMeasureData(measureData, key) { +export function flattenNumericalMeasureData(measureData: MeasureData[], key: string) { return measureData.map(v => parseFloat(v[key])).filter(x => !isNaN(x)); } diff --git a/packages/ui-map-components/tsconfig-build.json b/packages/ui-map-components/tsconfig-build.json new file mode 100644 index 0000000000..65a8078030 --- /dev/null +++ b/packages/ui-map-components/tsconfig-build.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "noEmit": false, + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "src/__tests__", + "src/stories/**/*" + ] +} diff --git a/packages/ui-map-components/tsconfig.json b/packages/ui-map-components/tsconfig.json new file mode 100644 index 0000000000..ceac796a45 --- /dev/null +++ b/packages/ui-map-components/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig-ts.json", + "compileOnSave": true, + "compilerOptions": { + "jsx": "react", + "allowJs": true, + "outDir": "dist", + "lib": [ + "es6", + "dom", + "es2019" + ], + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true + }, + "include": [ + "src/**/*", + "jest.config.ts", + "helpers/**/*" + ] +} diff --git a/packages/web-frontend/package.json b/packages/web-frontend/package.json index c3b9200376..1efcc6e8ef 100644 --- a/packages/web-frontend/package.json +++ b/packages/web-frontend/package.json @@ -22,14 +22,14 @@ "start-central-server": "yarn workspace @tupaia/central-server start-dev", "start-entity-server": "yarn workspace @tupaia/entity-server start-dev", "start-report-server": "yarn workspace @tupaia/report-server start-dev", - "start-web-config-server": "yarn workspace @tupaia/web-config-server start-dev", - "start-servers": "npm-run-all -c -l -p start-central-server start-entity-server start-report-server start-web-config-server", + "start-web-config-server": "yarn workspace @tupaia/web-config-server start-dev", + "start-servers": "npm-run-all -c -l -p start-central-server start-entity-server start-report-server start-web-config-server start-data-table-server", "start-data-table-server": "yarn workspace @tupaia/data-table-server start-dev", "storybook": "start-storybook -s public -p 6007", "test": "node scripts/test.js --env=jsdom", - "start-ui-components": "yarn workspace @tupaia/ui-components build-dev:watch", - "start-ui-chart-components": "yarn workspace @tupaia/ui-chart-components build-dev:watch", - "start-ui-map-components": "yarn workspace @tupaia/ui-map-components build-dev:watch", + "start-ui-components": "yarn workspace @tupaia/ui-components build -w", + "start-ui-chart-components": "yarn workspace @tupaia/ui-chart-components build -w", + "start-ui-map-components": "yarn workspace @tupaia/ui-map-components build -w", "start-frontend": "npm-run-all -c -l -p start-ui-components start-ui-chart-components start-ui-map-components start-dev" }, "resolutions": { diff --git a/yarn.lock b/yarn.lock index e2da077607..800799dba5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8769,10 +8769,10 @@ __metadata: "@tupaia/utils": 1.0.0 "@types/jest": ^29.5.1 "@types/lodash.throttle": ^4.1.7 - "@types/react-dom": ^18.2.4 + "@types/react-dom": ^16.9.18 "@types/react-router-dom": ^5.3.3 "@types/react-table": ^7.7.14 - "@types/styled-components": ^5.1.26 + "@types/styled-components": ^5.0.0 ace-builds: ^1.4.13 date-fns: ^2.12.0 faker: ^4.1.0 @@ -8808,13 +8808,19 @@ __metadata: "@material-ui/core": ^4.9.8 "@material-ui/icons": ^4.9.1 "@material-ui/styles": ^4.9.10 + "@mui/types": ^7.2.4 "@storybook/react": ^6.3.9 "@tupaia/ui-components": 1.0.0 "@tupaia/utils": 1.0.0 + "@types/jest": ^29.5.1 + "@types/leaflet": ^1.7.1 + "@types/react-dom": ^16.9.18 + "@types/styled-components": ^5.1.26 leaflet: ^1.7.1 moment: ^2.29.1 prop-types: ^15.7.2 react: ^16.13.1 + react-docgen-typescript-plugin: ^1.0.5 react-dom: ^16.13.1 react-leaflet: ^3.2.1 react-table: ^7.7.0 @@ -9304,6 +9310,13 @@ __metadata: languageName: node linkType: hard +"@types/geojson@npm:*": + version: 7946.0.10 + resolution: "@types/geojson@npm:7946.0.10" + checksum: 12c407c2dc93ecb26c08af533ee732f1506a9b29456616ba7ba1d525df96206c28ddf44a528f6a5415d7d22893e9d967420940a9c095ee5e539c1eba5fefc1f4 + languageName: node + linkType: hard + "@types/glob-base@npm:^0.3.0": version: 0.3.0 resolution: "@types/glob-base@npm:0.3.0" @@ -9538,6 +9551,15 @@ __metadata: languageName: node linkType: hard +"@types/leaflet@npm:^1.7.1": + version: 1.9.3 + resolution: "@types/leaflet@npm:1.9.3" + dependencies: + "@types/geojson": "*" + checksum: d12042edc3a043d074b4fd3b4c4800f479dfb01db3980d8c1151edbc70b23ca9e84ab7da72ce0d1c36c040f89ba1421bd36d9d9796ec3704a0d3c942985a87ae + languageName: node + linkType: hard + "@types/lodash.clonedeep@npm:^4.5.0": version: 4.5.7 resolution: "@types/lodash.clonedeep@npm:4.5.7" @@ -9932,12 +9954,12 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:^18.2.4": - version: 18.2.4 - resolution: "@types/react-dom@npm:18.2.4" +"@types/react-dom@npm:^16.9.18": + version: 16.9.19 + resolution: "@types/react-dom@npm:16.9.19" dependencies: - "@types/react": "*" - checksum: 8301f35cf1cbfec8c723e9477aecf87774e3c168bd457d353b23c45064737213d3e8008b067c6767b7b08e4f2b3823ee239242a6c225fc91e7f8725ef8734124 + "@types/react": ^16 + checksum: c696f137aba09be0ea87ad6e99a083607b4fc574857d92c264cc6518ee929ef7e94855362436c2ef5fefb70ebfc9b7413634bb3a2f3a8c2b7522521faaac7d62 languageName: node linkType: hard @@ -10041,6 +10063,17 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:^16": + version: 16.14.43 + resolution: "@types/react@npm:16.14.43" + dependencies: + "@types/prop-types": "*" + "@types/scheduler": "*" + csstype: ^3.0.2 + checksum: 10ce3f8b80eadd66178a53845bddf264fa109fcfe0a615a0dce7e027dc426768573e87aee3eba7c53a2a867e159bba928bd86cac92ba93c5bd0152a63a49ccad + languageName: node + linkType: hard + "@types/recharts@npm:^1.8.24": version: 1.8.24 resolution: "@types/recharts@npm:1.8.24" @@ -10145,7 +10178,7 @@ __metadata: languageName: node linkType: hard -"@types/styled-components@npm:^5.1.26": +"@types/styled-components@npm:^5.0.0, @types/styled-components@npm:^5.1.26": version: 5.1.26 resolution: "@types/styled-components@npm:5.1.26" dependencies: