diff --git a/packages/tupaia-web/src/api/queries/index.ts b/packages/tupaia-web/src/api/queries/index.ts index bd83e52712..786abb72aa 100644 --- a/packages/tupaia-web/src/api/queries/index.ts +++ b/packages/tupaia-web/src/api/queries/index.ts @@ -14,5 +14,5 @@ export { useEntities, useEntitiesWithLocation } from './useEntities'; export { useEmailVerification } from './useEmailVerification'; export { useDashboards } from './useDashboards'; export { useReport } from './useReport'; +export { useMapOverlayReport } from './useMapOverlayReport'; export { useMapOverlays } from './useMapOverlays'; -export { useMapOverlayData } from './useMapOverlayData'; diff --git a/packages/tupaia-web/src/api/queries/useEntities.ts b/packages/tupaia-web/src/api/queries/useEntities.ts index 133e72f312..3f84f95ef8 100644 --- a/packages/tupaia-web/src/api/queries/useEntities.ts +++ b/packages/tupaia-web/src/api/queries/useEntities.ts @@ -7,21 +7,25 @@ import { useQuery, QueryObserverOptions } from 'react-query'; import { EntityResponse } from '../../types'; import { get } from '../api'; +type EntitiesResponse = EntityResponse[]; + export const useEntities = ( projectCode?: string, entityCode?: string, axiosConfig?: AxiosRequestConfig, queryOptions?: QueryObserverOptions, ) => { - const enabled = - queryOptions?.enabled === undefined ? !!projectCode && !!entityCode : queryOptions.enabled; + let enabled = !!projectCode && !!entityCode; + + if (queryOptions?.enabled !== undefined) { + enabled = enabled && queryOptions.enabled; + } return useQuery( - ['entities', projectCode, entityCode, axiosConfig], - (): Promise => + ['entities', projectCode, entityCode, axiosConfig, queryOptions], + (): Promise => get(`entities/${projectCode}/${entityCode}`, { params: { - includeRoot: true, fields: [ 'parent_code', 'code', @@ -35,28 +39,39 @@ export const useEntities = ( }, ...axiosConfig, }), + { enabled, }, ); }; -export const useEntitiesWithLocation = (projectCode?: string, entityCode?: string) => - useEntities(projectCode, entityCode, { - params: { - includeRoot: true, - fields: [ - 'parent_code', - 'code', - 'name', - 'type', - 'bounds', - 'region', - 'point', - 'location_type', - 'image_url', - 'attributes', - 'child_codes', - ], +export const useEntitiesWithLocation = ( + projectCode?: string, + entityCode?: string, + axiosConfig?: AxiosRequestConfig, + queryOptions?: QueryObserverOptions, +) => + useEntities( + projectCode, + entityCode, + { + params: { + ...{ ...axiosConfig?.params }, + fields: [ + 'parent_code', + 'code', + 'name', + 'type', + 'bounds', + 'region', + 'point', + 'location_type', + 'image_url', + 'attributes', + 'child_codes', + ], + }, }, - }); + queryOptions, + ); diff --git a/packages/tupaia-web/src/api/queries/useEntity.ts b/packages/tupaia-web/src/api/queries/useEntity.ts index bf189d3a23..b3a6503ded 100644 --- a/packages/tupaia-web/src/api/queries/useEntity.ts +++ b/packages/tupaia-web/src/api/queries/useEntity.ts @@ -31,5 +31,6 @@ export const useEntity = (entityCode?: string) => { return entity; }, + { enabled: !!entityCode && !!projectCode }, ); }; diff --git a/packages/tupaia-web/src/api/queries/useMapOverlayData.ts b/packages/tupaia-web/src/api/queries/useMapOverlayData.ts deleted file mode 100644 index 05ecbd7eff..0000000000 --- a/packages/tupaia-web/src/api/queries/useMapOverlayData.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Tupaia - * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd - */ -import { useQuery } from 'react-query'; -import { momentToDateString } from '@tupaia/utils'; -import { get } from '../api'; -import { EntityCode, ProjectCode, SingleMapOverlayItem } from '../../types'; - -export const useMapOverlayData = ( - projectCode?: ProjectCode, - entityCode?: EntityCode, - mapOverlayCode?: SingleMapOverlayItem['code'], - legacy?: boolean, - params?: { - startDate?: string; - endDate?: string; - }, -) => { - // convert moment dates to date strings for the endpoint to use - const startDate = params?.startDate ? momentToDateString(params.startDate) : undefined; - const endDate = params?.startDate ? momentToDateString(params.endDate) : undefined; - const endpoint = legacy ? 'legacyMapOverlayReport' : 'report'; - return useQuery( - [endpoint, projectCode, entityCode, mapOverlayCode, startDate, endDate], - async () => { - return get(`${endpoint}/${mapOverlayCode}`, { - params: { - organisationUnitCode: entityCode, - projectCode, - shouldShowAllParentCountryResults: projectCode !== entityCode, // TODO: figure out the logic here for shouldShowAllParentCountryResults - startDate, - endDate, - }, - }); - }, - { - enabled: !!projectCode && !!entityCode && !!mapOverlayCode, - }, - ); -}; diff --git a/packages/tupaia-web/src/api/queries/useMapOverlayReport.ts b/packages/tupaia-web/src/api/queries/useMapOverlayReport.ts new file mode 100644 index 0000000000..6b6e06b609 --- /dev/null +++ b/packages/tupaia-web/src/api/queries/useMapOverlayReport.ts @@ -0,0 +1,120 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ +import { useQuery } from 'react-query'; +import { momentToDateString } from '@tupaia/utils'; +import { + autoAssignColors, + createValueMapping, + getSpectrumScaleValues, + SPECTRUM_MEASURE_TYPES, +} from '@tupaia/ui-map-components'; +import { get } from '../api'; +import { EntityCode, ProjectCode, SingleMapOverlayItem } from '../../types'; + +// make the response from the new endpoint look like the response from the legacy endpoint +const normaliseResponse = (measureDataResponse: any, overlay: SingleMapOverlayItem) => { + const { measureCode, measureLevel, displayType, dataElementCode, ...restOfOverlay } = overlay; + + const measureOptions = [ + { + measureLevel, + type: displayType, + key: dataElementCode || 'value', + ...restOfOverlay, + }, + ]; + + return { + measureCode, + measureLevel, + measureOptions, + serieses: measureOptions, + measureData: measureDataResponse, + }; +}; + +const formatMapOverlayData = (data: any) => { + const { serieses, measureData } = data; + + const processedSerieses = serieses.map((series: any) => { + const { values: mapOptionValues, type } = series; + const values = autoAssignColors(mapOptionValues); + const valueMapping = createValueMapping(values, type); + + if (SPECTRUM_MEASURE_TYPES.includes(type)) { + // for each spectrum, include the minimum and maximum values for + // use in the legend scale labels. + const { min, max } = getSpectrumScaleValues(measureData, series); + const noDataColour = '#c7c7c7'; + + return { + ...series, + values, + valueMapping, + min, + max, + noDataColour, + }; + } + + // If it is not a radius series and there is no icon set a default + if (series.type !== 'radius' && !series.icon) { + return { + ...series, + values, + valueMapping, + }; + } + + return { + ...series, + values, + valueMapping, + }; + }); + + return { + ...data, + serieses: processedSerieses, + }; +}; + +export const useMapOverlayReport = ( + projectCode?: ProjectCode, + entityCode?: EntityCode, + mapOverlay?: SingleMapOverlayItem, + params?: { + startDate?: string; + endDate?: string; + }, +) => { + // convert moment dates to date strings for the endpoint to use + const startDate = params?.startDate ? momentToDateString(params.startDate) : undefined; + const endDate = params?.startDate ? momentToDateString(params.endDate) : undefined; + const mapOverlayCode = mapOverlay?.code; + const isLegacy = mapOverlay?.legacy; + const endpoint = isLegacy ? 'legacyMapOverlayReport' : 'report'; + + return useQuery( + [projectCode, entityCode, mapOverlayCode, startDate, endDate], + async () => { + const response = await get(`${endpoint}/${mapOverlayCode}`, { + params: { + organisationUnitCode: entityCode, + projectCode, + shouldShowAllParentCountryResults: projectCode !== entityCode, // TODO: figure out the logic here for shouldShowAllParentCountryResults + startDate, + endDate, + }, + }); + + const responseData = isLegacy ? response : normaliseResponse(response.data, mapOverlay); + return formatMapOverlayData(responseData); + }, + { + enabled: !!projectCode && !!entityCode && !!mapOverlayCode, + }, + ); +}; diff --git a/packages/tupaia-web/src/api/queries/useMapOverlays.ts b/packages/tupaia-web/src/api/queries/useMapOverlays.ts index 5652827b5b..5ded39304d 100644 --- a/packages/tupaia-web/src/api/queries/useMapOverlays.ts +++ b/packages/tupaia-web/src/api/queries/useMapOverlays.ts @@ -31,6 +31,7 @@ const mapOverlayByCode = ( interface UseMapOverlaysResult { hasMapOverlays: boolean; mapOverlayGroups: MapOverlayGroup[]; + mapOverlaysByCode: { [code: EntityCode]: MapOverlayGroup }; isLoadingMapOverlays: boolean; errorLoadingMapOverlays: UseQueryResult['error']; selectedOverlayCode: string | null; @@ -61,6 +62,7 @@ export const useMapOverlays = ( const selectedOverlay = codedOverlays[selectedOverlayCode!]; return { + mapOverlaysByCode: codedOverlays, hasMapOverlays: !!data?.mapOverlays?.length, mapOverlayGroups: data?.mapOverlays, isLoadingMapOverlays: isLoading, diff --git a/packages/tupaia-web/src/constants/url.ts b/packages/tupaia-web/src/constants/url.ts index cdc38dadf4..aa6adc7a95 100644 --- a/packages/tupaia-web/src/constants/url.ts +++ b/packages/tupaia-web/src/constants/url.ts @@ -30,3 +30,5 @@ export enum TABS { DASHBOARD = 'dashboard', } export const DEFAULT_PERIOD_PARAM_STRING = 'DEFAULT_PERIOD'; + +export const DEFAULT_MAP_OVERLAY_ID = '126'; // 'Operational Facilities' diff --git a/packages/tupaia-web/src/features/Dashboard/Dashboard.tsx b/packages/tupaia-web/src/features/Dashboard/Dashboard.tsx index 5f475c6077..319eaadcd7 100644 --- a/packages/tupaia-web/src/features/Dashboard/Dashboard.tsx +++ b/packages/tupaia-web/src/features/Dashboard/Dashboard.tsx @@ -101,8 +101,8 @@ const DashboardImageContainer = styled.div` export const Dashboard = () => { const { projectCode, entityCode, dashboardName } = useParams(); - const [isExpanded, setIsExpanded] = useState(false); const { dashboards, activeDashboard } = useDashboards(projectCode, entityCode, dashboardName); + const [isExpanded, setIsExpanded] = useState(false); const { data: entity } = useEntity(entityCode); const bounds = entity?.bounds; diff --git a/packages/tupaia-web/src/features/MapOverlays/InteractivePolygon.tsx b/packages/tupaia-web/src/features/Map/InteractivePolygon.tsx similarity index 100% rename from packages/tupaia-web/src/features/MapOverlays/InteractivePolygon.tsx rename to packages/tupaia-web/src/features/Map/InteractivePolygon.tsx diff --git a/packages/tupaia-web/src/features/Map/Map.tsx b/packages/tupaia-web/src/features/Map/Map.tsx index dda5a2c5c8..4a8fac95fb 100644 --- a/packages/tupaia-web/src/features/Map/Map.tsx +++ b/packages/tupaia-web/src/features/Map/Map.tsx @@ -16,9 +16,11 @@ import { import { TRANSPARENT_BLACK, TILE_SETS, MOBILE_BREAKPOINT } from '../../constants'; import { MapWatermark } from './MapWatermark'; import { MapLegend } from './MapLegend'; -import { MapOverlays } from '../MapOverlays'; import { MapOverlaySelector } from './MapOverlaySelector'; -import { useEntity } from '../../api/queries'; +import { useEntity, useMapOverlays } from '../../api/queries'; +import { PolygonLayer } from './PolygonLayer'; +import { MarkerLayer } from './MarkerLayer'; +import { useDefaultMapOverlay } from './useDefaultMapOverlay'; const MapContainer = styled.div` height: 100%; @@ -70,7 +72,9 @@ const TilePickerWrapper = styled.div` // This contains the map controls (legend, overlay selector, etc, so that they can fit within the map appropriately) const MapControlWrapper = styled.div` - position: relative; + position: absolute; + top: 0; + left: 0; width: 100%; height: 100%; display: flex; @@ -86,11 +90,14 @@ const MapControlColumn = styled.div` `; export const Map = () => { - const { entityCode } = useParams(); + const { projectCode, entityCode } = useParams(); const [activeTileSet, setActiveTileSet] = useState(TILE_SETS[0]); - const { data: entity } = useEntity(entityCode); + // set the map default overlay if there isn't one selected + const { mapOverlaysByCode } = useMapOverlays(projectCode, entityCode); + useDefaultMapOverlay(projectCode!, mapOverlaysByCode); + const onTileSetChange = (tileSetKey: string) => { setActiveTileSet(TILE_SETS.find(({ key }) => key === tileSetKey) as typeof TILE_SETS[0]); }; @@ -99,23 +106,25 @@ export const Map = () => { - + + - - - - - - - - - + {/* Map Controls need to be outside the map so that the mouse events on controls don't inter wit the map */} + + + + + + + + + ); }; diff --git a/packages/tupaia-web/src/features/Map/MapOverlaySelector/DesktopMapOverlaySelector.tsx b/packages/tupaia-web/src/features/Map/MapOverlaySelector/DesktopMapOverlaySelector.tsx index 98e0cf8269..85ff06f677 100644 --- a/packages/tupaia-web/src/features/Map/MapOverlaySelector/DesktopMapOverlaySelector.tsx +++ b/packages/tupaia-web/src/features/Map/MapOverlaySelector/DesktopMapOverlaySelector.tsx @@ -11,13 +11,12 @@ import { ExpandMore, Layers } from '@material-ui/icons'; import { periodToMoment } from '@tupaia/utils'; import { MOBILE_BREAKPOINT, URL_SEARCH_PARAMS } from '../../../constants'; import { Entity } from '../../../types'; -import { useMapOverlayData, useMapOverlays } from '../../../api/queries'; +import { useMapOverlayReport, useMapOverlays } from '../../../api/queries'; import { MapOverlayList } from './MapOverlayList'; import { MapOverlaySelectorTitle } from './MapOverlaySelectorTitleSection'; import { useDateRanges } from '../../../utils'; const MaxHeightContainer = styled.div` - flex: 1; max-height: 100%; overflow: hidden; display: flex; @@ -25,7 +24,7 @@ const MaxHeightContainer = styled.div` `; const Wrapper = styled(MaxHeightContainer)` - pointer-events: auto; + flex: 1; max-width: 21.25rem; margin: 0.625rem; @media screen and (max-width: ${MOBILE_BREAKPOINT}) { @@ -37,6 +36,7 @@ const Header = styled.div` padding: 0.9rem 1rem; background-color: ${({ theme }) => theme.palette.secondary.main}; border-radius: 5px 5px 0 0; + pointer-events: auto; `; const Heading = styled(Typography).attrs({ @@ -49,6 +49,8 @@ const Heading = styled(Typography).attrs({ const Container = styled(MaxHeightContainer)` border-radius: 0 0 5px 5px; + // Set pointer events on the container rather than higher up so that it only applies to the open menu + pointer-events: auto; `; const OverlayLibraryAccordion = styled(Accordion)` @@ -132,24 +134,15 @@ export const DesktopMapOverlaySelector = ({ toggleOverlayLibrary, }: DesktopMapOverlaySelectorProps) => { const { projectCode, entityCode } = useParams(); - const { hasMapOverlays, selectedOverlayCode, selectedOverlay } = useMapOverlays( - projectCode, - entityCode, - ); + const { hasMapOverlays, selectedOverlay } = useMapOverlays(projectCode, entityCode); const { startDate, endDate } = useDateRanges( URL_SEARCH_PARAMS.MAP_OVERLAY_PERIOD, selectedOverlay, ); - const { data: mapOverlayData } = useMapOverlayData( - projectCode, - entityCode, - selectedOverlayCode, - selectedOverlay?.legacy, - { - startDate, - endDate, - }, - ); + const { data: mapOverlayData } = useMapOverlayReport(projectCode, entityCode, selectedOverlay, { + startDate, + endDate, + }); return ( diff --git a/packages/tupaia-web/src/features/Map/MapOverlaySelector/MapOverlayDatePicker.tsx b/packages/tupaia-web/src/features/Map/MapOverlaySelector/MapOverlayDatePicker.tsx index 141d789d41..3c8e676af6 100644 --- a/packages/tupaia-web/src/features/Map/MapOverlaySelector/MapOverlayDatePicker.tsx +++ b/packages/tupaia-web/src/features/Map/MapOverlaySelector/MapOverlayDatePicker.tsx @@ -5,14 +5,14 @@ import React from 'react'; import { useParams } from 'react-router-dom'; import { Skeleton } from '@material-ui/lab'; -import { useMapOverlayData, useMapOverlays } from '../../../api/queries'; +import { useMapOverlayReport, useMapOverlays } from '../../../api/queries'; import { DateRangePicker } from '../../../components'; import { useDateRanges } from '../../../utils'; import { URL_SEARCH_PARAMS } from '../../../constants'; export const MapOverlayDatePicker = () => { const { projectCode, entityCode } = useParams(); - const { selectedOverlay, selectedOverlayCode } = useMapOverlays(projectCode, entityCode); + const { selectedOverlay } = useMapOverlays(projectCode, entityCode); const { showDatePicker, startDate, @@ -23,11 +23,10 @@ export const MapOverlayDatePicker = () => { periodGranularity, } = useDateRanges(URL_SEARCH_PARAMS.MAP_OVERLAY_PERIOD, selectedOverlay); - const { isLoading: isLoadingMapOverlayData } = useMapOverlayData( + const { isLoading: isLoadingMapOverlayData } = useMapOverlayReport( projectCode, entityCode, - selectedOverlayCode, - selectedOverlay?.legacy, + selectedOverlay, { startDate, endDate, diff --git a/packages/tupaia-web/src/features/Map/MapOverlaySelector/MapOverlayList.tsx b/packages/tupaia-web/src/features/Map/MapOverlaySelector/MapOverlayList.tsx index 8d7cfc56b3..e47182fa3f 100644 --- a/packages/tupaia-web/src/features/Map/MapOverlaySelector/MapOverlayList.tsx +++ b/packages/tupaia-web/src/features/Map/MapOverlaySelector/MapOverlayList.tsx @@ -3,7 +3,7 @@ * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd */ -import React, { useState } from 'react'; +import React, { ChangeEvent, useState } from 'react'; import { useParams } from 'react-router'; import { Accordion, @@ -13,11 +13,12 @@ import { Radio, RadioGroup, } from '@material-ui/core'; +import { useSearchParams } from 'react-router-dom'; import { KeyboardArrowRight } from '@material-ui/icons'; import styled from 'styled-components'; import { MapOverlayGroup } from '../../../types'; import { useMapOverlays } from '../../../api/queries'; -import { updateSelectedMapOverlay } from '../../../utils'; +import { DEFAULT_PERIOD_PARAM_STRING, URL_SEARCH_PARAMS } from '../../../constants'; const AccordionWrapper = styled(Accordion)` background-color: transparent; @@ -77,6 +78,7 @@ const MapOverlayAccordion = ({ mapOverlayGroup }: { mapOverlayGroup: MapOverlayG const toggleExpanded = () => { setExpanded(!expanded); }; + return ( }>{mapOverlayGroup.name} @@ -86,7 +88,12 @@ const MapOverlayAccordion = ({ mapOverlayGroup }: { mapOverlayGroup: MapOverlayG mapOverlay.children ? ( ) : ( - } label={mapOverlay.name} /> + } + label={mapOverlay.name} + key={mapOverlay.code} + /> ), )} @@ -98,15 +105,23 @@ const MapOverlayAccordion = ({ mapOverlayGroup }: { mapOverlayGroup: MapOverlayG * This is the parent list of all the map overlays available to pick from */ export const MapOverlayList = () => { + const [urlSearchParams, setUrlParams] = useSearchParams(); const { projectCode, entityCode } = useParams(); const { mapOverlayGroups, selectedOverlayCode } = useMapOverlays(projectCode, entityCode); + const onChangeMapOverlay = (e: ChangeEvent) => { + urlSearchParams.set(URL_SEARCH_PARAMS.MAP_OVERLAY, e.target.value); + // when overlay changes, reset period to default + urlSearchParams.set(URL_SEARCH_PARAMS.MAP_OVERLAY_PERIOD, DEFAULT_PERIOD_PARAM_STRING); + setUrlParams(urlSearchParams); + }; + return ( {mapOverlayGroups .filter(item => item.name) diff --git a/packages/tupaia-web/src/features/Map/MarkerLayer/MarkerLayer.tsx b/packages/tupaia-web/src/features/Map/MarkerLayer/MarkerLayer.tsx new file mode 100644 index 0000000000..d10664a9e0 --- /dev/null +++ b/packages/tupaia-web/src/features/Map/MarkerLayer/MarkerLayer.tsx @@ -0,0 +1,99 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ + +import React from 'react'; +import { useParams } from 'react-router'; +import camelCase from 'camelcase'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { MarkerLayer as UIMarkerLayer, MeasureData } from '@tupaia/ui-map-components'; +import { + useEntitiesWithLocation, + useEntity, + useMapOverlayReport, + useMapOverlays, + useProject, +} from '../../../api/queries'; +import { EntityCode } from '../../../types'; +import { processMeasureData } from './processMeasureData'; + +const useNavigateToDashboard = () => { + const { projectCode } = useParams(); + const location = useLocation(); + const navigate = useNavigate(); + const { data: project } = useProject(projectCode); + + return (entityCode: EntityCode) => { + const link = { + ...location, + pathname: `/${projectCode}/${entityCode}/${project?.dashboardGroupName}`, + }; + navigate(link); + }; +}; + +const useEntitiesByMeasureLevel = (measureLevel?: string) => { + const { projectCode, entityCode } = useParams(); + const getSnakeCase = (measureLevel?: string) => { + return measureLevel + ?.split(/\.?(?=[A-Z])/) + .join('_') + .toLowerCase(); + }; + + return useEntitiesWithLocation( + projectCode, + entityCode, + { + params: { + includeRoot: false, + filter: { + type: getSnakeCase(measureLevel), + generational_distance: { + comparator: '<=', + comparisonValue: 2, + }, + }, + }, + }, + { enabled: !!measureLevel }, + ); +}; + +export const MarkerLayer = () => { + const navigateToDashboard = useNavigateToDashboard(); + const { projectCode, entityCode } = useParams(); + const { selectedOverlay } = useMapOverlays(projectCode, entityCode); + const { data: entitiesData } = useEntitiesByMeasureLevel(selectedOverlay?.measureLevel); + const { data: mapOverlayData } = useMapOverlayReport(projectCode, entityCode, selectedOverlay); + const { data: entity } = useEntity(entityCode); + + if (!entitiesData || !mapOverlayData || !entity) { + return null; + } + + // todo: move to mapOverlays route + // Don't show the marker layer if the entity type doesn't match the measure level + const firstSeries = mapOverlayData.serieses.find((series: any) => series.displayOnLevel); + if (firstSeries && camelCase(entity.type!) !== camelCase(firstSeries.displayOnLevel)) { + return null; + } + + const processedMeasureData = processMeasureData({ + entitiesData, + measureData: mapOverlayData.measureData, + serieses: mapOverlayData.serieses, + // Implement this when we add the legend + hiddenValues: {}, + }); + + return ( + + ); +}; diff --git a/packages/tupaia-web/src/features/MapOverlays/index.ts b/packages/tupaia-web/src/features/Map/MarkerLayer/index.ts similarity index 62% rename from packages/tupaia-web/src/features/MapOverlays/index.ts rename to packages/tupaia-web/src/features/Map/MarkerLayer/index.ts index 4bd91eab09..eda6d78617 100644 --- a/packages/tupaia-web/src/features/MapOverlays/index.ts +++ b/packages/tupaia-web/src/features/Map/MarkerLayer/index.ts @@ -2,4 +2,5 @@ * Tupaia * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd */ -export { MapOverlays } from './MapOverlays.tsx'; + +export { MarkerLayer } from './MarkerLayer'; diff --git a/packages/tupaia-web/src/features/Map/MarkerLayer/processMeasureData.ts b/packages/tupaia-web/src/features/Map/MarkerLayer/processMeasureData.ts new file mode 100644 index 0000000000..cf6fd12223 --- /dev/null +++ b/packages/tupaia-web/src/features/Map/MarkerLayer/processMeasureData.ts @@ -0,0 +1,52 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ +import { + calculateRadiusScaleFactor, + getMeasureDisplayInfo, + LegendProps, + MeasureData, + Series, +} from '@tupaia/ui-map-components'; +import { Entity } from '@tupaia/types'; + +interface processMeasureDataProps { + measureData: MeasureData[]; + entitiesData: Entity[]; + serieses: Series[]; + hiddenValues: LegendProps['hiddenValues']; +} +export const processMeasureData = ({ + measureData, + entitiesData, + serieses, + hiddenValues, +}: processMeasureDataProps) => { + const radiusScaleFactor = calculateRadiusScaleFactor(measureData); + + return entitiesData.map((entity: Entity) => { + const measure = measureData.find( + (measureEntity: any) => measureEntity.organisationUnitCode === entity.code, + ); + const { color, icon, originalValue, isHidden, radius } = getMeasureDisplayInfo( + measure!, + serieses, + hiddenValues, + radiusScaleFactor, + ); + + return { + ...entity, + ...measure, + isHidden, + radius, + organisationUnitCode: entity.code, + coordinates: entity.point, + region: entity.region, + color, + icon, + originalValue, + }; + }); +}; diff --git a/packages/tupaia-web/src/features/MapOverlays/PolygonLayer.tsx b/packages/tupaia-web/src/features/Map/PolygonLayer.tsx similarity index 87% rename from packages/tupaia-web/src/features/MapOverlays/PolygonLayer.tsx rename to packages/tupaia-web/src/features/Map/PolygonLayer.tsx index 80d24518a3..599e88f6ba 100644 --- a/packages/tupaia-web/src/features/MapOverlays/PolygonLayer.tsx +++ b/packages/tupaia-web/src/features/Map/PolygonLayer.tsx @@ -52,14 +52,15 @@ const SiblingEntities = ({ }; const ActiveEntity = ({ entity }: { entity: EntityResponse }) => { - const { region, children } = entity; - const hasChildren = children && children.length > 0; + const { region, childCodes } = entity; + const hasChildren = childCodes && childCodes.length > 0; if (!region) return null; return ( { ); }; -interface PolygonLayerProps { - entities: EntityResponse[]; - entityCode: EntityCode; -} +export const PolygonLayer = () => { + const { projectCode, entityCode } = useParams(); + const { data: entities = [] } = useEntitiesWithLocation(projectCode, entityCode, { + params: { includeRoot: true }, + }); -export const PolygonLayer = ({ entities, entityCode }: PolygonLayerProps) => { if (!entities || entities.length === 0) { return null; } @@ -84,6 +85,7 @@ export const PolygonLayer = ({ entities, entityCode }: PolygonLayerProps) => { return ( <> + {activeEntity && ( <> @@ -93,7 +95,6 @@ export const PolygonLayer = ({ entities, entityCode }: PolygonLayerProps) => { /> )} - ); }; diff --git a/packages/tupaia-web/src/features/Map/useDefaultMapOverlay.ts b/packages/tupaia-web/src/features/Map/useDefaultMapOverlay.ts new file mode 100644 index 0000000000..d555d6609d --- /dev/null +++ b/packages/tupaia-web/src/features/Map/useDefaultMapOverlay.ts @@ -0,0 +1,45 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ + +import { useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { useProject } from '../../api/queries'; +import { + DEFAULT_MAP_OVERLAY_ID, + DEFAULT_PERIOD_PARAM_STRING, + URL_SEARCH_PARAMS, +} from '../../constants'; +import { MapOverlayGroup, ProjectCode, EntityCode } from '../../types'; + +// When the map overlay groups change, update the default map overlay +export const useDefaultMapOverlay = ( + projectCode: ProjectCode, + mapOverlaysByCode: { [code: EntityCode]: MapOverlayGroup }, +) => { + const [urlSearchParams, setUrlParams] = useSearchParams(); + const { data: project } = useProject(projectCode); + + const selectedMapOverlay = urlSearchParams.get(URL_SEARCH_PARAMS.MAP_OVERLAY); + const selectedMapOverlayPeriod = urlSearchParams.get(URL_SEARCH_PARAMS.MAP_OVERLAY_PERIOD); + + useEffect(() => { + if (!project) { + return; + } + + const isValidMapOverlayId = !!mapOverlaysByCode[selectedMapOverlay!]; + + if (!selectedMapOverlay || !isValidMapOverlayId) { + const defaultMapOverlayId = project.defaultMeasure || DEFAULT_MAP_OVERLAY_ID; + urlSearchParams.set(URL_SEARCH_PARAMS.MAP_OVERLAY, defaultMapOverlayId); + } + + if (!selectedMapOverlayPeriod) { + urlSearchParams.set(URL_SEARCH_PARAMS.MAP_OVERLAY_PERIOD, DEFAULT_PERIOD_PARAM_STRING); + } + + setUrlParams(urlSearchParams); + }, [JSON.stringify(mapOverlaysByCode)]); +}; diff --git a/packages/tupaia-web/src/features/MapOverlays/MapOverlays.tsx b/packages/tupaia-web/src/features/MapOverlays/MapOverlays.tsx deleted file mode 100644 index 25e1060db9..0000000000 --- a/packages/tupaia-web/src/features/MapOverlays/MapOverlays.tsx +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Tupaia - * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd - */ -import React from 'react'; -import { useParams } from 'react-router-dom'; -import { PolygonLayer } from './PolygonLayer'; -import { EntityResponse } from '../../types'; -import { useEntitiesWithLocation } from '../../api/queries'; - -export const MapOverlays = () => { - const { projectCode, entityCode } = useParams(); - const { data } = useEntitiesWithLocation(projectCode, entityCode); - - return ; -}; diff --git a/packages/tupaia-web/src/features/MapOverlays/MarkerLayer.tsx b/packages/tupaia-web/src/features/MapOverlays/MarkerLayer.tsx deleted file mode 100644 index 1c57ad0119..0000000000 --- a/packages/tupaia-web/src/features/MapOverlays/MarkerLayer.tsx +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Tupaia - * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd - */ - -import React from 'react'; - -export const MarkerLayer = () => { - return <>; -}; diff --git a/packages/tupaia-web/src/utils/index.ts b/packages/tupaia-web/src/utils/index.ts index 6378b39c4c..55fbada2af 100644 --- a/packages/tupaia-web/src/utils/index.ts +++ b/packages/tupaia-web/src/utils/index.ts @@ -8,4 +8,3 @@ export { removeUrlSearchParams } from './removeUrlSearchParams'; export { useModal } from './useModal'; export { useEntityLink } from './useEntityLink'; export { useDateRanges } from './useDateRanges'; -export { updateSelectedMapOverlay } from './updateSelectedMapOverlay'; diff --git a/packages/tupaia-web/src/utils/updateSelectedMapOverlay.ts b/packages/tupaia-web/src/utils/updateSelectedMapOverlay.ts deleted file mode 100644 index 79f90f74d4..0000000000 --- a/packages/tupaia-web/src/utils/updateSelectedMapOverlay.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Tupaia - * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd - */ - -import { ChangeEvent } from 'react'; -import { DEFAULT_PERIOD_PARAM_STRING, URL_SEARCH_PARAMS } from '../constants'; -import { useSearchParams } from 'react-router-dom'; - -export const updateSelectedMapOverlay = (e: ChangeEvent) => { - const [urlSearchParams, setUrlParams] = useSearchParams(); - urlSearchParams.set(URL_SEARCH_PARAMS.MAP_OVERLAY, e.target.value); - // when overlay changes, reset period to default - urlSearchParams.set(URL_SEARCH_PARAMS.MAP_OVERLAY_PERIOD, DEFAULT_PERIOD_PARAM_STRING); - setUrlParams(urlSearchParams); -}; diff --git a/packages/types/src/schemas/schemas.ts b/packages/types/src/schemas/schemas.ts index 25150ed0a3..89eacbfc72 100644 --- a/packages/types/src/schemas/schemas.ts +++ b/packages/types/src/schemas/schemas.ts @@ -1265,171 +1265,341 @@ export const MatrixConfigSchema = { }, "presentationOptions": { "description": "Allows for conditional styling", - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "condition" - ] - }, - "conditions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "color": { - "type": "string" - }, - "label": { - "type": "string" - }, - "description": { - "type": "string" - }, - "condition": { - "description": "the value to match against exactly, or an object with match criteria e.g. { '>=': 5.5 }", - "anyOf": [ - { - "type": "object", - "properties": { - "=": { - "type": [ - "string", - "number" - ] - }, - ">": { - "type": [ - "string", - "number" - ] - }, - "<": { - "type": [ - "string", - "number" - ] - }, - ">=": { - "type": [ - "string", - "number" + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "condition" + ] + }, + "conditions": { + "type": "array", + "items": { + "additionalProperties": false, + "type": "object", + "properties": { + "color": { + "description": "Specify the color of the display item", + "type": "string" + }, + "description": { + "description": "Specify the text for the legend item. Also used in the enlarged cell view", + "type": "string" + }, + "label": { + "description": "Specify if you want a label to appear above the enlarged", + "type": "string" + }, + "key": { + "type": "string" + }, + "condition": { + "description": "the value to match against exactly, or an object with match criteria e.g. { '>=': 5.5 }", + "anyOf": [ + { + "type": "object", + "properties": { + "=": { + "type": [ + "string", + "number" + ] + }, + ">": { + "type": [ + "string", + "number" + ] + }, + "<": { + "type": [ + "string", + "number" + ] + }, + ">=": { + "type": [ + "string", + "number" + ] + }, + "<=": { + "type": [ + "string", + "number" + ] + } + }, + "additionalProperties": false, + "required": [ + "<", + "<=", + "=", + ">", + ">=" ] }, - "<=": { + { "type": [ "string", "number" ] } - }, - "additionalProperties": false, - "required": [ - "<", - "<=", - "=", - ">", - ">=" ] }, - { - "type": [ - "string", - "number" - ] + "legendLabel": { + "type": "string" } + }, + "required": [ + "condition", + "key" ] - }, - "legendLabel": { - "type": "string" } }, - "additionalProperties": false, - "required": [ - "condition", - "key" - ] - } - }, - "showRawValue": { - "default": false, - "type": "boolean" - }, - "showNestedRows": { - "default": false, - "type": "boolean" + "showRawValue": { + "default": false, + "type": "boolean" + }, + "showNestedRows": { + "default": false, + "type": "boolean" + }, + "applyLocation": { + "description": "Specify if you want to limit where to apply the conditional presentation", + "type": "object", + "properties": { + "columnIndexes": { + "type": "array", + "items": { + "type": "number" + } + } + }, + "additionalProperties": false, + "required": [ + "columnIndexes" + ] + } + }, + "additionalProperties": false }, - "applyLocation": { - "description": "Specify if you want to limit where to apply the conditional presentation", + { + "additionalProperties": false, "type": "object", "properties": { - "columnIndexes": { - "type": "array", - "items": { - "type": "number" - } + "type": { + "type": "string", + "enum": [ + "range" + ] + }, + "showRawValue": { + "type": "boolean" } }, - "additionalProperties": false, "required": [ - "columnIndexes" + "type" ] } - }, - "additionalProperties": false - }, - "categoryPresentationOptions": { - "description": "Category header rows can have values just like real rows, this is how you style them" - } - }, - "description": "Matrix viz type", - "additionalProperties": false, - "type": "object", - "required": [ - "name", - "type" - ] -} - -export const PresentationOptionsSchema = { - "properties": { - "type": { - "type": "string", - "enum": [ - "condition" ] }, - "conditions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "color": { - "type": "string" - }, - "label": { - "type": "string" - }, - "description": { - "type": "string" - }, - "condition": { - "description": "the value to match against exactly, or an object with match criteria e.g. { '>=': 5.5 }", - "anyOf": [ - { - "type": "object", - "properties": { - "=": { - "type": [ - "string", - "number" + "categoryPresentationOptions": { + "description": "Category header rows can have values just like real rows, this is how you style them", + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "condition" + ] + }, + "conditions": { + "type": "array", + "items": { + "additionalProperties": false, + "type": "object", + "properties": { + "color": { + "description": "Specify the color of the display item", + "type": "string" + }, + "description": { + "description": "Specify the text for the legend item. Also used in the enlarged cell view", + "type": "string" + }, + "label": { + "description": "Specify if you want a label to appear above the enlarged", + "type": "string" + }, + "key": { + "type": "string" + }, + "condition": { + "description": "the value to match against exactly, or an object with match criteria e.g. { '>=': 5.5 }", + "anyOf": [ + { + "type": "object", + "properties": { + "=": { + "type": [ + "string", + "number" + ] + }, + ">": { + "type": [ + "string", + "number" + ] + }, + "<": { + "type": [ + "string", + "number" + ] + }, + ">=": { + "type": [ + "string", + "number" + ] + }, + "<=": { + "type": [ + "string", + "number" + ] + } + }, + "additionalProperties": false, + "required": [ + "<", + "<=", + "=", + ">", + ">=" + ] + }, + { + "type": [ + "string", + "number" + ] + } + ] + }, + "legendLabel": { + "type": "string" + } + }, + "required": [ + "condition", + "key" + ] + } + }, + "showRawValue": { + "default": false, + "type": "boolean" + }, + "showNestedRows": { + "default": false, + "type": "boolean" + }, + "applyLocation": { + "description": "Specify if you want to limit where to apply the conditional presentation", + "type": "object", + "properties": { + "columnIndexes": { + "type": "array", + "items": { + "type": "number" + } + } + }, + "additionalProperties": false, + "required": [ + "columnIndexes" + ] + } + }, + "additionalProperties": false + }, + { + "additionalProperties": false, + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "range" + ] + }, + "showRawValue": { + "type": "boolean" + } + }, + "required": [ + "type" + ] + } + ] + } + }, + "description": "Matrix viz type", + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "type" + ] +} + +export const ConditionalPresentationOptionsSchema = { + "properties": { + "type": { + "type": "string", + "enum": [ + "condition" + ] + }, + "conditions": { + "type": "array", + "items": { + "additionalProperties": false, + "type": "object", + "properties": { + "color": { + "description": "Specify the color of the display item", + "type": "string" + }, + "description": { + "description": "Specify the text for the legend item. Also used in the enlarged cell view", + "type": "string" + }, + "label": { + "description": "Specify if you want a label to appear above the enlarged", + "type": "string" + }, + "key": { + "type": "string" + }, + "condition": { + "description": "the value to match against exactly, or an object with match criteria e.g. { '>=': 5.5 }", + "anyOf": [ + { + "type": "object", + "properties": { + "=": { + "type": [ + "string", + "number" ] }, ">": { @@ -1478,7 +1648,6 @@ export const PresentationOptionsSchema = { "type": "string" } }, - "additionalProperties": false, "required": [ "condition", "key" @@ -1514,172 +1683,382 @@ export const PresentationOptionsSchema = { "additionalProperties": false } -export const PresentationOptionConditionSchema = { +export const RangePresentationOptionsSchema = { "properties": { - "key": { - "type": "string" - }, - "color": { - "type": "string" - }, - "label": { - "type": "string" - }, - "description": { - "type": "string" - }, - "condition": { - "description": "the value to match against exactly, or an object with match criteria e.g. { '>=': 5.5 }", - "anyOf": [ - { - "type": "object", - "properties": { - "=": { - "type": [ - "string", - "number" - ] - }, - ">": { - "type": [ - "string", - "number" - ] - }, - "<": { - "type": [ - "string", - "number" - ] - }, - ">=": { - "type": [ - "string", - "number" - ] - }, - "<=": { - "type": [ - "string", - "number" - ] - } - }, - "additionalProperties": false, - "required": [ - "<", - "<=", - "=", - ">", - ">=" - ] - }, - { - "type": [ - "string", - "number" - ] - } + "type": { + "type": "string", + "enum": [ + "range" ] }, - "legendLabel": { - "type": "string" - } + "showRawValue": { + "type": "boolean" + } }, - "type": "object", "additionalProperties": false, + "type": "object", "required": [ - "condition", - "key" + "type" ] } -export const ConditionValueSchema = { - "type": [ - "string", - "number" +export const PresentationOptionsSchema = { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "condition" + ] + }, + "conditions": { + "type": "array", + "items": { + "additionalProperties": false, + "type": "object", + "properties": { + "color": { + "description": "Specify the color of the display item", + "type": "string" + }, + "description": { + "description": "Specify the text for the legend item. Also used in the enlarged cell view", + "type": "string" + }, + "label": { + "description": "Specify if you want a label to appear above the enlarged", + "type": "string" + }, + "key": { + "type": "string" + }, + "condition": { + "description": "the value to match against exactly, or an object with match criteria e.g. { '>=': 5.5 }", + "anyOf": [ + { + "type": "object", + "properties": { + "=": { + "type": [ + "string", + "number" + ] + }, + ">": { + "type": [ + "string", + "number" + ] + }, + "<": { + "type": [ + "string", + "number" + ] + }, + ">=": { + "type": [ + "string", + "number" + ] + }, + "<=": { + "type": [ + "string", + "number" + ] + } + }, + "additionalProperties": false, + "required": [ + "<", + "<=", + "=", + ">", + ">=" + ] + }, + { + "type": [ + "string", + "number" + ] + } + ] + }, + "legendLabel": { + "type": "string" + } + }, + "required": [ + "condition", + "key" + ] + } + }, + "showRawValue": { + "default": false, + "type": "boolean" + }, + "showNestedRows": { + "default": false, + "type": "boolean" + }, + "applyLocation": { + "description": "Specify if you want to limit where to apply the conditional presentation", + "type": "object", + "properties": { + "columnIndexes": { + "type": "array", + "items": { + "type": "number" + } + } + }, + "additionalProperties": false, + "required": [ + "columnIndexes" + ] + } + }, + "additionalProperties": false + }, + { + "additionalProperties": false, + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "range" + ] + }, + "showRawValue": { + "type": "boolean" + } + }, + "required": [ + "type" + ] + } ] } -export const ConditionTypeSchema = { - "enum": [ - "<", - "<=", - "=", - ">", - ">=" - ], - "type": "string" -} - -export const VizComponentNameSchema = { - "enum": [ - "ActiveDisasters", - "NoAccessDashboard", - "NoDataAtLevelDashboard", - "ProjectDescription" - ], - "type": "string" +export const BasePresentationOptionSchema = { + "properties": { + "color": { + "description": "Specify the color of the display item", + "type": "string" + }, + "description": { + "description": "Specify the text for the legend item. Also used in the enlarged cell view", + "type": "string" + }, + "label": { + "description": "Specify if you want a label to appear above the enlarged", + "type": "string" + } + }, + "type": "object", + "additionalProperties": false } -export const ComponentConfigSchema = { +export const PresentationOptionConditionSchema = { "properties": { - "name": { + "color": { + "description": "Specify the color of the display item", "type": "string" }, "description": { - "description": "A short description that appears above a viz", + "description": "Specify the text for the legend item. Also used in the enlarged cell view", "type": "string" }, - "placeholder": { - "description": "A url to an image to be used when a viz is collapsed. Some vizes display small, others display a placeholder.", + "label": { + "description": "Specify if you want a label to appear above the enlarged", "type": "string" }, - "periodGranularity": { - "enum": [ - "day", - "month", - "one_day_at_a_time", - "one_month_at_a_time", - "one_quarter_at_a_time", - "one_week_at_a_time", - "one_year_at_a_time", - "quarter", - "week", - "year" - ], + "key": { "type": "string" }, - "defaultTimePeriod": { + "condition": { + "description": "the value to match against exactly, or an object with match criteria e.g. { '>=': 5.5 }", "anyOf": [ { "type": "object", "properties": { - "offset": { - "type": "number" + "=": { + "type": [ + "string", + "number" + ] }, - "unit": { - "enum": [ - "day", - "month", - "quarter", - "week", - "year" - ], - "type": "string" + ">": { + "type": [ + "string", + "number" + ] + }, + "<": { + "type": [ + "string", + "number" + ] + }, + ">=": { + "type": [ + "string", + "number" + ] + }, + "<=": { + "type": [ + "string", + "number" + ] } }, "additionalProperties": false, "required": [ - "offset", - "unit" + "<", + "<=", + "=", + ">", + ">=" ] }, { - "type": "object", - "properties": { - "start": { - "type": "object", + "type": [ + "string", + "number" + ] + } + ] + }, + "legendLabel": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "condition", + "key" + ] +} + +export const PresentationOptionRangeSchema = { + "properties": { + "color": { + "description": "Specify the color of the display item", + "type": "string" + }, + "description": { + "description": "Specify the text for the legend item. Also used in the enlarged cell view", + "type": "string" + }, + "label": { + "description": "Specify if you want a label to appear above the enlarged", + "type": "string" + }, + "min": { + "type": "number" + }, + "max": { + "type": "number" + } + }, + "additionalProperties": false, + "type": "object" +} + +export const ConditionValueSchema = { + "type": [ + "string", + "number" + ] +} + +export const ConditionTypeSchema = { + "enum": [ + "<", + "<=", + "=", + ">", + ">=" + ], + "type": "string" +} + +export const VizComponentNameSchema = { + "enum": [ + "ActiveDisasters", + "NoAccessDashboard", + "NoDataAtLevelDashboard", + "ProjectDescription" + ], + "type": "string" +} + +export const ComponentConfigSchema = { + "properties": { + "name": { + "type": "string" + }, + "description": { + "description": "A short description that appears above a viz", + "type": "string" + }, + "placeholder": { + "description": "A url to an image to be used when a viz is collapsed. Some vizes display small, others display a placeholder.", + "type": "string" + }, + "periodGranularity": { + "enum": [ + "day", + "month", + "one_day_at_a_time", + "one_month_at_a_time", + "one_quarter_at_a_time", + "one_week_at_a_time", + "one_year_at_a_time", + "quarter", + "week", + "year" + ], + "type": "string" + }, + "defaultTimePeriod": { + "anyOf": [ + { + "type": "object", + "properties": { + "offset": { + "type": "number" + }, + "unit": { + "enum": [ + "day", + "month", + "quarter", + "week", + "year" + ], + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "offset", + "unit" + ] + }, + { + "type": "object", + "properties": { + "start": { + "type": "object", "properties": { "unit": { "enum": [ @@ -9753,125 +10132,291 @@ export const DashboardItemConfigSchema = { }, "presentationOptions": { "description": "Allows for conditional styling", - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "condition" - ] - }, - "conditions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "color": { - "type": "string" - }, - "label": { - "type": "string" - }, - "description": { - "type": "string" - }, - "condition": { - "description": "the value to match against exactly, or an object with match criteria e.g. { '>=': 5.5 }", - "anyOf": [ - { - "type": "object", - "properties": { - "=": { - "type": [ - "string", - "number" - ] - }, - ">": { - "type": [ - "string", - "number" + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "condition" + ] + }, + "conditions": { + "type": "array", + "items": { + "additionalProperties": false, + "type": "object", + "properties": { + "color": { + "description": "Specify the color of the display item", + "type": "string" + }, + "description": { + "description": "Specify the text for the legend item. Also used in the enlarged cell view", + "type": "string" + }, + "label": { + "description": "Specify if you want a label to appear above the enlarged", + "type": "string" + }, + "key": { + "type": "string" + }, + "condition": { + "description": "the value to match against exactly, or an object with match criteria e.g. { '>=': 5.5 }", + "anyOf": [ + { + "type": "object", + "properties": { + "=": { + "type": [ + "string", + "number" + ] + }, + ">": { + "type": [ + "string", + "number" + ] + }, + "<": { + "type": [ + "string", + "number" + ] + }, + ">=": { + "type": [ + "string", + "number" + ] + }, + "<=": { + "type": [ + "string", + "number" + ] + } + }, + "additionalProperties": false, + "required": [ + "<", + "<=", + "=", + ">", + ">=" ] }, - "<": { + { "type": [ "string", "number" ] - }, - ">=": { - "type": [ - "string", - "number" + } + ] + }, + "legendLabel": { + "type": "string" + } + }, + "required": [ + "condition", + "key" + ] + } + }, + "showRawValue": { + "default": false, + "type": "boolean" + }, + "showNestedRows": { + "default": false, + "type": "boolean" + }, + "applyLocation": { + "description": "Specify if you want to limit where to apply the conditional presentation", + "type": "object", + "properties": { + "columnIndexes": { + "type": "array", + "items": { + "type": "number" + } + } + }, + "additionalProperties": false, + "required": [ + "columnIndexes" + ] + } + }, + "additionalProperties": false + }, + { + "additionalProperties": false, + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "range" + ] + }, + "showRawValue": { + "type": "boolean" + } + }, + "required": [ + "type" + ] + } + ] + }, + "categoryPresentationOptions": { + "description": "Category header rows can have values just like real rows, this is how you style them", + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "condition" + ] + }, + "conditions": { + "type": "array", + "items": { + "additionalProperties": false, + "type": "object", + "properties": { + "color": { + "description": "Specify the color of the display item", + "type": "string" + }, + "description": { + "description": "Specify the text for the legend item. Also used in the enlarged cell view", + "type": "string" + }, + "label": { + "description": "Specify if you want a label to appear above the enlarged", + "type": "string" + }, + "key": { + "type": "string" + }, + "condition": { + "description": "the value to match against exactly, or an object with match criteria e.g. { '>=': 5.5 }", + "anyOf": [ + { + "type": "object", + "properties": { + "=": { + "type": [ + "string", + "number" + ] + }, + ">": { + "type": [ + "string", + "number" + ] + }, + "<": { + "type": [ + "string", + "number" + ] + }, + ">=": { + "type": [ + "string", + "number" + ] + }, + "<=": { + "type": [ + "string", + "number" + ] + } + }, + "additionalProperties": false, + "required": [ + "<", + "<=", + "=", + ">", + ">=" ] }, - "<=": { + { "type": [ "string", "number" ] } - }, - "additionalProperties": false, - "required": [ - "<", - "<=", - "=", - ">", - ">=" ] }, - { - "type": [ - "string", - "number" - ] + "legendLabel": { + "type": "string" } + }, + "required": [ + "condition", + "key" ] - }, - "legendLabel": { - "type": "string" } }, - "additionalProperties": false, - "required": [ - "condition", - "key" - ] - } - }, - "showRawValue": { - "default": false, - "type": "boolean" - }, - "showNestedRows": { - "default": false, - "type": "boolean" + "showRawValue": { + "default": false, + "type": "boolean" + }, + "showNestedRows": { + "default": false, + "type": "boolean" + }, + "applyLocation": { + "description": "Specify if you want to limit where to apply the conditional presentation", + "type": "object", + "properties": { + "columnIndexes": { + "type": "array", + "items": { + "type": "number" + } + } + }, + "additionalProperties": false, + "required": [ + "columnIndexes" + ] + } + }, + "additionalProperties": false }, - "applyLocation": { - "description": "Specify if you want to limit where to apply the conditional presentation", + { + "additionalProperties": false, "type": "object", "properties": { - "columnIndexes": { - "type": "array", - "items": { - "type": "number" - } + "type": { + "type": "string", + "enum": [ + "range" + ] + }, + "showRawValue": { + "type": "boolean" } }, - "additionalProperties": false, "required": [ - "columnIndexes" + "type" ] } - }, - "additionalProperties": false - }, - "categoryPresentationOptions": { - "description": "Category header rows can have values just like real rows, this is how you style them" + ] } }, "required": [ @@ -17348,125 +17893,291 @@ export const DashboardItemSchema = { }, "presentationOptions": { "description": "Allows for conditional styling", - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "condition" - ] - }, - "conditions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "color": { - "type": "string" - }, - "label": { - "type": "string" - }, - "description": { - "type": "string" - }, - "condition": { - "description": "the value to match against exactly, or an object with match criteria e.g. { '>=': 5.5 }", - "anyOf": [ - { - "type": "object", - "properties": { - "=": { - "type": [ - "string", - "number" - ] - }, - ">": { - "type": [ - "string", - "number" + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "condition" + ] + }, + "conditions": { + "type": "array", + "items": { + "additionalProperties": false, + "type": "object", + "properties": { + "color": { + "description": "Specify the color of the display item", + "type": "string" + }, + "description": { + "description": "Specify the text for the legend item. Also used in the enlarged cell view", + "type": "string" + }, + "label": { + "description": "Specify if you want a label to appear above the enlarged", + "type": "string" + }, + "key": { + "type": "string" + }, + "condition": { + "description": "the value to match against exactly, or an object with match criteria e.g. { '>=': 5.5 }", + "anyOf": [ + { + "type": "object", + "properties": { + "=": { + "type": [ + "string", + "number" + ] + }, + ">": { + "type": [ + "string", + "number" + ] + }, + "<": { + "type": [ + "string", + "number" + ] + }, + ">=": { + "type": [ + "string", + "number" + ] + }, + "<=": { + "type": [ + "string", + "number" + ] + } + }, + "additionalProperties": false, + "required": [ + "<", + "<=", + "=", + ">", + ">=" ] }, - "<": { + { "type": [ "string", "number" ] - }, - ">=": { - "type": [ - "string", - "number" + } + ] + }, + "legendLabel": { + "type": "string" + } + }, + "required": [ + "condition", + "key" + ] + } + }, + "showRawValue": { + "default": false, + "type": "boolean" + }, + "showNestedRows": { + "default": false, + "type": "boolean" + }, + "applyLocation": { + "description": "Specify if you want to limit where to apply the conditional presentation", + "type": "object", + "properties": { + "columnIndexes": { + "type": "array", + "items": { + "type": "number" + } + } + }, + "additionalProperties": false, + "required": [ + "columnIndexes" + ] + } + }, + "additionalProperties": false + }, + { + "additionalProperties": false, + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "range" + ] + }, + "showRawValue": { + "type": "boolean" + } + }, + "required": [ + "type" + ] + } + ] + }, + "categoryPresentationOptions": { + "description": "Category header rows can have values just like real rows, this is how you style them", + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "condition" + ] + }, + "conditions": { + "type": "array", + "items": { + "additionalProperties": false, + "type": "object", + "properties": { + "color": { + "description": "Specify the color of the display item", + "type": "string" + }, + "description": { + "description": "Specify the text for the legend item. Also used in the enlarged cell view", + "type": "string" + }, + "label": { + "description": "Specify if you want a label to appear above the enlarged", + "type": "string" + }, + "key": { + "type": "string" + }, + "condition": { + "description": "the value to match against exactly, or an object with match criteria e.g. { '>=': 5.5 }", + "anyOf": [ + { + "type": "object", + "properties": { + "=": { + "type": [ + "string", + "number" + ] + }, + ">": { + "type": [ + "string", + "number" + ] + }, + "<": { + "type": [ + "string", + "number" + ] + }, + ">=": { + "type": [ + "string", + "number" + ] + }, + "<=": { + "type": [ + "string", + "number" + ] + } + }, + "additionalProperties": false, + "required": [ + "<", + "<=", + "=", + ">", + ">=" ] }, - "<=": { + { "type": [ "string", "number" ] } - }, - "additionalProperties": false, - "required": [ - "<", - "<=", - "=", - ">", - ">=" ] }, - { - "type": [ - "string", - "number" - ] + "legendLabel": { + "type": "string" } + }, + "required": [ + "condition", + "key" ] - }, - "legendLabel": { - "type": "string" } }, - "additionalProperties": false, - "required": [ - "condition", - "key" - ] - } - }, - "showRawValue": { - "default": false, - "type": "boolean" - }, - "showNestedRows": { - "default": false, - "type": "boolean" + "showRawValue": { + "default": false, + "type": "boolean" + }, + "showNestedRows": { + "default": false, + "type": "boolean" + }, + "applyLocation": { + "description": "Specify if you want to limit where to apply the conditional presentation", + "type": "object", + "properties": { + "columnIndexes": { + "type": "array", + "items": { + "type": "number" + } + } + }, + "additionalProperties": false, + "required": [ + "columnIndexes" + ] + } + }, + "additionalProperties": false }, - "applyLocation": { - "description": "Specify if you want to limit where to apply the conditional presentation", + { + "additionalProperties": false, "type": "object", "properties": { - "columnIndexes": { - "type": "array", - "items": { - "type": "number" - } + "type": { + "type": "string", + "enum": [ + "range" + ] + }, + "showRawValue": { + "type": "boolean" } }, - "additionalProperties": false, "required": [ - "columnIndexes" + "type" ] } - }, - "additionalProperties": false - }, - "categoryPresentationOptions": { - "description": "Category header rows can have values just like real rows, this is how you style them" + ] } }, "required": [ diff --git a/packages/types/src/types/index.ts b/packages/types/src/types/index.ts index 8557bd1812..1ef4b7949b 100644 --- a/packages/types/src/types/index.ts +++ b/packages/types/src/types/index.ts @@ -22,6 +22,8 @@ export { PresentationOptions, ConditionValue, ConditionType, + RangePresentationOptions, + ConditionalPresentationOptions, } from './models-extra'; export * from './requests'; export * from './css'; diff --git a/packages/types/src/types/models-extra/dashboard-item/index.ts b/packages/types/src/types/models-extra/dashboard-item/index.ts index a8ea85968c..c51585afab 100644 --- a/packages/types/src/types/models-extra/dashboard-item/index.ts +++ b/packages/types/src/types/models-extra/dashboard-item/index.ts @@ -9,6 +9,8 @@ import type { PresentationOptions, ConditionValue, ConditionType, + RangePresentationOptions, + ConditionalPresentationOptions, } from './matricies'; import type { ComponentConfig } from './components'; import type { @@ -70,4 +72,6 @@ export type { PresentationOptions, ConditionValue, ConditionType, + RangePresentationOptions, + ConditionalPresentationOptions, }; diff --git a/packages/types/src/types/models-extra/dashboard-item/matricies.ts b/packages/types/src/types/models-extra/dashboard-item/matricies.ts index b37d195453..7f30617438 100644 --- a/packages/types/src/types/models-extra/dashboard-item/matricies.ts +++ b/packages/types/src/types/models-extra/dashboard-item/matricies.ts @@ -30,10 +30,10 @@ export type MatrixConfig = BaseConfig & { /** * @description Category header rows can have values just like real rows, this is how you style them */ - categoryPresentationOptions?: any; + categoryPresentationOptions?: PresentationOptions; }; -export type PresentationOptions = { +export type ConditionalPresentationOptions = { type?: 'condition'; // optional key it seems like conditions?: PresentationOptionCondition[]; /** @@ -52,11 +52,29 @@ export type PresentationOptions = { }; }; -export type PresentationOptionCondition = { - key: string; +export type RangePresentationOptions = Record & { + type: 'range'; + showRawValue?: boolean; +}; + +export type PresentationOptions = ConditionalPresentationOptions | RangePresentationOptions; + +type BasePresentationOption = { + /** + * @description Specify the color of the display item + */ color?: CssColor; - label?: string; + /** + * @description Specify the text for the legend item. Also used in the enlarged cell view + */ description?: string; + /** + * @description Specify if you want a label to appear above the enlarged + */ + label?: string; +}; +export type PresentationOptionCondition = BasePresentationOption & { + key: string; /** * @description the value to match against exactly, or an object with match criteria e.g. { '>=': 5.5 } */ @@ -64,6 +82,11 @@ export type PresentationOptionCondition = { legendLabel?: string; }; +export type PresentationOptionRange = BasePresentationOption & { + min?: number; + max?: number; +}; + export type ConditionValue = string | number; export enum ConditionType { diff --git a/packages/types/src/types/models-extra/index.ts b/packages/types/src/types/models-extra/index.ts index d801abe52b..43585dda47 100644 --- a/packages/types/src/types/models-extra/index.ts +++ b/packages/types/src/types/models-extra/index.ts @@ -19,4 +19,6 @@ export type { PresentationOptions, ConditionValue, ConditionType, + RangePresentationOptions, + ConditionalPresentationOptions, } from './dashboard-item'; diff --git a/packages/ui-components/src/components/Dialog.tsx b/packages/ui-components/src/components/Dialog.tsx index 20dd3fcb2a..abf61d8de0 100644 --- a/packages/ui-components/src/components/Dialog.tsx +++ b/packages/ui-components/src/components/Dialog.tsx @@ -67,6 +67,7 @@ interface DialogHeaderProps { onClose: () => void; color?: TypographyProps['color']; children?: ReactNode; + titleVariant?: TypographyProps['variant']; } export const DialogHeader = ({ @@ -74,9 +75,10 @@ export const DialogHeader = ({ onClose, color = 'textPrimary', children, + titleVariant = 'h3', }: DialogHeaderProps) => (
- + {title} {children} diff --git a/packages/ui-components/src/components/Matrix/EnlargedMatrixCell.tsx b/packages/ui-components/src/components/Matrix/EnlargedMatrixCell.tsx index 02bd8f487a..26950f1c88 100644 --- a/packages/ui-components/src/components/Matrix/EnlargedMatrixCell.tsx +++ b/packages/ui-components/src/components/Matrix/EnlargedMatrixCell.tsx @@ -3,7 +3,7 @@ * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd */ import React, { useContext } from 'react'; -import { DialogContent } from '@material-ui/core'; +import { DialogContent, Typography } from '@material-ui/core'; import Markdown from 'markdown-to-jsx'; import { ACTION_TYPES, MatrixContext, MatrixDispatchContext } from './MatrixContext'; import styled from 'styled-components'; @@ -20,24 +20,40 @@ const DisplayWrapper = styled.div` margin-bottom: 1rem; `; +const SecondaryHeader = styled(Typography).attrs({ + variant: 'h3', +})` + font-size: 1.2rem; + font-weight: ${({ theme }) => theme.typography.fontWeightMedium}; + margin-bottom: 1rem; +`; /** * This is the modal that appears when a user clicks on a cell in the matrix */ export const EnlargedMatrixCell = () => { - const { enlargedCell } = useContext(MatrixContext); + const { enlargedCell, presentationOptions = {}, categoryPresentationOptions = {} } = useContext( + MatrixContext, + ); const dispatch = useContext(MatrixDispatchContext)!; + // If there is no enlarged cell set, don't render anything if (!enlargedCell) return null; - const { rowTitle, value, displayValue, presentation = {} } = enlargedCell; - const { description = '', showRawValue } = presentation; + const { rowTitle, value, displayValue, presentation = {}, isCategory } = enlargedCell; + // If it is a category header cell, use the category presentation options, otherwise use the normal presentation options + const presentationOptionsToUse = isCategory ? categoryPresentationOptions : presentationOptions; + + const { showRawValue } = presentationOptionsToUse; + const { description = '', label } = presentation; const closeModal = () => { dispatch({ type: ACTION_TYPES.SET_ENLARGED_CELL, payload: null }); }; + // Render the description, and also value if showRawValue is true. Also handle newlines in markdown const bodyText = `${description}${showRawValue ? ` ${value}` : ''}`.replace(/\\n/g, '\n\n'); return ( - + + {label && {label}} {displayValue} {bodyText.replace(/\\n/g, '\n\n')} diff --git a/packages/ui-components/src/components/Matrix/Matrix.tsx b/packages/ui-components/src/components/Matrix/Matrix.tsx index f1600c2d86..c89a0ff763 100644 --- a/packages/ui-components/src/components/Matrix/Matrix.tsx +++ b/packages/ui-components/src/components/Matrix/Matrix.tsx @@ -1,18 +1,19 @@ /* * Tupaia - * Copyright (c) 2017 - 2022 Beyond Essential Systems Pty Ltd + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd */ import React, { useEffect, useReducer, useRef } from 'react'; import styled from 'styled-components'; import { Table, TableBody } from '@material-ui/core'; -import { MatrixHeaderRow } from './MatrixHeaderRow'; +import { MatrixConfig } from '@tupaia/types'; import { MatrixColumnType, MatrixRowType } from '../../types'; +import { getFlattenedColumns, getFullHex } from './utils'; +import { MatrixHeader } from './MatrixHeader'; import { ACTION_TYPES, MatrixContext, MatrixDispatchContext, matrixReducer } from './MatrixContext'; import { MatrixNavButtons } from './MatrixNavButtons'; import { MatrixRow } from './MatrixRow'; import { EnlargedMatrixCell } from './EnlargedMatrixCell'; -import { getFullHex } from './utils'; const MatrixTable = styled.table` border-collapse: collapse; @@ -31,13 +32,13 @@ const Wrapper = styled.div` width: 100%; overflow: auto; `; -interface MatrixProps { + +interface MatrixProps extends Omit { columns: MatrixColumnType[]; rows: MatrixRowType[]; - presentationOptions: any; } -export const Matrix = ({ columns = [], rows = [], presentationOptions }: MatrixProps) => { +export const Matrix = ({ columns = [], rows = [], ...config }: MatrixProps) => { const [{ startColumn, expandedRows, maxColumns, enlargedCell }, dispatch] = useReducer( matrixReducer, { @@ -55,21 +56,23 @@ export const Matrix = ({ columns = [], rows = [], presentationOptions }: MatrixP if (!tableEl || !tableEl?.current || !tableEl?.current?.offsetWidth) return; const { offsetWidth } = tableEl?.current; // 200px is the max width of a column that we want to show - const maxColumns = Math.floor(offsetWidth / 200); + const usableWidth = offsetWidth - 200; // the max size of the first column (row title) + const maxColumns = Math.floor(usableWidth / 200); + + const flattenedColumns = getFlattenedColumns(columns); dispatch({ type: ACTION_TYPES.SET_MAX_COLUMNS, - payload: Math.min(maxColumns, columns.length), + payload: Math.min(maxColumns, flattenedColumns.length), }); }; updateMaxColumns(); }, [tableEl?.current?.offsetWidth, columns]); - return ( - + {rows.map(row => ( ))}
+
diff --git a/packages/ui-components/src/components/Matrix/MatrixCell.tsx b/packages/ui-components/src/components/Matrix/MatrixCell.tsx index bb54562fe3..39916ffaaa 100644 --- a/packages/ui-components/src/components/Matrix/MatrixCell.tsx +++ b/packages/ui-components/src/components/Matrix/MatrixCell.tsx @@ -6,9 +6,16 @@ import React, { useContext } from 'react'; import { TableCell, Button } from '@material-ui/core'; import styled from 'styled-components'; -import { getIsUsingDots, getPresentationOption, getFullHex } from './utils'; +import { ConditionalPresentationOptions } from '@tupaia/types'; +import { + checkIfApplyDotStyle, + getFlattenedColumns, + getIsUsingDots, + getPresentationOption, + getFullHex, +} from './utils'; import { ACTION_TYPES, MatrixContext, MatrixDispatchContext } from './MatrixContext'; -import { MatrixRowType } from '../../types'; +import { MatrixColumnType, MatrixRowType } from '../../types'; export const Dot = styled.div<{ $color?: string }>` width: 2rem; @@ -25,11 +32,11 @@ export const Dot = styled.div<{ $color?: string }>` const DataCell = styled(TableCell)` vertical-align: middle; - text-align: center; position: relative; z-index: 1; padding: 0; height: 100%; + border: 1px solid ${({ theme }) => getFullHex(theme.palette.text.primary)}33; `; const DataCellContent = styled.div` @@ -38,13 +45,12 @@ const DataCellContent = styled.div` width: 100%; display: flex; align-items: center; + text-align: center; + justify-content: center; +`; + +const ExpandButton = styled(Button)` &:hover { - background-color: rgba( - 255, - 255, - 255, - 0.08 - ); // replicate the hover effect for all cell content, not just buttons ${Dot} { transform: scale(1.2); } @@ -54,13 +60,28 @@ const DataCellContent = styled.div` interface MatrixRowProps { value: any; rowTitle: MatrixRowType['title']; + isCategory?: boolean; + colKey: MatrixColumnType['key']; } -export const MatrixCell = ({ value, rowTitle }: MatrixRowProps) => { - const { presentationOptions = {} } = useContext(MatrixContext); +/** + * This renders a cell in the matrix table. It can either be a category header cell or a data cell. If it has presentation options, it will be a button that can be clicked to expand the data. Otherwise, it will just display the data as normal + */ +export const MatrixCell = ({ value, rowTitle, isCategory, colKey }: MatrixRowProps) => { + const { presentationOptions = {}, categoryPresentationOptions = {}, columns } = useContext( + MatrixContext, + ); const dispatch = useContext(MatrixDispatchContext)!; - const isDots = getIsUsingDots(presentationOptions); - const presentation = getPresentationOption(presentationOptions, value); + // If the cell is a category, it means it is a category header cell and should use the category presentation options. Otherwise, it should use the normal presentation options + + const allColumns = getFlattenedColumns(columns); + const colIndex = allColumns.findIndex(({ key }) => key === colKey); + + const presentationOptionsForCell = isCategory ? categoryPresentationOptions : presentationOptions; + const isDots = + getIsUsingDots(presentationOptionsForCell) && + checkIfApplyDotStyle(presentationOptionsForCell as ConditionalPresentationOptions, colIndex); + const presentation = getPresentationOption(presentationOptionsForCell, value); const displayValue = isDots ? ( { ) : ( value ); - // If the cell has presentation options, it should be a button so that the data can be expanded. Otherwise, it can just display the data as normal - const isButton = !!presentation; const onClickCellButton = () => { dispatch({ type: ACTION_TYPES.SET_ENLARGED_CELL, @@ -81,14 +100,15 @@ export const MatrixCell = ({ value, rowTitle }: MatrixRowProps) => { value, displayValue, presentation, + isCategory, }, }); }; return ( {displayValue} diff --git a/packages/ui-components/src/components/Matrix/MatrixContext.ts b/packages/ui-components/src/components/Matrix/MatrixContext.ts index 5488959b0e..42cc60800c 100644 --- a/packages/ui-components/src/components/Matrix/MatrixContext.ts +++ b/packages/ui-components/src/components/Matrix/MatrixContext.ts @@ -1,21 +1,24 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ + import { Dispatch, createContext } from 'react'; +import { MatrixConfig, PresentationOptions } from '@tupaia/types'; import { MatrixColumnType, MatrixRowType } from '../../types'; -import { PresentationOptions } from '@tupaia/types'; type RowTitle = MatrixRowType['title']; const defaultContextValue = { rows: [], columns: [], - presentationOptions: {}, startColumn: 0, maxColumns: 0, expandedRows: [], enlargedCell: null, -} as { +} as Omit & { rows: MatrixRowType[]; columns: MatrixColumnType[]; - presentationOptions: PresentationOptions; startColumn: number; maxColumns: number; expandedRows: RowTitle[]; diff --git a/packages/ui-components/src/components/Matrix/MatrixHeader.tsx b/packages/ui-components/src/components/Matrix/MatrixHeader.tsx new file mode 100644 index 0000000000..822bf36d57 --- /dev/null +++ b/packages/ui-components/src/components/Matrix/MatrixHeader.tsx @@ -0,0 +1,82 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ +import React, { useContext } from 'react'; +import { TableCell, TableHead, TableRow, darken } from '@material-ui/core'; +import styled from 'styled-components'; +import { MatrixContext } from './MatrixContext'; +import { getDisplayedColumns, getFullHex } from './utils'; +import { MatrixColumnType } from '../../types'; + +const HeaderCell = styled(TableCell)` + text-align: center; + max-width: 12rem; + border-width: 1px 1px 2px 1px; + border-style: solid; + border-color: ${({ theme }) => darken(theme.palette.text.primary, 0.4)} + ${({ theme }) => getFullHex(theme.palette.text.primary)}33; +`; + +const ColGroup = styled.colgroup` + border: 2px solid ${({ theme }) => darken(theme.palette.text.primary, 0.4)}; +`; + +/** + * This is a component that renders the header rows in the matrix. It renders the column groups and columns. + */ +export const MatrixHeader = () => { + const { columns, startColumn, maxColumns, hideColumnTitles = false } = useContext(MatrixContext); + const displayedColumns = getDisplayedColumns(columns, startColumn, maxColumns); + // If a column is not displayed, then it should not be rendered in the header. This means that if a column group has no displayed children, then it should not be rendered either. + const displayedColumnGroups = columns.reduce( + (result: MatrixColumnType[], column: MatrixColumnType) => { + const visibleChildren = + column.children?.filter(child => !!displayedColumns.find(({ key }) => key === child.key)) || + []; + if (!visibleChildren.length) return result; + return [...result, { ...column, children: visibleChildren }]; + }, + [], + ); + + // If there are parents, then there should be two rows: 1 for the column group headings, and one for the column headings + const hasParents = displayedColumnGroups.length > 0; + + return ( + /** If there are no parents, then there are only column groups to style for the row header column and the rest of the table. Otherwise, there are column groups for each displayed column group, plus one for the row header column.*/ + <> + + {hasParents ? ( + <> + {displayedColumnGroups.map(({ title, children = [] }) => ( + + ))} + + ) : ( + + )} + + {hasParents && ( + + + {displayedColumnGroups.map(({ title, children = [] }) => ( + + {title} + + ))} + + )} + + {/** If hasParents is true, then this row header column cell will have already been rendered. */} + {!hasParents && } + {displayedColumns.map(({ title, key }) => ( + + {!hideColumnTitles && title} + + ))} + + + + ); +}; diff --git a/packages/ui-components/src/components/Matrix/MatrixHeaderRow.tsx b/packages/ui-components/src/components/Matrix/MatrixHeaderRow.tsx deleted file mode 100644 index 169ec9d1e8..0000000000 --- a/packages/ui-components/src/components/Matrix/MatrixHeaderRow.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Tupaia - * Copyright (c) 2017 - 2022 Beyond Essential Systems Pty Ltd - */ -import React, { useContext } from 'react'; -import { TableCell, TableHead, TableRow } from '@material-ui/core'; -import styled from 'styled-components'; -import { MatrixContext } from './MatrixContext'; -import { MatrixColumnType } from '../../types'; - -const HeaderCell = styled(TableCell)` - text-align: center; - max-width: 12.5rem; -`; - -export const MatrixHeaderRow = () => { - const { columns, startColumn, maxColumns } = useContext(MatrixContext); - - const displayedColumns = columns.slice( - startColumn, - startColumn + maxColumns, - ) as MatrixColumnType[]; - return ( - - - - {displayedColumns.map(({ title }) => ( - {title} - ))} - - - ); -}; diff --git a/packages/ui-components/src/components/Matrix/MatrixLegend.tsx b/packages/ui-components/src/components/Matrix/MatrixLegend.tsx new file mode 100644 index 0000000000..35284e03f2 --- /dev/null +++ b/packages/ui-components/src/components/Matrix/MatrixLegend.tsx @@ -0,0 +1,66 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ + +import React, { useContext } from 'react'; +import { ConditionalPresentationOptions } from '@tupaia/types'; +import styled from 'styled-components'; +import { Typography } from '@material-ui/core'; +import { MatrixContext } from './MatrixContext'; +import { getIsUsingDots } from './utils'; + +const Wrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; + margin-top: 1rem; +`; + +const LegendItem = styled.div` + display: flex; + align-items: center; + padding: 0.5rem 0; + &:not(:last-child) { + margin-right: 1rem; + } +`; + +const LegendDot = styled.div<{ $color?: string }>` + background-color: ${({ $color }) => $color}; + border: 1px solid ${({ theme }) => theme.palette.text.primary}; + width: 1rem; + height: 1rem; + border-radius: 50%; +`; + +const LegendLabel = styled(Typography)` + margin-left: 0.5rem; +`; + +/** + * Renders a legend for the matrix, if the matrix is using dots + */ +export const MatrixLegend = () => { + const { presentationOptions } = useContext(MatrixContext); + + // Only render if the matrix is using dots. Otherwise, return null + if (!presentationOptions || !getIsUsingDots(presentationOptions)) return null; + + const { conditions } = presentationOptions as ConditionalPresentationOptions; + const legendConditions = conditions?.filter(condition => !!condition.legendLabel); + + // Only render if there are legend conditions. Otherwise, return null + if (legendConditions?.length === 0) return null; + return ( + + {legendConditions?.map(({ color, legendLabel }) => ( + + + {legendLabel} + + ))} + + ); +}; diff --git a/packages/ui-components/src/components/Matrix/MatrixNavButtons.tsx b/packages/ui-components/src/components/Matrix/MatrixNavButtons.tsx index e6ea139064..9ddb139e48 100644 --- a/packages/ui-components/src/components/Matrix/MatrixNavButtons.tsx +++ b/packages/ui-components/src/components/Matrix/MatrixNavButtons.tsx @@ -1,6 +1,6 @@ /* * Tupaia - * Copyright (c) 2017 - 2022 Beyond Essential Systems Pty Ltd + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd */ import React, { useContext } from 'react'; @@ -8,10 +8,12 @@ import styled from 'styled-components'; import { KeyboardArrowLeft, KeyboardArrowRight } from '@material-ui/icons'; import { Button } from '../Button'; import { ACTION_TYPES, MatrixContext, MatrixDispatchContext } from './MatrixContext'; +import { getFlattenedColumns } from './utils'; const TableMoveButtonWrapper = styled.div` display: flex; - justify-content: space-between; + justify-content: flex-end; + align-items: center; width: 100%; margin-bottom: 1rem; `; @@ -22,6 +24,7 @@ const TableMoveButton = styled(Button).attrs({ })` text-transform: none; font-size: 0.875rem; + padding: 0.5rem 1rem 0.5rem 0.5rem; // to compensate for left arrow background-color: transparent; border-color: transparent; font-weight: ${({ theme }) => theme.typography.fontWeightBold}; @@ -35,13 +38,22 @@ const TableMoveButton = styled(Button).attrs({ &:focus { color: ${({ theme }) => theme.palette.primary.main}; } + &:last-child { + padding: 0.5rem 0.5rem 0.5rem 1rem; // to compensate for right arrow + margin-left: 0; + } `; +/** + * Renders the buttons to move the columns left and right + */ export const MatrixNavButtons = () => { - const { startColumn, columns, maxColumns } = useContext(MatrixContext); + const { startColumn, maxColumns, columns } = useContext(MatrixContext); + // Get all of the flattened columns + const childColumns = getFlattenedColumns(columns); const dispatch = useContext(MatrixDispatchContext)!; - // Show the previous button when the first column is not visible - const showButtons = columns.length > maxColumns; + // Show the buttons when there are more columns than the max columns + const showButtons = childColumns.length > maxColumns; const handleMoveColumnLeft = () => { dispatch({ type: ACTION_TYPES.DECREASE_START_COLUMN }); @@ -64,7 +76,7 @@ export const MatrixNavButtons = () => { Previous = columns.length - maxColumns} + disabled={startColumn >= childColumns.length - maxColumns} onClick={handleMoveColumnRight} title="Show next columns" > diff --git a/packages/ui-components/src/components/Matrix/MatrixRow.tsx b/packages/ui-components/src/components/Matrix/MatrixRow.tsx index 806e0bcb4c..1c54073711 100644 --- a/packages/ui-components/src/components/Matrix/MatrixRow.tsx +++ b/packages/ui-components/src/components/Matrix/MatrixRow.tsx @@ -1,6 +1,6 @@ /* * Tupaia - * Copyright (c) 2017 - 2022 Beyond Essential Systems Pty Ltd + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd */ import React, { useContext } from 'react'; @@ -10,6 +10,7 @@ import styled from 'styled-components'; import { MatrixRowType } from '../../types'; import { MatrixCell } from './MatrixCell'; import { ACTION_TYPES, MatrixContext, MatrixDispatchContext } from './MatrixContext'; +import { getDisplayedColumns } from './utils'; const ExpandIcon = styled(KeyboardArrowRight)<{ $expanded: boolean; @@ -55,71 +56,89 @@ interface MatrixRowProps { parents: MatrixRowTitle[]; } -const ExpandableRow = ({ row, parents = [] }: MatrixRowProps) => { - const { children, title } = row; +/** + * This component renders the first cell of a row. It renders a button to expand/collapse the row if it has children, otherwise it renders a regular cell. + */ +const RowHeaderCell = ({ + rowTitle, + depth, + isExpanded, + hasChildren, + children, +}: { + depth: number; + isExpanded: boolean; + rowTitle: string; + hasChildren: boolean; + children: React.ReactNode; +}) => { const dispatch = useContext(MatrixDispatchContext)!; - const { maxColumns, expandedRows } = useContext(MatrixContext); - - const toggleExpandedRows = (rowTitle: string) => { - if (expandedRows.includes(rowTitle)) { + const toggleExpandedRows = () => { + if (isExpanded) { dispatch({ type: ACTION_TYPES.COLLAPSE_ROW, payload: rowTitle }); } else { dispatch({ type: ACTION_TYPES.EXPAND_ROW, payload: rowTitle }); } }; - const isExpanded = expandedRows.includes(title); - const isVisible = parents.every(parent => expandedRows.includes(parent)); - const depth = parents.length; + + if (!hasChildren) + return {children}; return ( - <> - - - - toggleExpandedRows(title)} - > - - - {title} - - - {/** render empty cells for the rest of the row */} - {Array(maxColumns) - .fill(0) - .map(() => ( - - ))} - - {children?.map(child => ( - - ))} - + + + + + + {children} + + ); }; +/** + * This is a recursive component that renders a row in the matrix. It renders a MatrixRowGroup component if the row has children, otherwise it renders a regular row. + */ export const MatrixRow = ({ row, parents = [] }: MatrixRowProps) => { const { children, title } = row; const { columns, startColumn, maxColumns, expandedRows } = useContext(MatrixContext); + + const displayedColumns = getDisplayedColumns(columns, startColumn, maxColumns); + + const isExpanded = expandedRows.includes(title); const isVisible = parents.every(parent => expandedRows.includes(parent)); const depth = parents.length; - // if is nested render a group - if (children) return ; + const isCategory = children ? children.length > 0 : false; - const displayedColumns = columns.slice(startColumn, startColumn + maxColumns); // render a regular row with the title cell and the values return ( - 0}> - {title} - {displayedColumns.map(({ key }) => ( - + <> + 0}> + + {title} + + {displayedColumns.map(({ key, title }) => ( + + ))} + + {children?.map(child => ( + ))} - + ); }; diff --git a/packages/ui-components/src/components/Matrix/index.ts b/packages/ui-components/src/components/Matrix/index.ts index af09083ad7..dd73d8877f 100644 --- a/packages/ui-components/src/components/Matrix/index.ts +++ b/packages/ui-components/src/components/Matrix/index.ts @@ -1,6 +1,6 @@ /* * Tupaia - * Copyright (c) 2017 - 2022 Beyond Essential Systems Pty Ltd + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd */ export { Matrix } from './Matrix'; diff --git a/packages/ui-components/src/components/Matrix/utils.ts b/packages/ui-components/src/components/Matrix/utils.ts index d6dc0241a3..0799c8e2d7 100644 --- a/packages/ui-components/src/components/Matrix/utils.ts +++ b/packages/ui-components/src/components/Matrix/utils.ts @@ -1,19 +1,26 @@ /* * Tupaia - * Copyright (c) 2017 - 2022 Beyond Essential Systems Pty Ltd + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd */ import { find, isString, isNumber } from 'lodash'; -import { PresentationOptions, ConditionValue } from '@tupaia/types'; +import { + PresentationOptions, + ConditionValue, + RangePresentationOptions, + ConditionalPresentationOptions, + PresentationOptionCondition, +} from '@tupaia/types'; +import { MatrixColumnType } from '../../types'; /** * This file contains any utils that useful for the matrix component. This is mainly used for presentation options */ -export const areStringsEqual = (a: string, b: number, caseSensitive = true) => +export const areStringsEqual = (a: string, b: string, caseSensitive = true) => a .toString() .localeCompare(b.toString(), undefined, caseSensitive ? {} : { sensitivity: 'accent' }) === 0; -// Handles when MUI theme variables are shortened hex codes, e.g '#fff' +// If the hex is shortened, double up each character. This is for cases like '#fff' export const getFullHex = (hex: string) => { let hexString = hex.replace('#', ''); const isShortened = hexString.length === 3; @@ -27,7 +34,7 @@ export const findByKey = ( caseSensitive = true, ) => (isString(key) || isNumber(key)) && - find(collection, (value, i) => areStringsEqual(key, i, caseSensitive)); + find(collection, (value, valueKey) => areStringsEqual(key, valueKey, caseSensitive)); /** Functions used to get matrix chart dot colors from presentation options */ const PRESENTATION_TYPES = { @@ -43,39 +50,57 @@ const CONDITION_CHECK_METHOD = { '<=': (value: number, filterValue: number) => value <= filterValue, }; +// This function is used to get the presentation option from the conditions, where the key is the value const getPresentationOptionFromKey = (options: PresentationOptions['conditions'], value: any) => findByKey(options, value, false) || null; -const getPresentationOptionFromCondition = (options: PresentationOptions = {}, value: any) => { +// This function is used to get the presentation option from the conditions, when conditions is an array +const getPresentationOptionFromCondition = ( + options: ConditionalPresentationOptions = {}, + value: any, +) => { const { conditions = [] } = options; - const option = conditions.find(({ condition }) => { - if (typeof condition === 'object') { - // Check if the value satisfies all the conditions if condition is an object - return Object.entries(condition).every(([operator, conditionalValue]) => { - const checkConditionMethod = - CONDITION_CHECK_METHOD[operator as keyof typeof CONDITION_CHECK_METHOD]; - - return checkConditionMethod - ? checkConditionMethod(value, conditionalValue as number) - : false; - }); - } + const option = conditions.find( + ({ condition }: { condition: PresentationOptionCondition['condition'] }) => { + if (typeof condition === 'object') { + // Check if the value satisfies all the conditions if condition is an object + return Object.entries(condition).every(([operator, conditionalValue]) => { + const checkConditionMethod = + CONDITION_CHECK_METHOD[operator as keyof typeof CONDITION_CHECK_METHOD]; - // If condition is not an object, assume its the value we want to check (with '=' operator) - const checkConditionMethod = CONDITION_CHECK_METHOD['=']; - return checkConditionMethod(value, condition); - }); + return checkConditionMethod + ? checkConditionMethod(value, conditionalValue as number) + : false; + }); + } + + // If condition is not an object, assume its the value we want to check (with '=' operator) + const checkConditionMethod = CONDITION_CHECK_METHOD['=']; + return checkConditionMethod(value, condition); + }, + ); + + return option; +}; +// This function returns the applicable presentation option from range type presentation options +export const getPresentationOptionFromRange = (options: RangePresentationOptions, value: any) => { + const { type, showRawValue, ...rest } = options; + const option = Object.values(rest).find(({ min, max }) => { + return min !== undefined && value >= min && (max === undefined || value <= max); + }); + if (value === undefined || value === '' || option === undefined) return null; return option; }; +// This function returns the applicable presentation option from the presentation options, for the value export const getPresentationOption = (options: PresentationOptions, value: any) => { switch (options.type) { - // case PRESENTATION_TYPES.RANGE: - // return getPresentationOptionFromRange(options, value); + case PRESENTATION_TYPES.RANGE: + return getPresentationOptionFromRange(options as RangePresentationOptions, value); case PRESENTATION_TYPES.CONDITION: - return getPresentationOptionFromCondition(options, value); + return getPresentationOptionFromCondition(options as ConditionalPresentationOptions, value); default: return getPresentationOptionFromKey(options?.conditions, value); } @@ -85,6 +110,30 @@ export function getIsUsingDots(presentationOptions: PresentationOptions = {}) { return Object.keys(presentationOptions).length > 0; } -export function checkIfApplyDotStyle(presentationOptions: PresentationOptions = {}) { - return presentationOptions?.applyLocation?.columnIndexes; +export function checkIfApplyDotStyle( + presentationOptions: ConditionalPresentationOptions = {}, + columnIndex: number, +) { + const appliedLocations = presentationOptions?.applyLocation?.columnIndexes; + if (!appliedLocations) return true; + return appliedLocations.includes(columnIndex); +} + +// This function returns a flattened array of columns, NOT including the parent columns +export function getFlattenedColumns(columns: MatrixColumnType[]): MatrixColumnType[] { + return columns.reduce((cols, column) => { + if (column.children) { + return [...cols, ...getFlattenedColumns(column.children)]; + } + return [...cols, column]; + }, [] as MatrixColumnType[]); +} + +// This function returns the displayed columns, based on the start column and max columns +export function getDisplayedColumns( + columns: MatrixColumnType[], + startColumn: number, + maxColumns: number, +) { + return getFlattenedColumns(columns).slice(startColumn, startColumn + maxColumns); } diff --git a/packages/ui-components/stories/matrix.stories.js b/packages/ui-components/stories/matrix.stories.js index 405aaa6742..04ff0178c2 100644 --- a/packages/ui-components/stories/matrix.stories.js +++ b/packages/ui-components/stories/matrix.stories.js @@ -31,7 +31,7 @@ export default { }, }; -const nestedRows = [ +const groupedRows = [ { title: 'Data item 1', children: [ @@ -182,10 +182,12 @@ const basicRows = [ title: 'Data item 6', Col1: 534, Col3: 0, + Col5: 10, + Col6: 3, }, ]; -const columns = [ +const basicColumns = [ { key: 'Col1', title: 'Demo country 1', @@ -204,13 +206,50 @@ const columns = [ }, ]; +const groupedColumns = [ + { + title: 'Column group 1', + children: [ + { + key: 'Col1', + title: 'Col1', + }, + { + key: 'Col2', + title: 'Col2', + }, + { + key: 'Col3', + title: 'Col3', + }, + ], + }, + { + title: 'Column group 2', + children: [ + { + key: 'Col4', + title: 'Col4', + }, + { + key: 'Col5', + title: 'Col5', + }, + { + key: 'Col6', + title: 'Col6', + }, + ], + }, +]; + const dotPresentationOptions = { type: 'condition', conditions: [ { key: 'red', color: '#b71c1c', - label: '', + label: 'Secondary header', condition: 0, description: 'Months of stock: ', legendLabel: 'Stock out (MOS 0)', @@ -251,20 +290,271 @@ const dotPresentationOptions = { showRawValue: true, }; -const Template = args => ; +const markdownPresentationOptions = { + conditions: [ + { + key: '0', + color: '#525258', + label: '', + description: 'A **Blank** cell means this.\n', + }, + { + key: '1', + color: '#279A63', + label: '', + description: '**Green** signifies something else.\n', + }, + { + key: '2', + color: '#EE9A30', + label: '', + description: '**Orange** signifies another thing.\n', + }, + { + key: '3', + color: '#EE4230', + label: '', + description: '**Red** signifies this other thing.\n', + }, + ], +}; + +const groupedRowsWithCategoryData = [ + { + title: 'Data item 1', + Col1: 10, + Col2: 20, + Col3: 30, + Col4: 40, + children: [ + { + title: 'Sub item 1', + Col4: 59.5, + Col2: 43.4, + }, + { + title: 'Sub item 2', + Col1: 6.76, + Col4: 74.2, + Col3: 44.998, + }, + { + title: 'Sub item 3', + Col1: 33.9, + Col4: 11.749, + Col2: 6.347, + Col3: 35.9, + }, + { + title: 'Sub item 4', + Col1: 0.05, + Col4: 32.11, + Col2: 0, + Col3: 7.1, + }, + { + title: 'Sub item 5', + Col1: 320, + Col4: 17, + Col2: 69.325, + Col3: 142.68, + }, + { + title: 'Sub item 6', + Col1: 534, + Col3: 0, + }, + ], + }, + { + title: 'Data item 2', + Col1: 1, + Col2: 2, + Col3: 4, + Col4: 25, + children: [ + { + title: 'Sub item 7', + Col1: 661, + Col4: 0, + Col2: 3.78, + Col3: 0, + }, + { + title: 'Sub item 8', + Col4: 24, + Col3: 53.4, + }, + { + title: 'Sub item 9', + Col4: 0, + Col2: 7.92, + }, + { + title: 'Sub item 10', + Col1: 46.22, + Col4: 2.35, + Col3: 1.62, + }, + { + title: 'Sub item 11', + }, + ], + }, + { + title: 'Data item 3', + Col1: 2, + Col4: 40, + children: [ + { + title: 'Sub item 12', + Col1: 10, + Col4: 50, + children: [ + { + title: 'Sub item 13', + Col1: 661.7, + Col4: 0, + Col2: 3.78, + Col3: 0, + }, + { + title: 'Sub item 14', + Col4: 24.34, + Col3: 53.43, + }, + ], + }, + { + title: 'Sub item 15', + children: [ + { + title: 'Sub item 16', + Col4: 22.86, + Col2: 19.24, + Col3: 1.532, + }, + { + title: 'Sub item 17', + Col4: 0, + Col2: 7.92, + }, + ], + }, + ], + }, +]; + +const rangeCategoryPresentationOptions = { + red: { + max: 20, + min: 0, + color: '#b71c1c', + label: '', + description: 'Averaged score:', + }, + type: 'range', + green: { + max: 100, + min: 70, + color: '#33691e', + label: '', + description: 'Averaged score:', + }, + yellow: { + max: 69, + min: 21, + color: '#fdd835', + label: '', + description: 'Averaged score:', + }, + showRawValue: true, +}; + +const applyLocationPresentationOptions = { + ...dotPresentationOptions, + applyLocation: { + columnIndexes: [3], + }, +}; + +const Template = args => ; export const Simple = Template.bind({}); Simple.args = { rows: basicRows, + columns: basicColumns, +}; + +export const GroupedRowsWithDots = Template.bind({}); +GroupedRowsWithDots.args = { + rows: groupedRows, + presentationOptions: dotPresentationOptions, + columns: basicColumns, }; -export const NestedWithDots = Template.bind({}); -NestedWithDots.args = { - rows: nestedRows, +export const HiddenColumnTitles = Template.bind({}); +HiddenColumnTitles.args = { + rows: groupedRows, presentationOptions: dotPresentationOptions, + hideColumnTitles: true, + columns: basicColumns, +}; + +export const GroupsRowsWithBasicData = Template.bind({}); +GroupsRowsWithBasicData.args = { + rows: groupedRows, + columns: basicColumns, }; -export const NestedWithBasicData = Template.bind({}); -NestedWithBasicData.args = { - rows: nestedRows, +export const GroupedColumns = Template.bind({}); +GroupedColumns.args = { + rows: basicRows, + columns: groupedColumns, +}; + +export const GroupedColumnsAndRows = Template.bind({}); +GroupedColumnsAndRows.args = { + rows: groupedRows, + columns: groupedColumns, +}; + +export const MarkdownPresentationDescription = Template.bind({}); +MarkdownPresentationDescription.args = { + rows: [ + { + title: 'Data item 1', + Col4: 2, + Col2: 3, + }, + { + title: 'Data item 2', + Col1: 3, + Col4: 2, + Col3: 4, + }, + { + title: 'Data item 3', + Col1: 3, + Col4: 1, + Col2: 2, + Col3: 2, + }, + ], + presentationOptions: markdownPresentationOptions, + columns: basicColumns, +}; + +export const GroupsRowsWithCategoryPresentationOptions = Template.bind({}); +GroupsRowsWithCategoryPresentationOptions.args = { + rows: groupedRowsWithCategoryData, + columns: basicColumns, + categoryPresentationOptions: rangeCategoryPresentationOptions, +}; + +export const ApplyLocationPresentationOptions = Template.bind({}); +ApplyLocationPresentationOptions.args = { + rows: basicRows, + presentationOptions: applyLocationPresentationOptions, + columns: basicColumns, }; diff --git a/packages/ui-map-components/src/components/MarkerLayer.tsx b/packages/ui-map-components/src/components/MarkerLayer.tsx index af1bb63c00..2a8fa6f6c7 100644 --- a/packages/ui-map-components/src/components/MarkerLayer.tsx +++ b/packages/ui-map-components/src/components/MarkerLayer.tsx @@ -40,11 +40,11 @@ const processData = (measureData: MeasureData[], serieses: Series[]): MeasureDat }; interface MarkerLayerProps { - measureData: MeasureData[]; - serieses: Series[]; - multiOverlayMeasureData: GenericDataItem[]; - multiOverlaySerieses: Series[]; - onSeeOrgUnitDashboard: (organisationUnitCode?: string) => void; + measureData?: MeasureData[]; + serieses?: Series[]; + multiOverlayMeasureData?: GenericDataItem[]; + multiOverlaySerieses?: Series[]; + onSeeOrgUnitDashboard?: (organisationUnitCode?: string) => void; } export const MarkerLayer = ({ diff --git a/packages/ui-map-components/src/components/Markers/MeasureMarker.tsx b/packages/ui-map-components/src/components/Markers/MeasureMarker.tsx index d8cd282aec..a6ed987913 100644 --- a/packages/ui-map-components/src/components/Markers/MeasureMarker.tsx +++ b/packages/ui-map-components/src/components/Markers/MeasureMarker.tsx @@ -11,7 +11,7 @@ import { MarkerProps } from '../../types'; export const MeasureMarker = React.memo((props: MarkerProps) => { const { icon, radius = 0 } = props; - if (radius && parseInt(String(radius), 10) === 0) { + if (radius !== undefined && parseInt(String(radius), 10) === 0) { if (icon) { // we have an icon, so don't render the radius at all return ;