From 1e1bb59171301fbfa8455c159ac39bec995a648e Mon Sep 17 00:00:00 2001 From: j8seangel Date: Wed, 8 Jan 2025 20:27:39 +0100 Subject: [PATCH 01/62] initial visualization library commit --- libs/responsive-visualizations/.babelrc | 12 ++ libs/responsive-visualizations/README.md | 7 + .../eslint.config.js | 3 + libs/responsive-visualizations/package.json | 21 +++ libs/responsive-visualizations/project.json | 54 +++++++ .../rollup.config.js | 14 ++ .../src/charts/barchart/BarChart.tsx | 135 ++++++++++++++++++ .../charts/barchart/BarChartAggregated.tsx | 48 +++++++ .../barchart/BarChartIndividual.module.css | 33 +++++ .../charts/barchart/BarChartIndividual.tsx | 122 ++++++++++++++++ .../src/charts/index.ts | 1 + libs/responsive-visualizations/src/index.ts | 2 + .../src/lib/density.ts | 47 ++++++ libs/responsive-visualizations/src/types.d.ts | 15 ++ libs/responsive-visualizations/src/types.ts | 17 +++ libs/responsive-visualizations/tsconfig.json | 17 +++ .../tsconfig.lib.json | 19 +++ .../tsconfig.node.json | 26 ++++ 18 files changed, 593 insertions(+) create mode 100644 libs/responsive-visualizations/.babelrc create mode 100644 libs/responsive-visualizations/README.md create mode 100644 libs/responsive-visualizations/eslint.config.js create mode 100644 libs/responsive-visualizations/package.json create mode 100644 libs/responsive-visualizations/project.json create mode 100644 libs/responsive-visualizations/rollup.config.js create mode 100644 libs/responsive-visualizations/src/charts/barchart/BarChart.tsx create mode 100644 libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx create mode 100644 libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.module.css create mode 100644 libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx create mode 100644 libs/responsive-visualizations/src/charts/index.ts create mode 100644 libs/responsive-visualizations/src/index.ts create mode 100644 libs/responsive-visualizations/src/lib/density.ts create mode 100644 libs/responsive-visualizations/src/types.d.ts create mode 100644 libs/responsive-visualizations/src/types.ts create mode 100644 libs/responsive-visualizations/tsconfig.json create mode 100644 libs/responsive-visualizations/tsconfig.lib.json create mode 100644 libs/responsive-visualizations/tsconfig.node.json diff --git a/libs/responsive-visualizations/.babelrc b/libs/responsive-visualizations/.babelrc new file mode 100644 index 0000000000..1ea870ead4 --- /dev/null +++ b/libs/responsive-visualizations/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/responsive-visualizations/README.md b/libs/responsive-visualizations/README.md new file mode 100644 index 0000000000..f881c2ed24 --- /dev/null +++ b/libs/responsive-visualizations/README.md @@ -0,0 +1,7 @@ +# responsive-visualizations + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build responsive-visualizations` to build the library. diff --git a/libs/responsive-visualizations/eslint.config.js b/libs/responsive-visualizations/eslint.config.js new file mode 100644 index 0000000000..71174b182c --- /dev/null +++ b/libs/responsive-visualizations/eslint.config.js @@ -0,0 +1,3 @@ +const gfwConfig = require('../../eslint.config') + +module.exports = gfwConfig diff --git a/libs/responsive-visualizations/package.json b/libs/responsive-visualizations/package.json new file mode 100644 index 0000000000..380503108d --- /dev/null +++ b/libs/responsive-visualizations/package.json @@ -0,0 +1,21 @@ +{ + "name": "@globalfishingwatch/responsive-visualizations", + "version": "14.0.9", + "description": "Set of react components and hooks to render responsive visualizations", + "author": "satellitestudio ", + "homepage": "https://github.com/GlobalFishingWatch/frontend#readme", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/GlobalFishingWatch/frontend.git" + }, + "scripts": {}, + "main": "./index.cjs", + "module": "./index.js", + "dependencies": { + "classnames": "2.x", + "react-dom": "18.x", + "react": "18.x", + "recharts": "2.x" + } +} diff --git a/libs/responsive-visualizations/project.json b/libs/responsive-visualizations/project.json new file mode 100644 index 0000000000..cfbcf2acc3 --- /dev/null +++ b/libs/responsive-visualizations/project.json @@ -0,0 +1,54 @@ +{ + "name": "responsive-visualizations", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/responsive-visualizations/src", + "projectType": "library", + "tags": [], + "targets": { + "dist": { + "executor": "@nx/rollup:rollup", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/responsive-visualizations", + "tsConfig": "libs/responsive-visualizations/tsconfig.lib.json", + "project": "libs/responsive-visualizations/package.json", + "extractCss": false, + "entryFile": "libs/responsive-visualizations/src/index.ts", + "external": ["countryflag"], + "format": ["esm", "cjs"], + "rollupConfig": "libs/responsive-visualizations/rollup.config.js", + "assets": [ + { + "glob": "libs/responsive-visualizations/README.md", + "input": ".", + "output": "." + }, + { + "glob": "libs/responsive-visualizations/src/base.css", + "input": ".", + "output": "." + }, + { + "glob": "libs/responsive-visualizations/src/header/html", + "input": ".", + "output": "." + }, + { + "glob": "libs/responsive-visualizations/src/icon/icons", + "input": ".", + "output": "." + } + ], + "styles": "libs/responsive-visualizations/src/base.css", + "updateBuildableProjectDepsInPackageJson": true + } + }, + "publish": { + "executor": "@nx/js:release-publish", + "dependsOn": ["dist"], + "options": { + "packageRoot": "dist/libs/responsive-visualizations" + } + } + } +} diff --git a/libs/responsive-visualizations/rollup.config.js b/libs/responsive-visualizations/rollup.config.js new file mode 100644 index 0000000000..f08f901822 --- /dev/null +++ b/libs/responsive-visualizations/rollup.config.js @@ -0,0 +1,14 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +const nrwlConfig = require('@nx/react/plugins/bundle-rollup') +const svgr = require('@svgr/rollup') +const dynamicImportVars = require('@rollup/plugin-dynamic-import-vars') +const pkg = require('./package.json') + +module.exports = (config) => { + nrwlConfig(config) + return { + ...config, + plugins: [...config.plugins, svgr(), dynamicImportVars()], + external: Object.keys(pkg.dependencies || {}), + } +} diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx new file mode 100644 index 0000000000..b5533ccd4d --- /dev/null +++ b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx @@ -0,0 +1,135 @@ +import type { ReactElement } from 'react' +import { useCallback, useEffect, useState } from 'react' +import type { ResponsiveVisualizationData } from '../../types' +import { getIsIndividualBarChartSupported } from '../../lib/density' +import { IndividualBarChart } from './BarChartIndividual' +import { AggregatedBarChart } from './BarChartAggregated' + +export type BaseResponsiveBarChartProps = { + color: string + customTick?: ReactElement + customTooltip?: ReactElement + valueFormatter?: (value: any) => string +} + +export type ResponsiveBarChartInteractionCallback = (item: any) => void + +export type ResponsiveBarChartProps = BaseResponsiveBarChartProps & { + containerRef: React.RefObject + onAggregatedItemClick?: ResponsiveBarChartInteractionCallback + onIndividualItemClick?: ResponsiveBarChartInteractionCallback + getIndividualData?: () => Promise> + getAggregatedData?: () => Promise> +} + +export function ResponsiveBarChart({ + containerRef, + getIndividualData, + getAggregatedData, + color, + customTick, + customTooltip, + valueFormatter, + onIndividualItemClick, + onAggregatedItemClick, +}: ResponsiveBarChartProps) { + const [data, setData] = useState(null) + const [isIndividualSupported, setIsIndividualSupported] = useState(false) + const [{ width, height }, setDimensions] = useState({ width: 0, height: 0 }) + + useEffect(() => { + const resizeObserver = new ResizeObserver(() => { + if (containerRef.current) { + const { width, height } = containerRef.current.getBoundingClientRect() + setDimensions({ width, height }) + } + }) + + if (containerRef.current) { + resizeObserver.observe(containerRef.current) + } + + return () => { + if (containerRef.current) { + resizeObserver.unobserve(containerRef.current) + } + } + }, [containerRef]) + + const loadData = useCallback( + async ({ width, height }: { width: number; height: number }) => { + if (getAggregatedData) { + const aggregatedData = await getAggregatedData() + if ( + getIndividualData && + getIsIndividualBarChartSupported({ data: aggregatedData, width, height }) + ) { + const individualData = await getIndividualData() + if (individualData) { + setIsIndividualSupported(true) + setData(individualData) + } else { + setIsIndividualSupported(false) + setData(aggregatedData) + } + } else { + setIsIndividualSupported(false) + setData(aggregatedData) + } + } else if (getIndividualData) { + const individualData = await getIndividualData() + if (getIsIndividualBarChartSupported({ data: individualData, width, height })) { + setIsIndividualSupported(true) + setData(individualData) + } else { + const aggregatedData = individualData.map((item) => ({ + name: item.name, + value: item.values.length, + })) + setIsIndividualSupported(false) + setData(aggregatedData) + } + } + }, + [getAggregatedData, getIndividualData] + ) + + useEffect(() => { + if (width && height) { + loadData({ width, height }) + } + }, [height, width, loadData]) + + if (!getAggregatedData && !getIndividualData) { + console.warn('No data getters functions provided') + return null + } + + if (!data) { + return 'Spinner' + } + if (isIndividualSupported && !data) { + return 'Spinner for individual' + } + + return isIndividualSupported ? ( + } + color={color} + onClick={onIndividualItemClick} + customTick={customTick} + customTooltip={customTooltip} + valueFormatter={valueFormatter} + /> + ) : ( + } + color={color} + onClick={onAggregatedItemClick} + customTick={customTick} + customTooltip={customTooltip} + valueFormatter={valueFormatter} + /> + ) +} diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx new file mode 100644 index 0000000000..fb7a67c9d2 --- /dev/null +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx @@ -0,0 +1,48 @@ +import { BarChart, Bar, XAxis, Tooltip, ResponsiveContainer, LabelList } from 'recharts' +import type { ResponsiveVisualizationData } from '../../types' +import type { BaseResponsiveBarChartProps, ResponsiveBarChartInteractionCallback } from './BarChart' + +type AggregatedBarChartProps = BaseResponsiveBarChartProps & { + data: ResponsiveVisualizationData<'aggregated'> + onClick?: ResponsiveBarChartInteractionCallback +} +export function AggregatedBarChart({ + data, + color, + customTick, + customTooltip, + valueFormatter, +}: AggregatedBarChartProps) { + return ( + + + {data && } + + valueFormatter?.(entry.value) || entry.value} + /> + + + + + ) +} diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.module.css b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.module.css new file mode 100644 index 0000000000..6967e6ec59 --- /dev/null +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.module.css @@ -0,0 +1,33 @@ +.container { + height: 100%; + padding-inline: 10px; + display: flex; + gap: 20px; + align-items: flex-end; + padding-bottom: 40px; +} + +.barContainer { + display: flex; + flex: 1; + height: 100%; + flex-direction: column; + align-items: center; + justify-content: flex-end; +} + +.bar { + display: flex; + gap: 3px; + flex-wrap: wrap-reverse; + justify-content: center; +} + +.point { + display: block; + width: 12px; + height: 12px; + background-color: red; + border-radius: 6px; + position: relative; +} diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx new file mode 100644 index 0000000000..7659f1a43c --- /dev/null +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx @@ -0,0 +1,122 @@ +import { BarChart, Bar, XAxis, Tooltip, ResponsiveContainer, LabelList } from 'recharts' +import { + useFloating, + autoUpdate, + offset, + flip, + shift, + useInteractions, + useHover, + useTransitionStyles, +} from '@floating-ui/react' +import type { ReactElement } from 'react' +import cx from 'classnames' +import { useState } from 'react' +import type { ResponsiveVisualizationData } from '../../types' +import type { BaseResponsiveBarChartProps, ResponsiveBarChartInteractionCallback } from './BarChart' +import styles from './BarChartIndividual.module.css' + +type IndividualBarChartProps = BaseResponsiveBarChartProps & { + width: number + data: ResponsiveVisualizationData<'individual'> + onClick?: ResponsiveBarChartInteractionCallback +} + +type IndividualBarChartPointProps = { + color?: string + point: IndividualBarChartProps['data'][0]['values'][0] + valueFormatter?: (value: any) => string + tooltip?: ReactElement + className?: string +} +export function IndividualBarChartPoint({ + point, + color, + tooltip, + valueFormatter, + className, +}: IndividualBarChartPointProps) { + const [isOpen, setIsOpen] = useState(false) + + const { refs, floatingStyles, context } = useFloating({ + open: isOpen, + placement: 'top', + onOpenChange: setIsOpen, + middleware: [offset(1), flip(), shift()], + whileElementsMounted: autoUpdate, + }) + const hover = useHover(context) + const { getReferenceProps, getFloatingProps } = useInteractions([hover]) + return ( +
  • + {isOpen && ( +
    + {tooltip ? tooltip : valueFormatter?.(point) || point.name} +
    + )} +
  • + ) +} +export function IndividualBarChart({ + data, + color, + customTick, + customTooltip, + valueFormatter, +}: IndividualBarChartProps) { + return ( + + + {data && } + +
    + {data.map((item, index) => ( +
    + +
      + {item.values?.map((point, pointIndex) => ( + + ))} +
    +
    + ))} +
    +
    + +
    +
    + ) +} diff --git a/libs/responsive-visualizations/src/charts/index.ts b/libs/responsive-visualizations/src/charts/index.ts new file mode 100644 index 0000000000..51f0691d1c --- /dev/null +++ b/libs/responsive-visualizations/src/charts/index.ts @@ -0,0 +1 @@ +export * from './barchart/BarChart' diff --git a/libs/responsive-visualizations/src/index.ts b/libs/responsive-visualizations/src/index.ts new file mode 100644 index 0000000000..baa86941b2 --- /dev/null +++ b/libs/responsive-visualizations/src/index.ts @@ -0,0 +1,2 @@ +export * from './charts' +export * from './types' diff --git a/libs/responsive-visualizations/src/lib/density.ts b/libs/responsive-visualizations/src/lib/density.ts new file mode 100644 index 0000000000..5a926bab78 --- /dev/null +++ b/libs/responsive-visualizations/src/lib/density.ts @@ -0,0 +1,47 @@ +import type { + ResponsiveVisualizationAggregatedItem, + ResponsiveVisualizationData, + ResponsiveVisualizationIndividualItem, +} from '../types' + +const COLUMN_LABEL_SIZE = 10 +const COLUMN_PADDING = 10 +// Comment this is the sum of .point size + .bar flex gap +const POINT_WIDTH = 15 +const AXIX_LABEL_PADDING = 40 + +export const getBarProps = ( + data: ResponsiveVisualizationData, + width: number +): { columnsNumber: number; columnsWidth: number; pointsByRow: number } => { + const columnsNumber = data.length + const columnsWidth = width / columnsNumber - COLUMN_PADDING * 2 + const pointsByRow = Math.floor(columnsWidth / POINT_WIDTH) + + return { columnsNumber, columnsWidth, pointsByRow } +} + +type IsIndividualSupportedParams = { + data: ResponsiveVisualizationData + width: number + height: number +} +export function getIsIndividualBarChartSupported({ + data, + width, + height, +}: IsIndividualSupportedParams): boolean { + const { pointsByRow } = getBarProps(data, width) + const biggestColumnValue = data.reduce((acc, column) => { + const value = (column as ResponsiveVisualizationIndividualItem).values + ? (column as ResponsiveVisualizationIndividualItem).values.length + : (column as ResponsiveVisualizationAggregatedItem).value + if (value > acc) { + return value + } + return acc + }, 0) + const rowsInBiggestColumn = Math.ceil(biggestColumnValue / pointsByRow) + const heightNeeded = rowsInBiggestColumn * POINT_WIDTH + return heightNeeded < height - AXIX_LABEL_PADDING - COLUMN_PADDING - COLUMN_LABEL_SIZE +} diff --git a/libs/responsive-visualizations/src/types.d.ts b/libs/responsive-visualizations/src/types.d.ts new file mode 100644 index 0000000000..9e78d237dd --- /dev/null +++ b/libs/responsive-visualizations/src/types.d.ts @@ -0,0 +1,15 @@ +declare module '*.svg' { + // eslint-disable-next-line @typescript-eslint/no-require-imports + import React = require('react') + export const ReactComponent: React.FC> + const src: string + export default src +} + +declare module '*.css' { + interface ClassNames { + [className: string]: string + } + const classNames: ClassNames + export = classNames +} diff --git a/libs/responsive-visualizations/src/types.ts b/libs/responsive-visualizations/src/types.ts new file mode 100644 index 0000000000..5e0b2c75f8 --- /dev/null +++ b/libs/responsive-visualizations/src/types.ts @@ -0,0 +1,17 @@ +export type ResponsiveVisualizationMode = 'individual' | 'aggregated' + +export type ResponsiveVisualizationChart = 'barchart' | 'timeseries' + +export type ResponsiveVisualizationAggregatedItem = { name: string; value: number } +export type ResponsiveVisualizationIndividualItem = { + name: string + values: { [key: string]: any }[] +} + +export type ResponsiveVisualizationData< + Data extends ResponsiveVisualizationMode | undefined = undefined, +> = Data extends 'aggregated' + ? ResponsiveVisualizationAggregatedItem[] + : Data extends 'individual' + ? ResponsiveVisualizationIndividualItem[] + : (ResponsiveVisualizationAggregatedItem | ResponsiveVisualizationIndividualItem)[] diff --git a/libs/responsive-visualizations/tsconfig.json b/libs/responsive-visualizations/tsconfig.json new file mode 100644 index 0000000000..05e2ebbace --- /dev/null +++ b/libs/responsive-visualizations/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ], + "extends": "../../tsconfig.base.json" +} diff --git a/libs/responsive-visualizations/tsconfig.lib.json b/libs/responsive-visualizations/tsconfig.lib.json new file mode 100644 index 0000000000..07f1e6cf46 --- /dev/null +++ b/libs/responsive-visualizations/tsconfig.lib.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["node", "@nx/react/typings/cssmodule.d.ts", "@nx/react/typings/image.d.ts"] + }, + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/responsive-visualizations/tsconfig.node.json b/libs/responsive-visualizations/tsconfig.node.json new file mode 100644 index 0000000000..1d810665df --- /dev/null +++ b/libs/responsive-visualizations/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "lib": ["es6", "dom"], + "jsx": "react", + "noImplicitAny": true, + "noImplicitThis": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "typeRoots": ["./types"], + "types": ["node"], + "noEmit": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "target": "es2020", + "allowJs": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} From 844e10cff0c33ae8511608583956f11265a57bc8 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Wed, 8 Jan 2025 20:27:52 +0100 Subject: [PATCH 02/62] use responsive-visualization in vessels list graph --- .../vessels/VesselGroupReportVessels.tsx | 9 ++- .../vessels/VesselGroupReportVesselsGraph.tsx | 79 +++++++++---------- .../vessel-group-report-vessels.selectors.ts | 70 +++++++++++++++- apps/fishing-map/tsconfig.json | 3 + tsconfig.base.json | 3 + 5 files changed, 120 insertions(+), 44 deletions(-) diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVessels.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVessels.tsx index d5bbf3ed3c..ca01a451a4 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVessels.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVessels.tsx @@ -1,7 +1,10 @@ import { Fragment } from 'react' import { useSelector } from 'react-redux' import ReportVesselsFilter from 'features/reports/shared/activity/vessels/ReportVesselsFilter' -import { selectVGRVesselsGraphDataGrouped } from 'features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors' +import { + selectVGRVesselsGraphAggregatedData, + selectVGRVesselsGraphIndividualData, +} from 'features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors' import {} from 'features/reports/vessel-groups/vessel-group-report.selectors' import { selectVGRVesselFilter, @@ -19,7 +22,8 @@ function VesselGroupReportVessels({ loading }: { loading: boolean }) { const subsection = useSelector(selectVGRVesselsSubsection) const reportDataview = useSelector(selectVGRDataview) const filter = useSelector(selectVGRVesselFilter) - const data = useSelector(selectVGRVesselsGraphDataGrouped) + const data = useSelector(selectVGRVesselsGraphAggregatedData) + const individualData = useSelector(selectVGRVesselsGraphIndividualData) return (
    @@ -29,6 +33,7 @@ function VesselGroupReportVessels({ loading }: { loading: boolean }) { { return null } -const CustomTick = (props: any) => { +const ReportGraphTick = (props: any) => { const { x, y, payload, width, visibleTicksCount, property, filterQueryParam, pageQueryParam } = props @@ -143,12 +145,14 @@ const CustomTick = (props: any) => { export default function VesselGroupReportVesselsGraph({ data, + individualData, color = COLOR_PRIMARY_BLUE, property, filterQueryParam, pageQueryParam, }: { - data: ReportEventsStatsResponseGroups + data: ResponsiveVisualizationData<'aggregated'> + individualData: ResponsiveVisualizationData<'individual'> color?: string property: VGREventsVesselsProperty filterQueryParam: @@ -158,6 +162,7 @@ export default function VesselGroupReportVesselsGraph({ | keyof Pick | keyof Pick }) { + const ref = useRef(null) const { dispatchQueryParams } = useLocationConnect() const onBarClick: CategoricalChartFunc = (e) => { const { payload } = e.activePayload?.[0] || {} @@ -170,46 +175,40 @@ export default function VesselGroupReportVesselsGraph({ }) } } + const onPointClick: CategoricalChartFunc = (e) => { + console.log('TODO', e) + } + + const getAggregatedData = useCallback(async () => { + return data + }, [data]) + const getIndividualData = useCallback(async () => { + return individualData + }, [individualData]) + return ( -
    +
    {data && data.length > 0 && ( - - - {data && } />} - - formatI18nNumber(entry.value)} - /> - - - } - tickMargin={0} + { + return getVesselShipNameLabel(value.identity) + }} + color={color} + customTick={ + - - + } + customTooltip={} + > )}
    diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts b/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts index 047a4a2a9b..f635904ca1 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts @@ -2,6 +2,7 @@ import { createSelector } from '@reduxjs/toolkit' import { groupBy } from 'es-toolkit' import type { Dataset, IdentityVessel } from '@globalfishingwatch/api-types' import { DatasetTypes, VesselIdentitySourceEnum } from '@globalfishingwatch/api-types' +import type { ResponsiveVisualizationData } from '@globalfishingwatch/responsive-visualizations' import { OTHER_CATEGORY_LABEL } from 'features/reports/vessel-groups/vessel-group-report.config' import { getSearchIdentityResolved, getVesselProperty } from 'features/vessel/vessel.utils' import { @@ -193,7 +194,7 @@ type GraphDataGroup = { value: number } -export const selectVGRVesselsGraphDataGrouped = createSelector( +export const selectVGRVesselsGraphAggregatedData = createSelector( [selectVGRVesselsFiltered, selectVGRVesselsSubsection], (vessels, subsection) => { if (!vessels) return [] @@ -254,7 +255,72 @@ export const selectVGRVesselsGraphDataGrouped = createSelector( name: OTHER_CATEGORY_LABEL, value: restOfGroups.reduce((acc, group) => acc + group.value, 0), }, - ] as GraphDataGroup[] + ] as ResponsiveVisualizationData<'aggregated'> + } +) + +export const selectVGRVesselsGraphIndividualData = createSelector( + [selectVGRVesselsFiltered, selectVGRVesselsSubsection], + (vessels, subsection) => { + if (!vessels) return [] + let vesselsGrouped = {} + switch (subsection) { + case 'flag': + vesselsGrouped = groupBy(vessels, (vessel) => vessel.flagTranslatedClean) + break + case 'shiptypes': + vesselsGrouped = groupBy(vessels, (vessel) => vessel.vesselType.split(', ')[0]) + break + case 'geartypes': + vesselsGrouped = groupBy(vessels, (vessel) => vessel.geartype.split(', ')[0]) + break + case 'source': + vesselsGrouped = groupBy(vessels, (vessel) => vessel.source) + } + const orderedGroups: ResponsiveVisualizationData<'individual'> = Object.entries(vesselsGrouped) + .map(([key, value]) => ({ + name: key, + values: value as any[], + })) + .sort((a, b) => { + return b.values.length - a.values.length + }) + const groupsWithoutOther: ResponsiveVisualizationData<'individual'> = [] + const otherGroups: ResponsiveVisualizationData<'individual'> = [] + orderedGroups.forEach((group) => { + if ( + group.name === 'null' || + group.name.toLowerCase() === OTHER_CATEGORY_LABEL.toLowerCase() || + group.name === EMPTY_FIELD_PLACEHOLDER + ) { + otherGroups.push(group) + } else { + groupsWithoutOther.push(group) + } + }) + const allGroups = + otherGroups.length > 0 + ? [ + ...groupsWithoutOther, + { + name: OTHER_CATEGORY_LABEL, + values: otherGroups, + }, + ] + : groupsWithoutOther + if (allGroups.length <= MAX_CATEGORIES) { + return allGroups as ResponsiveVisualizationData<'individual'> + } + const firstGroups = allGroups.slice(0, MAX_CATEGORIES) + const restOfGroups = allGroups.slice(MAX_CATEGORIES) + + return [ + ...firstGroups, + { + name: OTHER_CATEGORY_LABEL, + values: restOfGroups, + }, + ] as ResponsiveVisualizationData<'individual'> } ) diff --git a/apps/fishing-map/tsconfig.json b/apps/fishing-map/tsconfig.json index a21ad11b83..0229d060cd 100644 --- a/apps/fishing-map/tsconfig.json +++ b/apps/fishing-map/tsconfig.json @@ -31,6 +31,9 @@ "@globalfishingwatch/ocean-areas": ["../../libs/ocean-areas/src/index.ts"], "@globalfishingwatch/pbf-decoders": ["../../libs/pbf-decoders/index.ts"], "@globalfishingwatch/react-hooks": ["../../libs/react-hooks/src/index.ts"], + "@globalfishingwatch/responsive-visualizations": [ + "../../libs/responsive-visualizations/src/index.ts" + ], "@globalfishingwatch/timebar": ["../../libs/timebar/src/index.ts"], "@globalfishingwatch/ui-components": ["../../libs/ui-components/src/index.ts"] }, diff --git a/tsconfig.base.json b/tsconfig.base.json index 598bad7cc4..2fe81651c9 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -37,6 +37,9 @@ "@globalfishingwatch/pbf-decoders": ["libs/pbf-decoders/index.ts"], "@globalfishingwatch/react-hooks": ["libs/react-hooks/src/index.ts"], "@globalfishingwatch/react-hooks/*": ["libs/react-hooks/src/*"], + "@globalfishingwatch/responsive-visualizations": [ + "libs/responsive-visualizations/src/index.ts" + ], "@globalfishingwatch/timebar": ["libs/timebar/src/index.ts"], "@globalfishingwatch/ui-components": ["libs/ui-components/src/index.ts"], "@globalfishingwatch/ui-components/*": ["libs/ui-components/src/*"] From 669f7c74e637467a3720431bef132e2debe0391c Mon Sep 17 00:00:00 2001 From: j8seangel Date: Thu, 9 Jan 2025 12:19:12 +0100 Subject: [PATCH 03/62] individual tooltip component --- .../vessels/VesselGroupReportVesselsGraph.tsx | 41 +++++++++++++++---- .../src/charts/barchart/BarChart.tsx | 29 +++++++------ .../charts/barchart/BarChartAggregated.tsx | 10 +++-- .../barchart/BarChartIndividual.module.css | 11 +++++ .../charts/barchart/BarChartIndividual.tsx | 37 +++++++---------- 5 files changed, 81 insertions(+), 47 deletions(-) diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx index 41df26213f..87fe36d17b 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx @@ -1,14 +1,13 @@ import React, { Fragment, useCallback, useRef } from 'react' import cx from 'classnames' -import { BarChart, Bar, XAxis, Tooltip, ResponsiveContainer, LabelList } from 'recharts' import { useTranslation } from 'react-i18next' import type { CategoricalChartFunc } from 'recharts/types/chart/generateCategoricalChart' -import type { ReportEventsStatsResponseGroups } from 'queries/report-events-stats-api' import type { ResponsiveVisualizationData } from '@globalfishingwatch/responsive-visualizations' import { ResponsiveBarChart } from '@globalfishingwatch/responsive-visualizations' +import { VesselIdentitySourceEnum } from '@globalfishingwatch/api-types' import I18nNumber, { formatI18nNumber } from 'features/i18n/i18nNumber' import { EMPTY_API_VALUES, OTHERS_CATEGORY_LABEL } from 'features/reports/areas/area-reports.config' -import { formatInfoField, getVesselShipNameLabel } from 'utils/info' +import { formatInfoField, getVesselShipTypeLabel } from 'utils/info' import { useLocationConnect } from 'routes/routes.hook' import type { VesselGroupReportState, @@ -18,6 +17,8 @@ import type { import { COLOR_PRIMARY_BLUE } from 'features/app/app.config' import { OTHER_CATEGORY_LABEL } from 'features/reports/vessel-groups/vessel-group-report.config' import type { PortsReportState } from 'features/reports/ports/ports-report.types' +import type { ReportVesselWithDatasets } from 'features/reports/areas/area-reports.selectors' +import { getVesselProperty } from 'features/vessel/vessel.utils' import styles from './VesselGroupReportVesselsGraph.module.css' type ReportGraphTooltipProps = { @@ -43,7 +44,7 @@ const FILTER_PROPERTIES: Record = { source: 'source', } -const ReportGraphTooltip = (props: any) => { +const ReportBarTooltip = (props: any) => { const { active, payload, label, type } = props as ReportGraphTooltipProps const { t } = useTranslation() @@ -76,6 +77,26 @@ const ReportGraphTooltip = (props: any) => { return null } +const ReportPointTooltip = ({ type, data }: { type: string; data?: ReportVesselWithDatasets }) => { + const getVesselPropertyParams = { + identitySource: VesselIdentitySourceEnum.SelfReported, + } + const vesselName = formatInfoField( + getVesselProperty(data?.identity, 'shipname', getVesselPropertyParams), + 'shipname' + ) + + const vesselFlag = getVesselProperty(data?.identity, 'flag', getVesselPropertyParams) + + const vesselType = getVesselShipTypeLabel({ + shiptypes: getVesselProperty(data?.identity, 'shiptypes', getVesselPropertyParams), + }) + + return ( + {`${vesselName} ${vesselFlag ? `(${vesselFlag})` : ''} ${vesselType ? `- ${vesselType}` : ''}`} + ) +} + const ReportGraphTick = (props: any) => { const { x, y, payload, width, visibleTicksCount, property, filterQueryParam, pageQueryParam } = props @@ -191,23 +212,25 @@ export default function VesselGroupReportVesselsGraph({
    {data && data.length > 0 && ( { - return getVesselShipNameLabel(value.identity) + barValueFormatter={(value: any) => { + console.log('🚀 ~ value:', value) + return formatI18nNumber(value).toString() }} - color={color} - customTick={ + barLabel={ } - customTooltip={} + individualTooltip={} + aggregatedTooltip={} > )}
    diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx index b5533ccd4d..dff2aa2937 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx @@ -7,8 +7,8 @@ import { AggregatedBarChart } from './BarChartAggregated' export type BaseResponsiveBarChartProps = { color: string - customTick?: ReactElement - customTooltip?: ReactElement + barLabel?: ReactElement + barValueFormatter?: (value: any) => string valueFormatter?: (value: any) => string } @@ -16,10 +16,14 @@ export type ResponsiveBarChartInteractionCallback = (item: any) => void export type ResponsiveBarChartProps = BaseResponsiveBarChartProps & { containerRef: React.RefObject + // Aggregated props + aggregatedTooltip?: ReactElement onAggregatedItemClick?: ResponsiveBarChartInteractionCallback + getAggregatedData?: () => Promise> + // Individual props + individualTooltip?: ReactElement onIndividualItemClick?: ResponsiveBarChartInteractionCallback getIndividualData?: () => Promise> - getAggregatedData?: () => Promise> } export function ResponsiveBarChart({ @@ -27,9 +31,10 @@ export function ResponsiveBarChart({ getIndividualData, getAggregatedData, color, - customTick, - customTooltip, - valueFormatter, + barLabel, + aggregatedTooltip, + individualTooltip, + barValueFormatter, onIndividualItemClick, onAggregatedItemClick, }: ResponsiveBarChartProps) { @@ -118,18 +123,18 @@ export function ResponsiveBarChart({ data={data as ResponsiveVisualizationData<'individual'>} color={color} onClick={onIndividualItemClick} - customTick={customTick} - customTooltip={customTooltip} - valueFormatter={valueFormatter} + barLabel={barLabel} + customTooltip={individualTooltip} + barValueFormatter={barValueFormatter} /> ) : ( } color={color} onClick={onAggregatedItemClick} - customTick={customTick} - customTooltip={customTooltip} - valueFormatter={valueFormatter} + barLabel={barLabel} + customTooltip={aggregatedTooltip} + barValueFormatter={barValueFormatter} /> ) } diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx index fb7a67c9d2..7a619a7bb1 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx @@ -1,17 +1,19 @@ import { BarChart, Bar, XAxis, Tooltip, ResponsiveContainer, LabelList } from 'recharts' +import type { ReactElement } from 'react' import type { ResponsiveVisualizationData } from '../../types' import type { BaseResponsiveBarChartProps, ResponsiveBarChartInteractionCallback } from './BarChart' type AggregatedBarChartProps = BaseResponsiveBarChartProps & { data: ResponsiveVisualizationData<'aggregated'> onClick?: ResponsiveBarChartInteractionCallback + customTooltip?: ReactElement } export function AggregatedBarChart({ data, color, - customTick, + barLabel, customTooltip, - valueFormatter, + barValueFormatter, }: AggregatedBarChartProps) { return ( @@ -31,7 +33,7 @@ export function AggregatedBarChart({ valueFormatter?.(entry.value) || entry.value} + valueAccessor={(entry: any) => barValueFormatter?.(entry.value) || entry.value} /> diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.module.css b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.module.css index 6967e6ec59..d0c3f1eee4 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.module.css +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.module.css @@ -31,3 +31,14 @@ border-radius: 6px; position: relative; } + +.point:hover { + border: 1px solid black; +} + +.tooltip { + min-width: 200px; + padding: 10px; + background-color: white; + border-radius: 4px; +} diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx index 7659f1a43c..5635cfd4e3 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx @@ -1,17 +1,9 @@ -import { BarChart, Bar, XAxis, Tooltip, ResponsiveContainer, LabelList } from 'recharts' -import { - useFloating, - autoUpdate, - offset, - flip, - shift, - useInteractions, - useHover, - useTransitionStyles, -} from '@floating-ui/react' +import { BarChart, XAxis, ResponsiveContainer } from 'recharts' +import { useFloating, offset, flip, shift, useInteractions, useHover } from '@floating-ui/react' import type { ReactElement } from 'react' import cx from 'classnames' import { useState } from 'react' +import React from 'react' import type { ResponsiveVisualizationData } from '../../types' import type { BaseResponsiveBarChartProps, ResponsiveBarChartInteractionCallback } from './BarChart' import styles from './BarChartIndividual.module.css' @@ -20,20 +12,20 @@ type IndividualBarChartProps = BaseResponsiveBarChartProps & { width: number data: ResponsiveVisualizationData<'individual'> onClick?: ResponsiveBarChartInteractionCallback + customTooltip?: ReactElement } type IndividualBarChartPointProps = { color?: string point: IndividualBarChartProps['data'][0]['values'][0] - valueFormatter?: (value: any) => string tooltip?: ReactElement className?: string } + export function IndividualBarChartPoint({ point, color, tooltip, - valueFormatter, className, }: IndividualBarChartPointProps) { const [isOpen, setIsOpen] = useState(false) @@ -42,9 +34,9 @@ export function IndividualBarChartPoint({ open: isOpen, placement: 'top', onOpenChange: setIsOpen, - middleware: [offset(1), flip(), shift()], - whileElementsMounted: autoUpdate, + middleware: [offset(2), flip(), shift()], }) + const hover = useHover(context) const { getReferenceProps, getFloatingProps } = useInteractions([hover]) return ( @@ -61,7 +53,7 @@ export function IndividualBarChartPoint({ style={floatingStyles} {...getFloatingProps()} > - {tooltip ? tooltip : valueFormatter?.(point) || point.name} + {tooltip ? React.cloneElement(tooltip, { data: point } as any) : point.name}
    )} @@ -70,9 +62,9 @@ export function IndividualBarChartPoint({ export function IndividualBarChart({ data, color, - customTick, + barLabel, + barValueFormatter, customTooltip, - valueFormatter, }: IndividualBarChartProps) { return ( @@ -88,19 +80,20 @@ export function IndividualBarChart({ }} // onClick={onBarClick} > - {data && }
    {data.map((item, index) => (
    - +
      {item.values?.map((point, pointIndex) => ( ))}
    @@ -113,7 +106,7 @@ export function IndividualBarChart({ interval="equidistantPreserveStart" tickLine={false} minTickGap={-1000} - tick={customTick} + tick={barLabel} tickMargin={0} /> From 0b084b123ccc724cb49a0078521f4d3e56649bfd Mon Sep 17 00:00:00 2001 From: j8seangel Date: Thu, 9 Jan 2025 18:46:31 +0100 Subject: [PATCH 04/62] support dataKey and labelKey and improve typings --- .../vessels/VesselGroupReportVesselsGraph.tsx | 48 ++++++++------- .../src/charts/barchart/BarChart.tsx | 58 +++++++++++++++---- .../charts/barchart/BarChartAggregated.tsx | 23 ++++---- .../charts/barchart/BarChartIndividual.tsx | 54 ++++++++--------- libs/responsive-visualizations/src/types.ts | 26 +++++---- 5 files changed, 130 insertions(+), 79 deletions(-) diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx index 87fe36d17b..d0d5129c26 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx @@ -1,8 +1,10 @@ import React, { Fragment, useCallback, useRef } from 'react' import cx from 'classnames' import { useTranslation } from 'react-i18next' -import type { CategoricalChartFunc } from 'recharts/types/chart/generateCategoricalChart' -import type { ResponsiveVisualizationData } from '@globalfishingwatch/responsive-visualizations' +import type { + ResponsiveBarChartInteractionCallback, + ResponsiveVisualizationData, +} from '@globalfishingwatch/responsive-visualizations' import { ResponsiveBarChart } from '@globalfishingwatch/responsive-visualizations' import { VesselIdentitySourceEnum } from '@globalfishingwatch/api-types' import I18nNumber, { formatI18nNumber } from 'features/i18n/i18nNumber' @@ -17,9 +19,9 @@ import type { import { COLOR_PRIMARY_BLUE } from 'features/app/app.config' import { OTHER_CATEGORY_LABEL } from 'features/reports/vessel-groups/vessel-group-report.config' import type { PortsReportState } from 'features/reports/ports/ports-report.types' -import type { ReportVesselWithDatasets } from 'features/reports/areas/area-reports.selectors' import { getVesselProperty } from 'features/vessel/vessel.utils' import styles from './VesselGroupReportVesselsGraph.module.css' +import type { VesselGroupVesselTableParsed } from './vessel-group-report-vessels.selectors' type ReportGraphTooltipProps = { active: boolean @@ -77,19 +79,22 @@ const ReportBarTooltip = (props: any) => { return null } -const ReportPointTooltip = ({ type, data }: { type: string; data?: ReportVesselWithDatasets }) => { +const ReportPointTooltip = ({ data }: { type: string; data?: VesselGroupVesselTableParsed }) => { + if (!data?.identity) { + return null + } const getVesselPropertyParams = { identitySource: VesselIdentitySourceEnum.SelfReported, } const vesselName = formatInfoField( - getVesselProperty(data?.identity, 'shipname', getVesselPropertyParams), + getVesselProperty(data.identity, 'shipname', getVesselPropertyParams), 'shipname' ) - const vesselFlag = getVesselProperty(data?.identity, 'flag', getVesselPropertyParams) + const vesselFlag = getVesselProperty(data.identity, 'flag', getVesselPropertyParams) const vesselType = getVesselShipTypeLabel({ - shiptypes: getVesselProperty(data?.identity, 'shiptypes', getVesselPropertyParams), + shiptypes: getVesselProperty(data.identity, 'shiptypes', getVesselPropertyParams), }) return ( @@ -164,16 +169,9 @@ const ReportGraphTick = (props: any) => { ) } -export default function VesselGroupReportVesselsGraph({ - data, - individualData, - color = COLOR_PRIMARY_BLUE, - property, - filterQueryParam, - pageQueryParam, -}: { +type VesselGroupReportVesselsGraphProps = { data: ResponsiveVisualizationData<'aggregated'> - individualData: ResponsiveVisualizationData<'individual'> + individualData?: ResponsiveVisualizationData<'individual'> color?: string property: VGREventsVesselsProperty filterQueryParam: @@ -182,10 +180,20 @@ export default function VesselGroupReportVesselsGraph({ pageQueryParam: | keyof Pick | keyof Pick -}) { +} + +export default function VesselGroupReportVesselsGraph({ + data, + individualData, + color = COLOR_PRIMARY_BLUE, + property, + filterQueryParam, + pageQueryParam, +}: VesselGroupReportVesselsGraphProps) { const ref = useRef(null) const { dispatchQueryParams } = useLocationConnect() - const onBarClick: CategoricalChartFunc = (e) => { + + const onBarClick: ResponsiveBarChartInteractionCallback = (e) => { const { payload } = e.activePayload?.[0] || {} if (payload && payload?.name !== OTHER_CATEGORY_LABEL) { dispatchQueryParams({ @@ -196,13 +204,14 @@ export default function VesselGroupReportVesselsGraph({ }) } } - const onPointClick: CategoricalChartFunc = (e) => { + const onPointClick: ResponsiveBarChartInteractionCallback = (e) => { console.log('TODO', e) } const getAggregatedData = useCallback(async () => { return data }, [data]) + const getIndividualData = useCallback(async () => { return individualData }, [individualData]) @@ -219,7 +228,6 @@ export default function VesselGroupReportVesselsGraph({ onAggregatedItemClick={onBarClick} onIndividualItemClick={onPointClick} barValueFormatter={(value: any) => { - console.log('🚀 ~ value:', value) return formatI18nNumber(value).toString() }} barLabel={ diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx index dff2aa2937..0f1a7df0e1 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx @@ -1,29 +1,50 @@ import type { ReactElement } from 'react' import { useCallback, useEffect, useState } from 'react' -import type { ResponsiveVisualizationData } from '../../types' +import type { + ResponsiveVisualizationData, + ResponsiveVisualizationItem, + ResponsiveVisualizationMode, +} from '../../types' import { getIsIndividualBarChartSupported } from '../../lib/density' import { IndividualBarChart } from './BarChartIndividual' import { AggregatedBarChart } from './BarChartAggregated' +export const DEFAULT_LABEL_KEY = 'label' +export const DEFAULT_AGGREGATED_VALUE_KEY = 'value' +export const DEFAULT_INDIVIDUAL_VALUE_KEY = 'values' + export type BaseResponsiveBarChartProps = { color: string barLabel?: ReactElement barValueFormatter?: (value: any) => string - valueFormatter?: (value: any) => string } -export type ResponsiveBarChartInteractionCallback = (item: any) => void +export type ResponsiveBarChartInteractionCallback = ( + item: Item +) => void + +export type BarChartByTypeProps = + BaseResponsiveBarChartProps & { + labelKey: string + valueKey: string + data: ResponsiveVisualizationData + onClick?: ResponsiveBarChartInteractionCallback + customTooltip?: ReactElement + } export type ResponsiveBarChartProps = BaseResponsiveBarChartProps & { containerRef: React.RefObject + labelKey?: string // Aggregated props aggregatedTooltip?: ReactElement onAggregatedItemClick?: ResponsiveBarChartInteractionCallback - getAggregatedData?: () => Promise> + getAggregatedData?: () => Promise | undefined> + aggregatedValueKey?: string // Individual props individualTooltip?: ReactElement onIndividualItemClick?: ResponsiveBarChartInteractionCallback - getIndividualData?: () => Promise> + getIndividualData?: () => Promise | undefined> + individualValueKey?: string } export function ResponsiveBarChart({ @@ -31,6 +52,9 @@ export function ResponsiveBarChart({ getIndividualData, getAggregatedData, color, + aggregatedValueKey = DEFAULT_AGGREGATED_VALUE_KEY, + individualValueKey = DEFAULT_INDIVIDUAL_VALUE_KEY, + labelKey = DEFAULT_LABEL_KEY, barLabel, aggregatedTooltip, individualTooltip, @@ -65,6 +89,9 @@ export function ResponsiveBarChart({ async ({ width, height }: { width: number; height: number }) => { if (getAggregatedData) { const aggregatedData = await getAggregatedData() + if (!aggregatedData) { + return + } if ( getIndividualData && getIsIndividualBarChartSupported({ data: aggregatedData, width, height }) @@ -83,20 +110,26 @@ export function ResponsiveBarChart({ } } else if (getIndividualData) { const individualData = await getIndividualData() + if (!individualData) { + return + } if (getIsIndividualBarChartSupported({ data: individualData, width, height })) { setIsIndividualSupported(true) setData(individualData) } else { - const aggregatedData = individualData.map((item) => ({ - name: item.name, - value: item.values.length, - })) + const aggregatedData = individualData.map((item) => { + const value = item[aggregatedValueKey] as ResponsiveVisualizationItem[] + return { + [labelKey]: item[labelKey], + [aggregatedValueKey]: value.length, + } + }) setIsIndividualSupported(false) setData(aggregatedData) } } }, - [getAggregatedData, getIndividualData] + [getAggregatedData, getIndividualData, aggregatedValueKey, labelKey] ) useEffect(() => { @@ -119,9 +152,10 @@ export function ResponsiveBarChart({ return isIndividualSupported ? ( } color={color} + valueKey={individualValueKey} + labelKey={labelKey} onClick={onIndividualItemClick} barLabel={barLabel} customTooltip={individualTooltip} @@ -131,6 +165,8 @@ export function ResponsiveBarChart({ } color={color} + valueKey={aggregatedValueKey} + labelKey={labelKey} onClick={onAggregatedItemClick} barLabel={barLabel} customTooltip={aggregatedTooltip} diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx index 7a619a7bb1..d3d67dcdf8 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx @@ -1,17 +1,16 @@ import { BarChart, Bar, XAxis, Tooltip, ResponsiveContainer, LabelList } from 'recharts' -import type { ReactElement } from 'react' import type { ResponsiveVisualizationData } from '../../types' -import type { BaseResponsiveBarChartProps, ResponsiveBarChartInteractionCallback } from './BarChart' +import type { BarChartByTypeProps } from './BarChart' + +type AggregatedBarChartProps = BarChartByTypeProps<'aggregated'> -type AggregatedBarChartProps = BaseResponsiveBarChartProps & { - data: ResponsiveVisualizationData<'aggregated'> - onClick?: ResponsiveBarChartInteractionCallback - customTooltip?: ReactElement -} export function AggregatedBarChart({ data, color, barLabel, + valueKey, + labelKey, + onClick, customTooltip, barValueFormatter, }: AggregatedBarChartProps) { @@ -27,17 +26,19 @@ export function AggregatedBarChart({ left: 0, bottom: 0, }} - // onClick={onBarClick} + onClick={onClick} > {data && } - + barValueFormatter?.(entry.value) || entry.value} + valueAccessor={(entry: ResponsiveVisualizationData<'aggregated'>[0]) => + barValueFormatter?.(entry[valueKey]) || entry[valueKey] + } /> - onClick?: ResponsiveBarChartInteractionCallback - customTooltip?: ReactElement -} +type IndividualBarChartProps = BarChartByTypeProps<'individual'> type IndividualBarChartPointProps = { color?: string - point: IndividualBarChartProps['data'][0]['values'][0] + point: ResponsiveVisualizationItem tooltip?: ReactElement className?: string } @@ -63,6 +58,8 @@ export function IndividualBarChart({ data, color, barLabel, + valueKey, + labelKey, barValueFormatter, customTooltip, }: IndividualBarChartProps) { @@ -82,27 +79,30 @@ export function IndividualBarChart({ >
    - {data.map((item, index) => ( -
    - -
      - {item.values?.map((point, pointIndex) => ( - - ))} -
    -
    - ))} + {data.map((item, index) => { + const points = item?.[valueKey] as ResponsiveVisualizationItem[] + return ( +
    + +
      + {points?.map((point, pointIndex) => ( + + ))} +
    +
    + ) + })}
    +export type ResponsiveVisualizationAggregatedItem = { + label?: string + value?: number +} & Item + +export type ResponsiveVisualizationIndividualItem = { + label?: string + values?: ResponsiveVisualizationItem[] +} & Item export type ResponsiveVisualizationData< - Data extends ResponsiveVisualizationMode | undefined = undefined, -> = Data extends 'aggregated' - ? ResponsiveVisualizationAggregatedItem[] - : Data extends 'individual' - ? ResponsiveVisualizationIndividualItem[] + Mode extends ResponsiveVisualizationMode | undefined = undefined, + Data = ResponsiveVisualizationItem, +> = Mode extends 'aggregated' + ? ResponsiveVisualizationAggregatedItem[] + : Mode extends 'individual' + ? ResponsiveVisualizationIndividualItem[] : (ResponsiveVisualizationAggregatedItem | ResponsiveVisualizationIndividualItem)[] From 627b0f7aef3f73ff3bf05280a4c8e7b7f065dab2 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Thu, 9 Jan 2025 19:32:54 +0100 Subject: [PATCH 05/62] extract reusable logic from BarChart to hooks --- libs/responsive-visualizations/package.json | 2 + .../src/charts/barchart/BarChart.tsx | 133 +++--------------- .../charts/barchart/BarChartAggregated.tsx | 2 +- .../charts/barchart/BarChartIndividual.tsx | 2 +- .../src/charts/config.ts | 4 + .../src/charts/hooks.ts | 132 +++++++++++++++++ .../src/charts/types.ts | 59 ++++++++ .../src/lib/density.ts | 10 +- 8 files changed, 226 insertions(+), 118 deletions(-) create mode 100644 libs/responsive-visualizations/src/charts/config.ts create mode 100644 libs/responsive-visualizations/src/charts/hooks.ts create mode 100644 libs/responsive-visualizations/src/charts/types.ts diff --git a/libs/responsive-visualizations/package.json b/libs/responsive-visualizations/package.json index 380503108d..3025eb009e 100644 --- a/libs/responsive-visualizations/package.json +++ b/libs/responsive-visualizations/package.json @@ -14,6 +14,8 @@ "module": "./index.js", "dependencies": { "classnames": "2.x", + "lodash": "4.x", + "luxon": "3.x", "react-dom": "18.x", "react": "18.x", "recharts": "2.x" diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx index 0f1a7df0e1..4cb353f06a 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx @@ -1,51 +1,18 @@ -import type { ReactElement } from 'react' -import { useCallback, useEffect, useState } from 'react' -import type { - ResponsiveVisualizationData, - ResponsiveVisualizationItem, - ResponsiveVisualizationMode, -} from '../../types' +import { useEffect } from 'react' +import type { ResponsiveVisualizationData } from '../../types' import { getIsIndividualBarChartSupported } from '../../lib/density' +import type { BaseResponsiveBarChartProps, BaseResponsiveChartProps } from '../types' +import { + DEFAULT_AGGREGATED_VALUE_KEY, + DEFAULT_INDIVIDUAL_VALUE_KEY, + DEFAULT_LABEL_KEY, +} from '../config' +import { useResponsiveDimensions, useResponsiveVisualizationData } from '../hooks' import { IndividualBarChart } from './BarChartIndividual' import { AggregatedBarChart } from './BarChartAggregated' -export const DEFAULT_LABEL_KEY = 'label' -export const DEFAULT_AGGREGATED_VALUE_KEY = 'value' -export const DEFAULT_INDIVIDUAL_VALUE_KEY = 'values' - -export type BaseResponsiveBarChartProps = { - color: string - barLabel?: ReactElement - barValueFormatter?: (value: any) => string -} - -export type ResponsiveBarChartInteractionCallback = ( - item: Item -) => void - -export type BarChartByTypeProps = - BaseResponsiveBarChartProps & { - labelKey: string - valueKey: string - data: ResponsiveVisualizationData - onClick?: ResponsiveBarChartInteractionCallback - customTooltip?: ReactElement - } - -export type ResponsiveBarChartProps = BaseResponsiveBarChartProps & { - containerRef: React.RefObject - labelKey?: string - // Aggregated props - aggregatedTooltip?: ReactElement - onAggregatedItemClick?: ResponsiveBarChartInteractionCallback - getAggregatedData?: () => Promise | undefined> - aggregatedValueKey?: string - // Individual props - individualTooltip?: ReactElement - onIndividualItemClick?: ResponsiveBarChartInteractionCallback - getIndividualData?: () => Promise | undefined> - individualValueKey?: string -} +type ResponsiveBarChartProps = BaseResponsiveChartProps & + BaseResponsiveBarChartProps & { labelKey?: string } export function ResponsiveBarChart({ containerRef, @@ -62,75 +29,15 @@ export function ResponsiveBarChart({ onIndividualItemClick, onAggregatedItemClick, }: ResponsiveBarChartProps) { - const [data, setData] = useState(null) - const [isIndividualSupported, setIsIndividualSupported] = useState(false) - const [{ width, height }, setDimensions] = useState({ width: 0, height: 0 }) - - useEffect(() => { - const resizeObserver = new ResizeObserver(() => { - if (containerRef.current) { - const { width, height } = containerRef.current.getBoundingClientRect() - setDimensions({ width, height }) - } - }) - - if (containerRef.current) { - resizeObserver.observe(containerRef.current) - } - - return () => { - if (containerRef.current) { - resizeObserver.unobserve(containerRef.current) - } - } - }, [containerRef]) - - const loadData = useCallback( - async ({ width, height }: { width: number; height: number }) => { - if (getAggregatedData) { - const aggregatedData = await getAggregatedData() - if (!aggregatedData) { - return - } - if ( - getIndividualData && - getIsIndividualBarChartSupported({ data: aggregatedData, width, height }) - ) { - const individualData = await getIndividualData() - if (individualData) { - setIsIndividualSupported(true) - setData(individualData) - } else { - setIsIndividualSupported(false) - setData(aggregatedData) - } - } else { - setIsIndividualSupported(false) - setData(aggregatedData) - } - } else if (getIndividualData) { - const individualData = await getIndividualData() - if (!individualData) { - return - } - if (getIsIndividualBarChartSupported({ data: individualData, width, height })) { - setIsIndividualSupported(true) - setData(individualData) - } else { - const aggregatedData = individualData.map((item) => { - const value = item[aggregatedValueKey] as ResponsiveVisualizationItem[] - return { - [labelKey]: item[labelKey], - [aggregatedValueKey]: value.length, - } - }) - setIsIndividualSupported(false) - setData(aggregatedData) - } - } - }, - [getAggregatedData, getIndividualData, aggregatedValueKey, labelKey] - ) + const { width, height } = useResponsiveDimensions(containerRef) + const { data, isIndividualSupported, loadData } = useResponsiveVisualizationData({ + labelKey, + aggregatedValueKey, + individualValueKey, + getAggregatedData, + getIndividualData, + getIsIndividualSupported: getIsIndividualBarChartSupported, + }) useEffect(() => { if (width && height) { diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx index d3d67dcdf8..c29fbe2448 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx @@ -1,6 +1,6 @@ import { BarChart, Bar, XAxis, Tooltip, ResponsiveContainer, LabelList } from 'recharts' import type { ResponsiveVisualizationData } from '../../types' -import type { BarChartByTypeProps } from './BarChart' +import type { BarChartByTypeProps } from '../types' type AggregatedBarChartProps = BarChartByTypeProps<'aggregated'> diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx index 7287ce1fe6..b5741443be 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx @@ -5,7 +5,7 @@ import cx from 'classnames' import { useState } from 'react' import React from 'react' import type { ResponsiveVisualizationItem } from '../../types' -import type { BarChartByTypeProps } from './BarChart' +import type { BarChartByTypeProps } from '../types' import styles from './BarChartIndividual.module.css' type IndividualBarChartProps = BarChartByTypeProps<'individual'> diff --git a/libs/responsive-visualizations/src/charts/config.ts b/libs/responsive-visualizations/src/charts/config.ts new file mode 100644 index 0000000000..1c50292826 --- /dev/null +++ b/libs/responsive-visualizations/src/charts/config.ts @@ -0,0 +1,4 @@ +export const DEFAULT_LABEL_KEY = 'label' +export const DEFAULT_DATE_KEY = 'date' +export const DEFAULT_AGGREGATED_VALUE_KEY = 'value' +export const DEFAULT_INDIVIDUAL_VALUE_KEY = 'values' diff --git a/libs/responsive-visualizations/src/charts/hooks.ts b/libs/responsive-visualizations/src/charts/hooks.ts new file mode 100644 index 0000000000..91d6def378 --- /dev/null +++ b/libs/responsive-visualizations/src/charts/hooks.ts @@ -0,0 +1,132 @@ +import { useState, useEffect, useCallback, useMemo } from 'react' +import type { ResponsiveVisualizationData, ResponsiveVisualizationItem } from '../types' +import type { + getIsIndividualBarChartSupported, + getIsIndividualTimeseriesSupported, +} from '../lib/density' +import type { BaseResponsiveChartProps, ResponsiveVisualizationContainerRef } from './types' +import { + DEFAULT_AGGREGATED_VALUE_KEY, + DEFAULT_INDIVIDUAL_VALUE_KEY, + DEFAULT_LABEL_KEY, +} from './config' + +export function useResponsiveDimensions(containerRef: ResponsiveVisualizationContainerRef) { + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }) + + useEffect(() => { + const resizeObserver = new ResizeObserver(() => { + if (containerRef.current) { + const { width, height } = containerRef.current.getBoundingClientRect() + setDimensions({ width, height }) + } + }) + + if (containerRef.current) { + resizeObserver.observe(containerRef.current) + } + + return () => { + if (containerRef.current) { + resizeObserver.unobserve(containerRef.current) + } + } + }, [containerRef]) + + return dimensions +} + +type UseResponsiveLoadDataProps = { + labelKey: string + individualValueKey: BaseResponsiveChartProps['individualValueKey'] + aggregatedValueKey: BaseResponsiveChartProps['aggregatedValueKey'] + getAggregatedData?: BaseResponsiveChartProps['getAggregatedData'] + getIndividualData?: BaseResponsiveChartProps['getAggregatedData'] + getIsIndividualSupported: + | typeof getIsIndividualBarChartSupported + | typeof getIsIndividualTimeseriesSupported +} +export function useResponsiveVisualizationData({ + labelKey = DEFAULT_LABEL_KEY, + individualValueKey = DEFAULT_INDIVIDUAL_VALUE_KEY, + aggregatedValueKey = DEFAULT_AGGREGATED_VALUE_KEY, + getAggregatedData, + getIndividualData, + getIsIndividualSupported, +}: UseResponsiveLoadDataProps) { + const [data, setData] = useState(null) + const [isIndividualSupported, setIsIndividualSupported] = useState(false) + + const loadData = useCallback( + async ({ width, height }: { width: number; height: number }) => { + if (getAggregatedData) { + const aggregatedData = await getAggregatedData() + if (!aggregatedData) { + return + } + if ( + getIndividualData && + getIsIndividualSupported({ + data: aggregatedData, + width, + height, + individualValueKey, + aggregatedValueKey, + }) + ) { + const individualData = await getIndividualData() + if (individualData) { + setIsIndividualSupported(true) + setData(individualData) + } else { + setIsIndividualSupported(false) + setData(aggregatedData) + } + } else { + setIsIndividualSupported(false) + setData(aggregatedData) + } + } else if (getIndividualData) { + const individualData = await getIndividualData() + if (!individualData) { + return + } + if ( + getIsIndividualSupported({ + data: individualData, + width, + height, + individualValueKey, + aggregatedValueKey, + }) + ) { + setIsIndividualSupported(true) + setData(individualData) + } else { + const aggregatedData = individualData.map((item) => { + const value = item[individualValueKey] as ResponsiveVisualizationItem[] + return { + [labelKey]: item[labelKey], + [individualValueKey]: value.length, + } + }) + setIsIndividualSupported(false) + setData(aggregatedData) + } + } + }, + [ + getAggregatedData, + getIndividualData, + getIsIndividualSupported, + individualValueKey, + aggregatedValueKey, + labelKey, + ] + ) + + return useMemo( + () => ({ data, isIndividualSupported, loadData }), + [data, isIndividualSupported, loadData] + ) +} diff --git a/libs/responsive-visualizations/src/charts/types.ts b/libs/responsive-visualizations/src/charts/types.ts new file mode 100644 index 0000000000..726283297d --- /dev/null +++ b/libs/responsive-visualizations/src/charts/types.ts @@ -0,0 +1,59 @@ +import type { ReactElement } from 'react' +import type { + ResponsiveVisualizationData, + ResponsiveVisualizationItem, + ResponsiveVisualizationMode, +} from '../types' + +// Shared types between all charts +export type ResponsiveVisualizationInteractionCallback = ( + item: Item +) => void + +export type ResponsiveVisualizationContainerRef = React.RefObject +export type BaseResponsiveChartProps = { + containerRef: ResponsiveVisualizationContainerRef + // Aggregated props + aggregatedTooltip?: ReactElement + onAggregatedItemClick?: ResponsiveVisualizationInteractionCallback + getAggregatedData?: () => Promise | undefined> + aggregatedValueKey?: string + // Individual props + individualTooltip?: ReactElement + onIndividualItemClick?: ResponsiveVisualizationInteractionCallback + getIndividualData?: () => Promise | undefined> + individualValueKey?: string +} + +// Shared types within the BarChart +export type BaseResponsiveBarChartProps = { + color: string + barLabel?: ReactElement + barValueFormatter?: (value: any) => string +} + +export type BarChartByTypeProps = + BaseResponsiveBarChartProps & { + labelKey: string + valueKey: string + data: ResponsiveVisualizationData + onClick?: ResponsiveVisualizationInteractionCallback + customTooltip?: ReactElement + } + +// Shared types within the Timeseries +export type BaseResponsiveTimeseriesProps = { + start: string + end: string + color: string + tickLabel?: ReactElement +} + +export type TimeseriesByTypeProps = + BaseResponsiveTimeseriesProps & { + dateKey: string + valueKey: string + data: ResponsiveVisualizationData + onClick?: ResponsiveVisualizationInteractionCallback + customTooltip?: ReactElement + } diff --git a/libs/responsive-visualizations/src/lib/density.ts b/libs/responsive-visualizations/src/lib/density.ts index 5a926bab78..1628428c33 100644 --- a/libs/responsive-visualizations/src/lib/density.ts +++ b/libs/responsive-visualizations/src/lib/density.ts @@ -25,17 +25,21 @@ type IsIndividualSupportedParams = { data: ResponsiveVisualizationData width: number height: number + aggregatedValueKey: string + individualValueKey: string } export function getIsIndividualBarChartSupported({ data, width, height, + aggregatedValueKey, + individualValueKey, }: IsIndividualSupportedParams): boolean { const { pointsByRow } = getBarProps(data, width) const biggestColumnValue = data.reduce((acc, column) => { - const value = (column as ResponsiveVisualizationIndividualItem).values - ? (column as ResponsiveVisualizationIndividualItem).values.length - : (column as ResponsiveVisualizationAggregatedItem).value + const value = (column as ResponsiveVisualizationIndividualItem)[aggregatedValueKey] + ? (column as ResponsiveVisualizationIndividualItem)[aggregatedValueKey].length + : (column as ResponsiveVisualizationAggregatedItem)[individualValueKey].value if (value > acc) { return value } From 895912e2ef08ad9f9b5385c484eac83a8ddeb2bf Mon Sep 17 00:00:00 2001 From: j8seangel Date: Thu, 9 Jan 2025 19:46:20 +0100 Subject: [PATCH 06/62] wip: timeseries aggregated graph --- .../shared/events/EventsReportGraph.tsx | 115 +++--------------- .../vessels/VesselGroupReportVesselsGraph.tsx | 6 +- .../src/charts/index.ts | 2 + .../src/charts/timeseries/Timeseries.tsx | 88 ++++++++++++++ .../timeseries/TimeseriesAggregated.tsx | 103 ++++++++++++++++ .../TimeseriesIndividual.module.css | 0 .../timeseries/TimeseriesIndividual.tsx | 8 ++ .../src/charts/types.ts | 4 +- .../src/lib/density.ts | 17 +++ 9 files changed, 239 insertions(+), 104 deletions(-) create mode 100644 libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx create mode 100644 libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx create mode 100644 libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.module.css create mode 100644 libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx diff --git a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx index d8173bb1c8..360778a25a 100644 --- a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx +++ b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx @@ -1,24 +1,10 @@ -import React, { useMemo } from 'react' -import { - ResponsiveContainer, - CartesianGrid, - XAxis, - YAxis, - Tooltip, - Line, - ComposedChart, -} from 'recharts' -import min from 'lodash/min' -import max from 'lodash/max' -import type { DurationUnit } from 'luxon' -import { DateTime, Duration } from 'luxon' +import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' import type { FourwingsInterval } from '@globalfishingwatch/deck-loaders' -import { getFourwingsInterval } from '@globalfishingwatch/deck-loaders' +import { ResponsiveTimeseries } from '@globalfishingwatch/responsive-visualizations' import i18n from 'features/i18n/i18n' import { formatDateForInterval, getUTCDateTime } from 'utils/dates' import { formatI18nNumber } from 'features/i18n/i18nNumber' -import { tickFormatter } from 'features/reports/areas/area-reports.utils' import { COLOR_PRIMARY_BLUE } from 'features/app/app.config' import styles from './EventsReportGraph.module.css' @@ -56,13 +42,12 @@ const ReportGraphTooltip = (props: any) => { return null } -const formatDateTicks = (tick: string, timeChunkInterval: FourwingsInterval) => { +// TODO: REVIEW HOW TO HANDLE INTERVALS IN THIS COMPONENT +const formatDateTicks = (tick: string, timeChunkInterval: FourwingsInterval = 'DAY') => { const date = getUTCDateTime(tick).setLocale(i18n.language) return formatDateForInterval(date, timeChunkInterval) } -const graphMargin = { top: 0, right: 0, left: -20, bottom: -10 } - export default function EventsReportGraph({ color = COLOR_PRIMARY_BLUE, end, @@ -74,93 +59,25 @@ export default function EventsReportGraph({ start: string timeseries: { date: string; value: number }[] }) { + const containerRef = React.useRef(null) const { t } = useTranslation() - const startMillis = DateTime.fromISO(start).toMillis() - const endMillis = DateTime.fromISO(end).toMillis() - const interval = getFourwingsInterval(startMillis, endMillis) - - const domain = useMemo(() => { - if (start && end && interval) { - const cleanEnd = DateTime.fromISO(end, { zone: 'utc' }) - .minus({ [interval]: 1 }) - .toISO() as string - return [new Date(start).getTime(), new Date(cleanEnd).getTime()] - } - }, [start, end, interval]) - const dataMin: number = timeseries.length - ? (min(timeseries.map(({ value }: { value: number }) => value)) as number) - : 0 - const dataMax: number = timeseries.length - ? (max(timeseries.map(({ value }: { value: number }) => value)) as number) - : 0 - - const domainPadding = (dataMax - dataMin) / 8 - const paddedDomain: [number, number] = [ - Math.max(0, Math.floor(dataMin - domainPadding)), - Math.ceil(dataMax + domainPadding), - ] - const intervalDiff = Math.floor( - Duration.fromMillis(endMillis - startMillis).as(interval.toLowerCase() as DurationUnit) - ) - const fullTimeseries = useMemo(() => { - if (!timeseries || !domain) { - return [] - } - return Array(intervalDiff) - .fill(0) - .map((_, i) => i) - .map((i) => { - const d = DateTime.fromMillis(startMillis, { zone: 'UTC' }) - .plus({ [interval]: i }) - .toISO() - return { - date: d, - value: timeseries.find(({ date }: { date: string }) => date === d)?.value || 0, - } - }) - }, [timeseries, domain, intervalDiff, startMillis, interval]) + const getAggregatedData = useCallback(async () => timeseries, [timeseries]) - if (!fullTimeseries.length || !domain) { + if (!timeseries.length) { return null } return ( -
    - - - - formatDateTicks(tick, interval)} - axisLine={paddedDomain[0] === 0} - /> - - {timeseries?.length && ( - } /> - )} - - - +
    +
    ) } diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx index d0d5129c26..4636697573 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx @@ -2,8 +2,8 @@ import React, { Fragment, useCallback, useRef } from 'react' import cx from 'classnames' import { useTranslation } from 'react-i18next' import type { - ResponsiveBarChartInteractionCallback, ResponsiveVisualizationData, + ResponsiveVisualizationInteractionCallback, } from '@globalfishingwatch/responsive-visualizations' import { ResponsiveBarChart } from '@globalfishingwatch/responsive-visualizations' import { VesselIdentitySourceEnum } from '@globalfishingwatch/api-types' @@ -193,7 +193,7 @@ export default function VesselGroupReportVesselsGraph({ const ref = useRef(null) const { dispatchQueryParams } = useLocationConnect() - const onBarClick: ResponsiveBarChartInteractionCallback = (e) => { + const onBarClick: ResponsiveVisualizationInteractionCallback = (e) => { const { payload } = e.activePayload?.[0] || {} if (payload && payload?.name !== OTHER_CATEGORY_LABEL) { dispatchQueryParams({ @@ -204,7 +204,7 @@ export default function VesselGroupReportVesselsGraph({ }) } } - const onPointClick: ResponsiveBarChartInteractionCallback = (e) => { + const onPointClick: ResponsiveVisualizationInteractionCallback = (e) => { console.log('TODO', e) } diff --git a/libs/responsive-visualizations/src/charts/index.ts b/libs/responsive-visualizations/src/charts/index.ts index 51f0691d1c..cc348e5b92 100644 --- a/libs/responsive-visualizations/src/charts/index.ts +++ b/libs/responsive-visualizations/src/charts/index.ts @@ -1 +1,3 @@ export * from './barchart/BarChart' +export * from './timeseries/Timeseries' +export * from './types' diff --git a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx new file mode 100644 index 0000000000..51499a97c7 --- /dev/null +++ b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx @@ -0,0 +1,88 @@ +import { useEffect } from 'react' +import type { ResponsiveVisualizationData } from '../../types' +import { getIsIndividualTimeseriesSupported } from '../../lib/density' +import type { BaseResponsiveChartProps, BaseResponsiveTimeseriesProps } from '../types' +import { useResponsiveDimensions, useResponsiveVisualizationData } from '../hooks' +import { + DEFAULT_AGGREGATED_VALUE_KEY, + DEFAULT_INDIVIDUAL_VALUE_KEY, + DEFAULT_DATE_KEY, +} from '../config' +import { IndividualTimeseries } from './TimeseriesIndividual' +import { AggregatedTimeseries } from './TimeseriesAggregated' + +type ResponsiveTimeseriesProps = BaseResponsiveChartProps & + BaseResponsiveTimeseriesProps & { dateKey?: string } + +export function ResponsiveTimeseries({ + start, + end, + dateKey = DEFAULT_DATE_KEY, + aggregatedValueKey = DEFAULT_AGGREGATED_VALUE_KEY, + individualValueKey = DEFAULT_INDIVIDUAL_VALUE_KEY, + containerRef, + getIndividualData, + getAggregatedData, + color, + tickLabelFormatter, + aggregatedTooltip, + individualTooltip, + onIndividualItemClick, + onAggregatedItemClick, +}: ResponsiveTimeseriesProps) { + const { width, height } = useResponsiveDimensions(containerRef) + const { data, isIndividualSupported, loadData } = useResponsiveVisualizationData({ + labelKey: dateKey, + individualValueKey, + aggregatedValueKey, + getAggregatedData, + getIndividualData, + getIsIndividualSupported: getIsIndividualTimeseriesSupported, + }) + + useEffect(() => { + if (width && height) { + console.log('loading data') + loadData({ width, height }) + } + }, [height, width, loadData]) + + if (!getAggregatedData && !getIndividualData) { + console.warn('No data getters functions provided') + return null + } + + if (!data) { + return 'Spinner' + } + if (isIndividualSupported && !data) { + return 'Spinner for individual' + } + + return isIndividualSupported ? ( + } + start={start} + end={end} + color={color} + dateKey={dateKey} + valueKey={individualValueKey} + onClick={onIndividualItemClick} + tickLabelFormatter={tickLabelFormatter} + customTooltip={individualTooltip} + /> + ) : ( + } + start={start} + end={end} + color={color} + dateKey={dateKey} + valueKey={aggregatedValueKey} + onClick={onAggregatedItemClick} + tickLabelFormatter={tickLabelFormatter} + customTooltip={aggregatedTooltip} + /> + ) +} diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx new file mode 100644 index 0000000000..69efaa06ad --- /dev/null +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx @@ -0,0 +1,103 @@ +import { XAxis, ResponsiveContainer, YAxis, CartesianGrid, ComposedChart, Line } from 'recharts' +import min from 'lodash/min' +import max from 'lodash/max' +import type { DurationUnit } from 'luxon' +import { DateTime, Duration } from 'luxon' +import { useMemo } from 'react' +import type { TimeseriesByTypeProps } from '../types' + +const graphMargin = { top: 0, right: 0, left: -20, bottom: -10 } + +type AggregatedTimeseriesProps = TimeseriesByTypeProps<'aggregated'> +export function AggregatedTimeseries({ + data, + color, + start, + end, + dateKey, + valueKey, + tickLabelFormatter, +}: AggregatedTimeseriesProps) { + const startMillis = DateTime.fromISO(start).toMillis() + const endMillis = DateTime.fromISO(end).toMillis() + // TODO: REVIEW HOW TO HANDLE INTERVALS IN THIS COMPONENT + // const interval = getFourwingsInterval(startMillis, endMillis) + const interval = 'DAY' as DurationUnit + + const intervalDiff = Math.floor( + Duration.fromMillis(endMillis - startMillis).as(interval.toLowerCase() as DurationUnit) + ) + + const dataMin: number = data.length ? (min(data.map((item) => item[valueKey])) as number) : 0 + const dataMax: number = data.length ? (max(data.map((item) => item[valueKey])) as number) : 0 + + const domainPadding = (dataMax - dataMin) / 8 + const paddedDomain: [number, number] = [ + Math.max(0, Math.floor(dataMin - domainPadding)), + Math.ceil(dataMax + domainPadding), + ] + + const domain = useMemo(() => { + if (start && end && interval) { + const cleanEnd = DateTime.fromISO(end, { zone: 'utc' }) + .minus({ [interval]: 1 }) + .toISO() as string + return [new Date(start).getTime(), new Date(cleanEnd).getTime()] + } + }, [start, end, interval]) + + const fullTimeseries = useMemo(() => { + if (!data || !domain) { + return [] + } + return Array(intervalDiff) + .fill(0) + .map((_, i) => i) + .map((i) => { + const d = DateTime.fromMillis(startMillis, { zone: 'UTC' }) + .plus({ [interval]: i }) + .toISO() + return { + date: d, + value: data.find((item) => item[dateKey] === d)?.value || 0, + } + }) + }, [data, domain, intervalDiff, startMillis, dateKey]) + + return ( + + + + tickLabelFormatter?.(tick) || tick} + axisLine={paddedDomain[0] === 0} + /> + + {/* {fullTimeseries?.length && ( + } /> + )} */} + + + + ) +} diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.module.css b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.module.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx new file mode 100644 index 0000000000..910f1dc88a --- /dev/null +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx @@ -0,0 +1,8 @@ +import type { TimeseriesByTypeProps } from '../types' + +type IndividualTimeseriesProps = TimeseriesByTypeProps<'individual'> & { width: number } + +export function IndividualTimeseries({ data }: IndividualTimeseriesProps) { + console.log('🚀 ~ IndividualTimeseries ~ data:', data) + return

    TODO

    +} diff --git a/libs/responsive-visualizations/src/charts/types.ts b/libs/responsive-visualizations/src/charts/types.ts index 726283297d..b047ec18ba 100644 --- a/libs/responsive-visualizations/src/charts/types.ts +++ b/libs/responsive-visualizations/src/charts/types.ts @@ -29,7 +29,7 @@ export type BaseResponsiveChartProps = { export type BaseResponsiveBarChartProps = { color: string barLabel?: ReactElement - barValueFormatter?: (value: any) => string + barValueFormatter?: (value: number) => string } export type BarChartByTypeProps = @@ -46,7 +46,7 @@ export type BaseResponsiveTimeseriesProps = { start: string end: string color: string - tickLabel?: ReactElement + tickLabelFormatter?: (item: string) => string } export type TimeseriesByTypeProps = diff --git a/libs/responsive-visualizations/src/lib/density.ts b/libs/responsive-visualizations/src/lib/density.ts index 1628428c33..03d6ebf2ed 100644 --- a/libs/responsive-visualizations/src/lib/density.ts +++ b/libs/responsive-visualizations/src/lib/density.ts @@ -49,3 +49,20 @@ export function getIsIndividualBarChartSupported({ const heightNeeded = rowsInBiggestColumn * POINT_WIDTH return heightNeeded < height - AXIX_LABEL_PADDING - COLUMN_PADDING - COLUMN_LABEL_SIZE } + +type getIsIndividualTimeseriesSupportedParams = { + data: ResponsiveVisualizationData + width: number + height: number + aggregatedValueKey: string + individualValueKey: string +} +export function getIsIndividualTimeseriesSupported({ + data, + width, + height, + aggregatedValueKey, + individualValueKey, +}: getIsIndividualTimeseriesSupportedParams): boolean { + return false +} From a0fbe6265e0d451bf51b669779ef12c310280b63 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Fri, 10 Jan 2025 14:32:50 +0100 Subject: [PATCH 07/62] individual timeseries points --- .../shared/events/EventsReportGraph.tsx | 25 +++- .../src/charts/barchart/BarChart.tsx | 13 +- .../barchart/BarChartIndividual.module.css | 20 --- .../charts/barchart/BarChartIndividual.tsx | 51 +------- .../src/charts/hooks.ts | 38 +++++- .../src/charts/points/IndividualPoint.tsx | 45 +++++++ .../charts/points/IndividualPoints.module.css | 19 +++ .../src/charts/timeseries/Timeseries.tsx | 18 +-- .../timeseries/TimeseriesAggregated.tsx | 13 +- .../TimeseriesIndividual.module.css | 24 ++++ .../timeseries/TimeseriesIndividual.tsx | 121 +++++++++++++++++- .../src/charts/types.ts | 17 +-- .../src/lib/density.ts | 26 +++- libs/responsive-visualizations/src/types.ts | 35 +++-- 14 files changed, 332 insertions(+), 133 deletions(-) create mode 100644 libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx create mode 100644 libs/responsive-visualizations/src/charts/points/IndividualPoints.module.css diff --git a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx index 360778a25a..daa1756a10 100644 --- a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx +++ b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx @@ -1,6 +1,7 @@ import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import type { FourwingsInterval } from '@globalfishingwatch/deck-loaders' +import { type FourwingsInterval } from '@globalfishingwatch/deck-loaders' +import type { BaseResponsiveTimeseriesProps } from '@globalfishingwatch/responsive-visualizations' import { ResponsiveTimeseries } from '@globalfishingwatch/responsive-visualizations' import i18n from 'features/i18n/i18n' import { formatDateForInterval, getUTCDateTime } from 'utils/dates' @@ -23,7 +24,7 @@ type EventsReportGraphTooltipProps = { timeChunkInterval: FourwingsInterval } -const ReportGraphTooltip = (props: any) => { +const AggregatedGraphTooltip = (props: any) => { const { active, payload, label, timeChunkInterval } = props as EventsReportGraphTooltipProps if (active && payload && payload.length) { @@ -42,8 +43,14 @@ const ReportGraphTooltip = (props: any) => { return null } -// TODO: REVIEW HOW TO HANDLE INTERVALS IN THIS COMPONENT -const formatDateTicks = (tick: string, timeChunkInterval: FourwingsInterval = 'DAY') => { +const IndividualGraphTooltip = ({ data }: { data?: any }) => { + return data.value +} + +const formatDateTicks: BaseResponsiveTimeseriesProps['tickLabelFormatter'] = ( + tick, + timeChunkInterval +) => { const date = getUTCDateTime(tick).setLocale(i18n.language) return formatDateForInterval(date, timeChunkInterval) } @@ -63,19 +70,29 @@ export default function EventsReportGraph({ const { t } = useTranslation() const getAggregatedData = useCallback(async () => timeseries, [timeseries]) + const getIndividualData = useCallback(async () => { + return timeseries.map((t) => ({ + date: t.date, + values: [...new Array(t.value)].map((v, i) => ({ label: i, value: i })), + })) + }, [timeseries]) if (!timeseries.length) { return null } return ( + // TODO: remove this ref and move it inside
    } + individualTooltip={} color={color} />
    diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx index 4cb353f06a..533c84b192 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx @@ -1,4 +1,3 @@ -import { useEffect } from 'react' import type { ResponsiveVisualizationData } from '../../types' import { getIsIndividualBarChartSupported } from '../../lib/density' import type { BaseResponsiveBarChartProps, BaseResponsiveChartProps } from '../types' @@ -7,7 +6,7 @@ import { DEFAULT_INDIVIDUAL_VALUE_KEY, DEFAULT_LABEL_KEY, } from '../config' -import { useResponsiveDimensions, useResponsiveVisualizationData } from '../hooks' +import { useResponsiveVisualization } from '../hooks' import { IndividualBarChart } from './BarChartIndividual' import { AggregatedBarChart } from './BarChartAggregated' @@ -29,8 +28,8 @@ export function ResponsiveBarChart({ onIndividualItemClick, onAggregatedItemClick, }: ResponsiveBarChartProps) { - const { width, height } = useResponsiveDimensions(containerRef) - const { data, isIndividualSupported, loadData } = useResponsiveVisualizationData({ + const { data, isIndividualSupported } = useResponsiveVisualization({ + containerRef, labelKey, aggregatedValueKey, individualValueKey, @@ -39,12 +38,6 @@ export function ResponsiveBarChart({ getIsIndividualSupported: getIsIndividualBarChartSupported, }) - useEffect(() => { - if (width && height) { - loadData({ width, height }) - } - }, [height, width, loadData]) - if (!getAggregatedData && !getIndividualData) { console.warn('No data getters functions provided') return null diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.module.css b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.module.css index d0c3f1eee4..ffbd917ee7 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.module.css +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.module.css @@ -22,23 +22,3 @@ flex-wrap: wrap-reverse; justify-content: center; } - -.point { - display: block; - width: 12px; - height: 12px; - background-color: red; - border-radius: 6px; - position: relative; -} - -.point:hover { - border: 1px solid black; -} - -.tooltip { - min-width: 200px; - padding: 10px; - background-color: white; - border-radius: 4px; -} diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx index b5741443be..626ca0e5f0 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx @@ -1,59 +1,12 @@ import { BarChart, XAxis, ResponsiveContainer } from 'recharts' -import { useFloating, offset, flip, shift, useInteractions, useHover } from '@floating-ui/react' -import type { ReactElement } from 'react' -import cx from 'classnames' -import { useState } from 'react' import React from 'react' import type { ResponsiveVisualizationItem } from '../../types' import type { BarChartByTypeProps } from '../types' +import { IndividualPoint } from '../points/IndividualPoint' import styles from './BarChartIndividual.module.css' type IndividualBarChartProps = BarChartByTypeProps<'individual'> -type IndividualBarChartPointProps = { - color?: string - point: ResponsiveVisualizationItem - tooltip?: ReactElement - className?: string -} - -export function IndividualBarChartPoint({ - point, - color, - tooltip, - className, -}: IndividualBarChartPointProps) { - const [isOpen, setIsOpen] = useState(false) - - const { refs, floatingStyles, context } = useFloating({ - open: isOpen, - placement: 'top', - onOpenChange: setIsOpen, - middleware: [offset(2), flip(), shift()], - }) - - const hover = useHover(context) - const { getReferenceProps, getFloatingProps } = useInteractions([hover]) - return ( -
  • - {isOpen && ( -
    - {tooltip ? React.cloneElement(tooltip, { data: point } as any) : point.name} -
    - )} -
  • - ) -} export function IndividualBarChart({ data, color, @@ -88,7 +41,7 @@ export function IndividualBarChart({
      {points?.map((point, pointIndex) => ( - (null) const [isIndividualSupported, setIsIndividualSupported] = useState(false) @@ -130,3 +130,37 @@ export function useResponsiveVisualizationData({ [data, isIndividualSupported, loadData] ) } + +type UseResponsiveVisualizationProps = { + containerRef: ResponsiveVisualizationContainerRef +} & UseResponsiveVisualizationDataProps +export function useResponsiveVisualization({ + containerRef, + labelKey, + aggregatedValueKey, + individualValueKey, + getAggregatedData, + getIndividualData, + getIsIndividualSupported, +}: UseResponsiveVisualizationProps) { + const dimensions = useResponsiveDimensions(containerRef) + const { data, isIndividualSupported, loadData } = useResponsiveVisualizationData({ + labelKey, + aggregatedValueKey, + individualValueKey, + getAggregatedData, + getIndividualData, + getIsIndividualSupported, + }) + + useEffect(() => { + if (dimensions.width && dimensions.height) { + loadData(dimensions) + } + }, [dimensions, loadData]) + + return useMemo( + () => ({ ...dimensions, data, isIndividualSupported }), + [data, isIndividualSupported, dimensions] + ) +} diff --git a/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx b/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx new file mode 100644 index 0000000000..e7311f7696 --- /dev/null +++ b/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx @@ -0,0 +1,45 @@ +import { useFloating, offset, flip, shift, useInteractions, useHover } from '@floating-ui/react' +import { cloneElement, useState, type ReactElement } from 'react' +import cx from 'classnames' +import type { ResponsiveVisualizationItem } from '../../types' +import styles from './IndividualPoint.module.css' + +type IndividualPointProps = { + color?: string + point: ResponsiveVisualizationItem + tooltip?: ReactElement + className?: string +} + +export function IndividualPoint({ point, color, tooltip, className }: IndividualPointProps) { + const [isOpen, setIsOpen] = useState(false) + + const { refs, floatingStyles, context } = useFloating({ + open: isOpen, + placement: 'top', + onOpenChange: setIsOpen, + middleware: [offset(2), flip(), shift()], + }) + + const hover = useHover(context) + const { getReferenceProps, getFloatingProps } = useInteractions([hover]) + return ( +
    • + {isOpen && ( +
      + {tooltip ? cloneElement(tooltip, { data: point } as any) : point.name} +
      + )} +
    • + ) +} diff --git a/libs/responsive-visualizations/src/charts/points/IndividualPoints.module.css b/libs/responsive-visualizations/src/charts/points/IndividualPoints.module.css new file mode 100644 index 0000000000..301892d2bf --- /dev/null +++ b/libs/responsive-visualizations/src/charts/points/IndividualPoints.module.css @@ -0,0 +1,19 @@ +.point { + display: block; + width: 12px; + height: 12px; + background-color: red; + border-radius: 6px; + position: relative; +} + +.point:hover { + border: 1px solid black; +} + +.tooltip { + min-width: 200px; + padding: 10px; + background-color: white; + border-radius: 4px; +} diff --git a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx index 51499a97c7..68f1e9181a 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx @@ -1,8 +1,7 @@ -import { useEffect } from 'react' import type { ResponsiveVisualizationData } from '../../types' import { getIsIndividualTimeseriesSupported } from '../../lib/density' import type { BaseResponsiveChartProps, BaseResponsiveTimeseriesProps } from '../types' -import { useResponsiveDimensions, useResponsiveVisualizationData } from '../hooks' +import { useResponsiveVisualization } from '../hooks' import { DEFAULT_AGGREGATED_VALUE_KEY, DEFAULT_INDIVIDUAL_VALUE_KEY, @@ -12,9 +11,9 @@ import { IndividualTimeseries } from './TimeseriesIndividual' import { AggregatedTimeseries } from './TimeseriesAggregated' type ResponsiveTimeseriesProps = BaseResponsiveChartProps & - BaseResponsiveTimeseriesProps & { dateKey?: string } + BaseResponsiveTimeseriesProps & { dateKey?: keyof ResponsiveVisualizationData<'aggregated'>[0] } -export function ResponsiveTimeseries({ +export function ResponsiveTimeseries({ start, end, dateKey = DEFAULT_DATE_KEY, @@ -30,8 +29,8 @@ export function ResponsiveTimeseries({ onIndividualItemClick, onAggregatedItemClick, }: ResponsiveTimeseriesProps) { - const { width, height } = useResponsiveDimensions(containerRef) - const { data, isIndividualSupported, loadData } = useResponsiveVisualizationData({ + const { width, data, isIndividualSupported } = useResponsiveVisualization({ + containerRef, labelKey: dateKey, individualValueKey, aggregatedValueKey, @@ -40,13 +39,6 @@ export function ResponsiveTimeseries({ getIsIndividualSupported: getIsIndividualTimeseriesSupported, }) - useEffect(() => { - if (width && height) { - console.log('loading data') - loadData({ width, height }) - } - }, [height, width, loadData]) - if (!getAggregatedData && !getIndividualData) { console.warn('No data getters functions provided') return null diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx index 69efaa06ad..067ecacf98 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx @@ -4,6 +4,7 @@ import max from 'lodash/max' import type { DurationUnit } from 'luxon' import { DateTime, Duration } from 'luxon' import { useMemo } from 'react' +import { getFourwingsInterval } from '@globalfishingwatch/deck-loaders' import type { TimeseriesByTypeProps } from '../types' const graphMargin = { top: 0, right: 0, left: -20, bottom: -10 } @@ -20,9 +21,7 @@ export function AggregatedTimeseries({ }: AggregatedTimeseriesProps) { const startMillis = DateTime.fromISO(start).toMillis() const endMillis = DateTime.fromISO(end).toMillis() - // TODO: REVIEW HOW TO HANDLE INTERVALS IN THIS COMPONENT - // const interval = getFourwingsInterval(startMillis, endMillis) - const interval = 'DAY' as DurationUnit + const interval = getFourwingsInterval(startMillis, endMillis) const intervalDiff = Math.floor( Duration.fromMillis(endMillis - startMillis).as(interval.toLowerCase() as DurationUnit) @@ -58,11 +57,11 @@ export function AggregatedTimeseries({ .plus({ [interval]: i }) .toISO() return { - date: d, - value: data.find((item) => item[dateKey] === d)?.value || 0, + [dateKey]: d, + [valueKey]: data.find((item) => item[dateKey] === d)?.[valueKey] || 0, } }) - }, [data, domain, intervalDiff, startMillis, dateKey]) + }, [data, domain, intervalDiff, startMillis, interval, valueKey, dateKey]) return ( @@ -72,7 +71,7 @@ export function AggregatedTimeseries({ domain={domain} dataKey={dateKey} interval="preserveStartEnd" - tickFormatter={(tick: string) => tickLabelFormatter?.(tick) || tick} + tickFormatter={(tick: string) => tickLabelFormatter?.(tick, interval) || tick} axisLine={paddedDomain[0] === 0} /> & { width: number } +const graphMargin = { top: 0, right: 0, left: -20, bottom: -10 } -export function IndividualTimeseries({ data }: IndividualTimeseriesProps) { - console.log('🚀 ~ IndividualTimeseries ~ data:', data) - return

      TODO

      +type IndividualTimeseriesProps = TimeseriesByTypeProps<'individual'> +export function IndividualTimeseries({ + data, + color, + start, + end, + dateKey, + valueKey, + tickLabelFormatter, + customTooltip, +}: IndividualTimeseriesProps) { + const startMillis = DateTime.fromISO(start).toMillis() + const endMillis = DateTime.fromISO(end).toMillis() + const interval = getFourwingsInterval(startMillis, endMillis) + + const intervalDiff = Math.floor( + Duration.fromMillis(endMillis - startMillis).as(interval.toLowerCase() as DurationUnit) + ) + + const dataMin: number = data.length + ? (min(data.map((item) => item[valueKey].length)) as number) + : 0 + const dataMax: number = data.length + ? (max(data.map((item) => item[valueKey].length)) as number) + : 0 + + const domainPadding = (dataMax - dataMin) / 8 + const paddedDomain: [number, number] = [ + Math.max(0, Math.floor(dataMin - domainPadding)), + Math.ceil(dataMax + domainPadding), + ] + + const domain = useMemo(() => { + if (start && end && interval) { + const cleanEnd = DateTime.fromISO(end, { zone: 'utc' }) + .minus({ [interval]: 1 }) + .toISO() as string + return [new Date(start).getTime(), new Date(cleanEnd).getTime()] + } + }, [start, end, interval]) + + const fullTimeseries = useMemo(() => { + if (!data || !domain) { + return [] + } + return Array(intervalDiff) + .fill(0) + .map((_, i) => i) + .map((i) => { + const d = DateTime.fromMillis(startMillis, { zone: 'UTC' }) + .plus({ [interval]: i }) + .toISO() + return { + [dateKey]: d, + [valueKey]: data.find((item) => item[dateKey] === d)?.[valueKey] || 0, + } + }) + }, [data, domain, intervalDiff, startMillis, dateKey]) + + return ( + + + + tickLabelFormatter?.(tick, interval) || tick} + axisLine={paddedDomain[0] === 0} + /> + + {/* {fullTimeseries?.length && ( + } /> + )} */} + +
      + {data.map((item, index) => { + const points = item?.[valueKey] as ResponsiveVisualizationItem[] + return ( +
      +
        + {points?.map((point, pointIndex) => ( + + ))} +
      +
      + ) + })} +
      +
      +
      +
      + ) } diff --git a/libs/responsive-visualizations/src/charts/types.ts b/libs/responsive-visualizations/src/charts/types.ts index b047ec18ba..f1caa1251a 100644 --- a/libs/responsive-visualizations/src/charts/types.ts +++ b/libs/responsive-visualizations/src/charts/types.ts @@ -1,4 +1,5 @@ import type { ReactElement } from 'react' +import type { FourwingsInterval } from '@globalfishingwatch/deck-loaders' import type { ResponsiveVisualizationData, ResponsiveVisualizationItem, @@ -11,18 +12,18 @@ export type ResponsiveVisualizationInteractionCallback void export type ResponsiveVisualizationContainerRef = React.RefObject -export type BaseResponsiveChartProps = { +export type BaseResponsiveChartProps = { containerRef: ResponsiveVisualizationContainerRef // Aggregated props aggregatedTooltip?: ReactElement onAggregatedItemClick?: ResponsiveVisualizationInteractionCallback getAggregatedData?: () => Promise | undefined> - aggregatedValueKey?: string + aggregatedValueKey?: keyof ResponsiveVisualizationData<'aggregated'>[0] // Individual props individualTooltip?: ReactElement onIndividualItemClick?: ResponsiveVisualizationInteractionCallback getIndividualData?: () => Promise | undefined> - individualValueKey?: string + individualValueKey?: keyof ResponsiveVisualizationData<'individual'>[0] } // Shared types within the BarChart @@ -34,8 +35,8 @@ export type BaseResponsiveBarChartProps = { export type BarChartByTypeProps = BaseResponsiveBarChartProps & { - labelKey: string - valueKey: string + labelKey: keyof ResponsiveVisualizationData[0] + valueKey: keyof ResponsiveVisualizationData[0] data: ResponsiveVisualizationData onClick?: ResponsiveVisualizationInteractionCallback customTooltip?: ReactElement @@ -46,13 +47,13 @@ export type BaseResponsiveTimeseriesProps = { start: string end: string color: string - tickLabelFormatter?: (item: string) => string + tickLabelFormatter?: (item: string, interval: FourwingsInterval) => string } export type TimeseriesByTypeProps = BaseResponsiveTimeseriesProps & { - dateKey: string - valueKey: string + dateKey: keyof ResponsiveVisualizationData[0] + valueKey: keyof ResponsiveVisualizationData[0] data: ResponsiveVisualizationData onClick?: ResponsiveVisualizationInteractionCallback customTooltip?: ReactElement diff --git a/libs/responsive-visualizations/src/lib/density.ts b/libs/responsive-visualizations/src/lib/density.ts index 03d6ebf2ed..13239d0de7 100644 --- a/libs/responsive-visualizations/src/lib/density.ts +++ b/libs/responsive-visualizations/src/lib/density.ts @@ -7,7 +7,7 @@ import type { const COLUMN_LABEL_SIZE = 10 const COLUMN_PADDING = 10 // Comment this is the sum of .point size + .bar flex gap -const POINT_WIDTH = 15 +const POINT_SIZE = 15 const AXIX_LABEL_PADDING = 40 export const getBarProps = ( @@ -16,7 +16,7 @@ export const getBarProps = ( ): { columnsNumber: number; columnsWidth: number; pointsByRow: number } => { const columnsNumber = data.length const columnsWidth = width / columnsNumber - COLUMN_PADDING * 2 - const pointsByRow = Math.floor(columnsWidth / POINT_WIDTH) + const pointsByRow = Math.floor(columnsWidth / POINT_SIZE) return { columnsNumber, columnsWidth, pointsByRow } } @@ -38,15 +38,15 @@ export function getIsIndividualBarChartSupported({ const { pointsByRow } = getBarProps(data, width) const biggestColumnValue = data.reduce((acc, column) => { const value = (column as ResponsiveVisualizationIndividualItem)[aggregatedValueKey] - ? (column as ResponsiveVisualizationIndividualItem)[aggregatedValueKey].length - : (column as ResponsiveVisualizationAggregatedItem)[individualValueKey].value + ? (column as ResponsiveVisualizationIndividualItem)[aggregatedValueKey] + : (column as ResponsiveVisualizationAggregatedItem)[individualValueKey].length if (value > acc) { return value } return acc }, 0) const rowsInBiggestColumn = Math.ceil(biggestColumnValue / pointsByRow) - const heightNeeded = rowsInBiggestColumn * POINT_WIDTH + const heightNeeded = rowsInBiggestColumn * POINT_SIZE return heightNeeded < height - AXIX_LABEL_PADDING - COLUMN_PADDING - COLUMN_LABEL_SIZE } @@ -57,12 +57,24 @@ type getIsIndividualTimeseriesSupportedParams = { aggregatedValueKey: string individualValueKey: string } + export function getIsIndividualTimeseriesSupported({ data, - width, height, aggregatedValueKey, individualValueKey, }: getIsIndividualTimeseriesSupportedParams): boolean { - return false + const biggestColumnValue = data.reduce((acc, column) => { + const value = (column as ResponsiveVisualizationIndividualItem)[aggregatedValueKey] + ? (column as ResponsiveVisualizationIndividualItem)[aggregatedValueKey] + : (column as ResponsiveVisualizationAggregatedItem)[individualValueKey].length + if (value > acc) { + return value + } + return acc + }, 0) + const heightNeeded = biggestColumnValue * POINT_SIZE + console.log('🚀 ~ biggestColumnValue:', biggestColumnValue) + console.log('🚀 ~ heightNeeded:', heightNeeded) + return heightNeeded < height - AXIX_LABEL_PADDING - COLUMN_PADDING - COLUMN_LABEL_SIZE } diff --git a/libs/responsive-visualizations/src/types.ts b/libs/responsive-visualizations/src/types.ts index 3af7748f7c..8b963710af 100644 --- a/libs/responsive-visualizations/src/types.ts +++ b/libs/responsive-visualizations/src/types.ts @@ -1,21 +1,38 @@ +import type { + DEFAULT_LABEL_KEY, + DEFAULT_DATE_KEY, + DEFAULT_AGGREGATED_VALUE_KEY, + DEFAULT_INDIVIDUAL_VALUE_KEY, +} from './charts/config' + export type ResponsiveVisualizationMode = 'individual' | 'aggregated' export type ResponsiveVisualizationChart = 'barchart' | 'timeseries' +export type DefaultResponsiveVisualizationLabels = { + [DEFAULT_LABEL_KEY]: string + [DEFAULT_DATE_KEY]: string +} export type ResponsiveVisualizationItem = Record -export type ResponsiveVisualizationAggregatedItem = { - label?: string - value?: number -} & Item +export type ResponsiveVisualizationAggregatedItem< + Item = DefaultResponsiveVisualizationLabels & { + [DEFAULT_AGGREGATED_VALUE_KEY]: number + }, +> = Item -export type ResponsiveVisualizationIndividualItem = { - label?: string - values?: ResponsiveVisualizationItem[] -} & Item +export type ResponsiveVisualizationIndividualItem< + Item = DefaultResponsiveVisualizationLabels & { + [DEFAULT_INDIVIDUAL_VALUE_KEY]: ResponsiveVisualizationItem[] + }, +> = Item export type ResponsiveVisualizationData< Mode extends ResponsiveVisualizationMode | undefined = undefined, - Data = ResponsiveVisualizationItem, + Data extends + | ResponsiveVisualizationAggregatedItem + | ResponsiveVisualizationIndividualItem = Mode extends 'aggregated' + ? ResponsiveVisualizationAggregatedItem + : ResponsiveVisualizationIndividualItem, > = Mode extends 'aggregated' ? ResponsiveVisualizationAggregatedItem[] : Mode extends 'individual' From f79a22b1456f9119207fb6f8e363befd31eef69e Mon Sep 17 00:00:00 2001 From: j8seangel Date: Fri, 10 Jan 2025 16:46:47 +0100 Subject: [PATCH 08/62] align timeseries points --- .../shared/events/EventsReportGraph.tsx | 4 +- .../barchart/BarChartIndividual.module.css | 1 - .../charts/barchart/BarChartIndividual.tsx | 3 +- .../src/charts/config.ts | 6 +++ .../src/charts/hooks.ts | 10 +++-- ....module.css => IndividualPoint.module.css} | 1 + .../src/charts/timeseries/Timeseries.tsx | 17 +++++++-- .../TimeseriesIndividual.module.css | 6 +-- .../timeseries/TimeseriesIndividual.tsx | 37 ++++++++----------- .../src/charts/types.ts | 11 ++++-- .../src/lib/density.ts | 18 +++------ libs/responsive-visualizations/src/types.ts | 12 +++--- 12 files changed, 69 insertions(+), 57 deletions(-) rename libs/responsive-visualizations/src/charts/points/{IndividualPoints.module.css => IndividualPoint.module.css} (95%) diff --git a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx index daa1756a10..42f975c4a9 100644 --- a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx +++ b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx @@ -44,7 +44,7 @@ const AggregatedGraphTooltip = (props: any) => { } const IndividualGraphTooltip = ({ data }: { data?: any }) => { - return data.value + return data.label } const formatDateTicks: BaseResponsiveTimeseriesProps['tickLabelFormatter'] = ( @@ -73,7 +73,7 @@ export default function EventsReportGraph({ const getIndividualData = useCallback(async () => { return timeseries.map((t) => ({ date: t.date, - values: [...new Array(t.value)].map((v, i) => ({ label: i, value: i })), + values: [...new Array(t.value)].map((v, i) => ({ label: t.date, value: i })), })) }, [timeseries]) diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.module.css b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.module.css index ffbd917ee7..fbac2183ed 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.module.css +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.module.css @@ -4,7 +4,6 @@ display: flex; gap: 20px; align-items: flex-end; - padding-bottom: 40px; } .barContainer { diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx index 626ca0e5f0..5d8415a2fa 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx @@ -3,6 +3,7 @@ import React from 'react' import type { ResponsiveVisualizationItem } from '../../types' import type { BarChartByTypeProps } from '../types' import { IndividualPoint } from '../points/IndividualPoint' +import { AXIS_LABEL_PADDING } from '../config' import styles from './BarChartIndividual.module.css' type IndividualBarChartProps = BarChartByTypeProps<'individual'> @@ -31,7 +32,7 @@ export function IndividualBarChart({ // onClick={onBarClick} > -
      +
      {data.map((item, index) => { const points = item?.[valueKey] as ResponsiveVisualizationItem[] return ( diff --git a/libs/responsive-visualizations/src/charts/config.ts b/libs/responsive-visualizations/src/charts/config.ts index 1c50292826..616a35c18e 100644 --- a/libs/responsive-visualizations/src/charts/config.ts +++ b/libs/responsive-visualizations/src/charts/config.ts @@ -2,3 +2,9 @@ export const DEFAULT_LABEL_KEY = 'label' export const DEFAULT_DATE_KEY = 'date' export const DEFAULT_AGGREGATED_VALUE_KEY = 'value' export const DEFAULT_INDIVIDUAL_VALUE_KEY = 'values' + +export const COLUMN_LABEL_SIZE = 10 +export const COLUMN_PADDING = 10 +// This is the sum of .point size + .bar flex gap +export const POINT_SIZE = 15 +export const AXIS_LABEL_PADDING = 34 diff --git a/libs/responsive-visualizations/src/charts/hooks.ts b/libs/responsive-visualizations/src/charts/hooks.ts index 22536d05af..fd76ef492f 100644 --- a/libs/responsive-visualizations/src/charts/hooks.ts +++ b/libs/responsive-visualizations/src/charts/hooks.ts @@ -4,7 +4,11 @@ import type { getIsIndividualBarChartSupported, getIsIndividualTimeseriesSupported, } from '../lib/density' -import type { BaseResponsiveChartProps, ResponsiveVisualizationContainerRef } from './types' +import type { + BaseResponsiveChartProps, + ResponsiveVisualizationAnyItemKey, + ResponsiveVisualizationContainerRef, +} from './types' import { DEFAULT_AGGREGATED_VALUE_KEY, DEFAULT_INDIVIDUAL_VALUE_KEY, @@ -37,11 +41,11 @@ export function useResponsiveDimensions(containerRef: ResponsiveVisualizationCon } type UseResponsiveVisualizationDataProps = { - labelKey: string + labelKey: ResponsiveVisualizationAnyItemKey individualValueKey: BaseResponsiveChartProps['individualValueKey'] aggregatedValueKey: BaseResponsiveChartProps['aggregatedValueKey'] getAggregatedData?: BaseResponsiveChartProps['getAggregatedData'] - getIndividualData?: BaseResponsiveChartProps['getAggregatedData'] + getIndividualData?: BaseResponsiveChartProps['getIndividualData'] getIsIndividualSupported: | typeof getIsIndividualBarChartSupported | typeof getIsIndividualTimeseriesSupported diff --git a/libs/responsive-visualizations/src/charts/points/IndividualPoints.module.css b/libs/responsive-visualizations/src/charts/points/IndividualPoint.module.css similarity index 95% rename from libs/responsive-visualizations/src/charts/points/IndividualPoints.module.css rename to libs/responsive-visualizations/src/charts/points/IndividualPoint.module.css index 301892d2bf..0d32e12699 100644 --- a/libs/responsive-visualizations/src/charts/points/IndividualPoints.module.css +++ b/libs/responsive-visualizations/src/charts/points/IndividualPoint.module.css @@ -16,4 +16,5 @@ padding: 10px; background-color: white; border-radius: 4px; + z-index: 1; } diff --git a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx index 68f1e9181a..e50e2d6405 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx @@ -1,4 +1,8 @@ -import type { ResponsiveVisualizationData } from '../../types' +import type { + ResponsiveVisualizationData, + ResponsiveVisualizationAggregatedItem, + ResponsiveVisualizationIndividualItem, +} from '../../types' import { getIsIndividualTimeseriesSupported } from '../../lib/density' import type { BaseResponsiveChartProps, BaseResponsiveTimeseriesProps } from '../types' import { useResponsiveVisualization } from '../hooks' @@ -11,9 +15,13 @@ import { IndividualTimeseries } from './TimeseriesIndividual' import { AggregatedTimeseries } from './TimeseriesAggregated' type ResponsiveTimeseriesProps = BaseResponsiveChartProps & - BaseResponsiveTimeseriesProps & { dateKey?: keyof ResponsiveVisualizationData<'aggregated'>[0] } + BaseResponsiveTimeseriesProps & { + dateKey?: + | keyof ResponsiveVisualizationData<'aggregated'>[0] + | keyof ResponsiveVisualizationData<'individual'>[0] + } -export function ResponsiveTimeseries({ +export function ResponsiveTimeseries({ start, end, dateKey = DEFAULT_DATE_KEY, @@ -29,7 +37,7 @@ export function ResponsiveTimeseries({ onIndividualItemClick, onAggregatedItemClick, }: ResponsiveTimeseriesProps) { - const { width, data, isIndividualSupported } = useResponsiveVisualization({ + const { width, height, data, isIndividualSupported } = useResponsiveVisualization({ containerRef, labelKey: dateKey, individualValueKey, @@ -53,6 +61,7 @@ export function ResponsiveTimeseries({ return isIndividualSupported ? ( } start={start} diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.module.css b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.module.css index 611095251c..0e481ac13b 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.module.css +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.module.css @@ -1,10 +1,8 @@ .container { height: 100%; - padding-inline: 10px; display: flex; - gap: 20px; align-items: flex-end; - padding-bottom: 40px; + padding-inline: 8px; } .barContainer { @@ -13,7 +11,7 @@ height: 100%; flex-direction: column; align-items: center; - justify-content: center; + justify-content: flex-end; } .bar { diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx index 3d033cead1..1294e67424 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx @@ -8,12 +8,18 @@ import { getFourwingsInterval } from '@globalfishingwatch/deck-loaders' import type { TimeseriesByTypeProps } from '../types' import type { ResponsiveVisualizationItem } from '../../types' import { IndividualPoint } from '../points/IndividualPoint' +import { AXIS_LABEL_PADDING, POINT_SIZE } from '../config' import styles from './TimeseriesIndividual.module.css' -const graphMargin = { top: 0, right: 0, left: -20, bottom: -10 } +const graphMargin = { top: 0, right: POINT_SIZE, left: POINT_SIZE, bottom: 0 } + +type IndividualTimeseriesProps = TimeseriesByTypeProps<'individual'> & { + width: number + height: number +} -type IndividualTimeseriesProps = TimeseriesByTypeProps<'individual'> export function IndividualTimeseries({ + height, data, color, start, @@ -31,19 +37,6 @@ export function IndividualTimeseries({ Duration.fromMillis(endMillis - startMillis).as(interval.toLowerCase() as DurationUnit) ) - const dataMin: number = data.length - ? (min(data.map((item) => item[valueKey].length)) as number) - : 0 - const dataMax: number = data.length - ? (max(data.map((item) => item[valueKey].length)) as number) - : 0 - - const domainPadding = (dataMax - dataMin) / 8 - const paddedDomain: [number, number] = [ - Math.max(0, Math.floor(dataMin - domainPadding)), - Math.ceil(dataMax + domainPadding), - ] - const domain = useMemo(() => { if (start && end && interval) { const cleanEnd = DateTime.fromISO(end, { zone: 'utc' }) @@ -69,7 +62,7 @@ export function IndividualTimeseries({ [valueKey]: data.find((item) => item[dateKey] === d)?.[valueKey] || 0, } }) - }, [data, domain, intervalDiff, startMillis, dateKey]) + }, [data, domain, intervalDiff, startMillis, interval, dateKey, valueKey]) return ( @@ -80,22 +73,24 @@ export function IndividualTimeseries({ dataKey={dateKey} interval="preserveStartEnd" tickFormatter={(tick: string) => tickLabelFormatter?.(tick, interval) || tick} - axisLine={paddedDomain[0] === 0} + axisLine={true} /> - + /> */} {/* {fullTimeseries?.length && ( } /> )} */} -
      +
      {data.map((item, index) => { const points = item?.[valueKey] as ResponsiveVisualizationItem[] return ( diff --git a/libs/responsive-visualizations/src/charts/types.ts b/libs/responsive-visualizations/src/charts/types.ts index f1caa1251a..beb20f5b0d 100644 --- a/libs/responsive-visualizations/src/charts/types.ts +++ b/libs/responsive-visualizations/src/charts/types.ts @@ -12,7 +12,7 @@ export type ResponsiveVisualizationInteractionCallback void export type ResponsiveVisualizationContainerRef = React.RefObject -export type BaseResponsiveChartProps = { +export type BaseResponsiveChartProps = { containerRef: ResponsiveVisualizationContainerRef // Aggregated props aggregatedTooltip?: ReactElement @@ -26,6 +26,11 @@ export type BaseResponsiveChartProps = { individualValueKey?: keyof ResponsiveVisualizationData<'individual'>[0] } +// TODO: remove this +export type ResponsiveVisualizationAnyItemKey = + | keyof ResponsiveVisualizationData<'aggregated'>[0] + | keyof ResponsiveVisualizationData<'individual'>[0] + // Shared types within the BarChart export type BaseResponsiveBarChartProps = { color: string @@ -35,7 +40,7 @@ export type BaseResponsiveBarChartProps = { export type BarChartByTypeProps = BaseResponsiveBarChartProps & { - labelKey: keyof ResponsiveVisualizationData[0] + labelKey: ResponsiveVisualizationAnyItemKey valueKey: keyof ResponsiveVisualizationData[0] data: ResponsiveVisualizationData onClick?: ResponsiveVisualizationInteractionCallback @@ -52,7 +57,7 @@ export type BaseResponsiveTimeseriesProps = { export type TimeseriesByTypeProps = BaseResponsiveTimeseriesProps & { - dateKey: keyof ResponsiveVisualizationData[0] + dateKey: ResponsiveVisualizationAnyItemKey valueKey: keyof ResponsiveVisualizationData[0] data: ResponsiveVisualizationData onClick?: ResponsiveVisualizationInteractionCallback diff --git a/libs/responsive-visualizations/src/lib/density.ts b/libs/responsive-visualizations/src/lib/density.ts index 13239d0de7..a6e2ced612 100644 --- a/libs/responsive-visualizations/src/lib/density.ts +++ b/libs/responsive-visualizations/src/lib/density.ts @@ -1,15 +1,11 @@ +import type { ResponsiveVisualizationAnyItemKey } from '../charts' +import { COLUMN_PADDING, POINT_SIZE, AXIS_LABEL_PADDING, COLUMN_LABEL_SIZE } from '../charts/config' import type { ResponsiveVisualizationAggregatedItem, ResponsiveVisualizationData, ResponsiveVisualizationIndividualItem, } from '../types' -const COLUMN_LABEL_SIZE = 10 -const COLUMN_PADDING = 10 -// Comment this is the sum of .point size + .bar flex gap -const POINT_SIZE = 15 -const AXIX_LABEL_PADDING = 40 - export const getBarProps = ( data: ResponsiveVisualizationData, width: number @@ -25,8 +21,8 @@ type IsIndividualSupportedParams = { data: ResponsiveVisualizationData width: number height: number - aggregatedValueKey: string - individualValueKey: string + aggregatedValueKey: ResponsiveVisualizationAnyItemKey + individualValueKey: ResponsiveVisualizationAnyItemKey } export function getIsIndividualBarChartSupported({ data, @@ -47,7 +43,7 @@ export function getIsIndividualBarChartSupported({ }, 0) const rowsInBiggestColumn = Math.ceil(biggestColumnValue / pointsByRow) const heightNeeded = rowsInBiggestColumn * POINT_SIZE - return heightNeeded < height - AXIX_LABEL_PADDING - COLUMN_PADDING - COLUMN_LABEL_SIZE + return heightNeeded < height - AXIS_LABEL_PADDING - COLUMN_PADDING - COLUMN_LABEL_SIZE } type getIsIndividualTimeseriesSupportedParams = { @@ -74,7 +70,5 @@ export function getIsIndividualTimeseriesSupported({ return acc }, 0) const heightNeeded = biggestColumnValue * POINT_SIZE - console.log('🚀 ~ biggestColumnValue:', biggestColumnValue) - console.log('🚀 ~ heightNeeded:', heightNeeded) - return heightNeeded < height - AXIX_LABEL_PADDING - COLUMN_PADDING - COLUMN_LABEL_SIZE + return heightNeeded < height - AXIS_LABEL_PADDING } diff --git a/libs/responsive-visualizations/src/types.ts b/libs/responsive-visualizations/src/types.ts index 8b963710af..c30e2abdf4 100644 --- a/libs/responsive-visualizations/src/types.ts +++ b/libs/responsive-visualizations/src/types.ts @@ -9,19 +9,19 @@ export type ResponsiveVisualizationMode = 'individual' | 'aggregated' export type ResponsiveVisualizationChart = 'barchart' | 'timeseries' -export type DefaultResponsiveVisualizationLabels = { - [DEFAULT_LABEL_KEY]: string - [DEFAULT_DATE_KEY]: string -} export type ResponsiveVisualizationItem = Record export type ResponsiveVisualizationAggregatedItem< - Item = DefaultResponsiveVisualizationLabels & { + Item = { + [DEFAULT_LABEL_KEY]?: string + [DEFAULT_DATE_KEY]?: string [DEFAULT_AGGREGATED_VALUE_KEY]: number }, > = Item export type ResponsiveVisualizationIndividualItem< - Item = DefaultResponsiveVisualizationLabels & { + Item = { + [DEFAULT_LABEL_KEY]?: string + [DEFAULT_DATE_KEY]?: string [DEFAULT_INDIVIDUAL_VALUE_KEY]: ResponsiveVisualizationItem[] }, > = Item From 7ebf76edb8260d8fcb4d8183f1b03e04b53b6c08 Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Fri, 10 Jan 2025 17:38:58 +0100 Subject: [PATCH 09/62] fix deck core version --- libs/deck-layers/package.json | 2 +- package.json | 2 +- yarn.lock | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/deck-layers/package.json b/libs/deck-layers/package.json index 0e8f009722..32da2b399b 100644 --- a/libs/deck-layers/package.json +++ b/libs/deck-layers/package.json @@ -6,7 +6,7 @@ "dependencies": { "@deck.gl-community/editable-layers": "^9.0.2", "@deck.gl-community/layers": "^9.0.2", - "@deck.gl/core": "^9.0.35", + "@deck.gl/core": "9.0.36", "@deck.gl/extensions": "^9.0.35", "@deck.gl/geo-layers": "^9.0.35", "@deck.gl/layers": "^9.0.35", diff --git a/package.json b/package.json index 38dac01dfb..2e3a3b5f83 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "dependencies": { "@deck.gl-community/editable-layers": "9.0.3", "@deck.gl-community/layers": "9.0.3", - "@deck.gl/core": "^9.0.35", + "@deck.gl/core": "9.0.36", "@deck.gl/extensions": "^9.0.35", "@deck.gl/geo-layers": "^9.0.35", "@deck.gl/layers": "^9.0.35", diff --git a/yarn.lock b/yarn.lock index 916ee74960..039bfbd07b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1713,7 +1713,7 @@ __metadata: languageName: node linkType: hard -"@deck.gl/core@npm:^9.0.12, @deck.gl/core@npm:^9.0.35": +"@deck.gl/core@npm:9.0.36, @deck.gl/core@npm:^9.0.12": version: 9.0.36 resolution: "@deck.gl/core@npm:9.0.36" dependencies: @@ -2859,7 +2859,7 @@ __metadata: dependencies: "@deck.gl-community/editable-layers": "npm:9.0.3" "@deck.gl-community/layers": "npm:9.0.3" - "@deck.gl/core": "npm:^9.0.35" + "@deck.gl/core": "npm:9.0.36" "@deck.gl/extensions": "npm:^9.0.35" "@deck.gl/geo-layers": "npm:^9.0.35" "@deck.gl/layers": "npm:^9.0.35" From b8c5a7aeaaf5d430ccf51a432e9da73cc416463a Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Fri, 10 Jan 2025 17:39:34 +0100 Subject: [PATCH 10/62] fix timeseries layout --- .../src/charts/timeseries/Timeseries.tsx | 5 +- .../TimeseriesIndividual.module.css | 2 +- .../timeseries/TimeseriesIndividual.tsx | 53 +++++++++---------- 3 files changed, 26 insertions(+), 34 deletions(-) diff --git a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx index e50e2d6405..c809bc5bbc 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx @@ -1,7 +1,5 @@ import type { ResponsiveVisualizationData, - ResponsiveVisualizationAggregatedItem, - ResponsiveVisualizationIndividualItem, } from '../../types' import { getIsIndividualTimeseriesSupported } from '../../lib/density' import type { BaseResponsiveChartProps, BaseResponsiveTimeseriesProps } from '../types' @@ -37,7 +35,7 @@ export function ResponsiveTimeseries({ onIndividualItemClick, onAggregatedItemClick, }: ResponsiveTimeseriesProps) { - const { width, height, data, isIndividualSupported } = useResponsiveVisualization({ + const { width, data, isIndividualSupported } = useResponsiveVisualization({ containerRef, labelKey: dateKey, individualValueKey, @@ -61,7 +59,6 @@ export function ResponsiveTimeseries({ return isIndividualSupported ? ( } start={start} diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.module.css b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.module.css index 0e481ac13b..94887790cc 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.module.css +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.module.css @@ -2,12 +2,12 @@ height: 100%; display: flex; align-items: flex-end; + justify-content: space-between; padding-inline: 8px; } .barContainer { display: flex; - flex: 1; height: 100%; flex-direction: column; align-items: center; diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx index 1294e67424..d194ddeae4 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx @@ -1,8 +1,5 @@ -import { XAxis, ResponsiveContainer, YAxis, CartesianGrid, ComposedChart, Line } from 'recharts' -import min from 'lodash/min' -import max from 'lodash/max' -import type { DurationUnit } from 'luxon' -import { DateTime, Duration } from 'luxon' +import { XAxis, ResponsiveContainer, ComposedChart } from 'recharts' +import { DateTime } from 'luxon' import { useMemo } from 'react' import { getFourwingsInterval } from '@globalfishingwatch/deck-loaders' import type { TimeseriesByTypeProps } from '../types' @@ -15,11 +12,9 @@ const graphMargin = { top: 0, right: POINT_SIZE, left: POINT_SIZE, bottom: 0 } type IndividualTimeseriesProps = TimeseriesByTypeProps<'individual'> & { width: number - height: number } export function IndividualTimeseries({ - height, data, color, start, @@ -33,9 +28,9 @@ export function IndividualTimeseries({ const endMillis = DateTime.fromISO(end).toMillis() const interval = getFourwingsInterval(startMillis, endMillis) - const intervalDiff = Math.floor( - Duration.fromMillis(endMillis - startMillis).as(interval.toLowerCase() as DurationUnit) - ) + // const intervalDiff = Math.floor( + // Duration.fromMillis(endMillis - startMillis).as(interval.toLowerCase() as DurationUnit) + // ) const domain = useMemo(() => { if (start && end && interval) { @@ -46,28 +41,28 @@ export function IndividualTimeseries({ } }, [start, end, interval]) - const fullTimeseries = useMemo(() => { - if (!data || !domain) { - return [] - } - return Array(intervalDiff) - .fill(0) - .map((_, i) => i) - .map((i) => { - const d = DateTime.fromMillis(startMillis, { zone: 'UTC' }) - .plus({ [interval]: i }) - .toISO() - return { - [dateKey]: d, - [valueKey]: data.find((item) => item[dateKey] === d)?.[valueKey] || 0, - } - }) - }, [data, domain, intervalDiff, startMillis, interval, dateKey, valueKey]) + // const fullTimeseries = useMemo(() => { + // if (!data || !domain) { + // return [] + // } + // return Array(intervalDiff) + // .fill(0) + // .map((_, i) => i) + // .map((i) => { + // const d = DateTime.fromMillis(startMillis, { zone: 'UTC' }) + // .plus({ [interval]: i }) + // .toISO() + // return { + // [dateKey]: d, + // [valueKey]: data.find((item) => item[dateKey] === d)?.[valueKey] || 0, + // } + // }) + // }, [data, domain, intervalDiff, startMillis, interval, dateKey, valueKey]) return ( - + {/* */} { const points = item?.[valueKey] as ResponsiveVisualizationItem[] return ( -
      +
        {points?.map((point, pointIndex) => ( Date: Fri, 10 Jan 2025 17:39:59 +0100 Subject: [PATCH 11/62] fix vessel group vessels graph labels --- .../vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx index 4636697573..f6e82d67a7 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx @@ -237,9 +237,10 @@ export default function VesselGroupReportVesselsGraph({ pageQueryParam={pageQueryParam} /> } + labelKey='name' individualTooltip={} aggregatedTooltip={} - > + /> )}
      From d6c00c1feb5da4e89b106b0be0a157d7250619f7 Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Fri, 10 Jan 2025 18:15:53 +0100 Subject: [PATCH 12/62] fix tooltip overflow --- .../charts/points/IndividualPoint.module.css | 4 ++-- .../src/charts/points/IndividualPoint.tsx | 20 ++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/libs/responsive-visualizations/src/charts/points/IndividualPoint.module.css b/libs/responsive-visualizations/src/charts/points/IndividualPoint.module.css index 0d32e12699..cbd90bfac1 100644 --- a/libs/responsive-visualizations/src/charts/points/IndividualPoint.module.css +++ b/libs/responsive-visualizations/src/charts/points/IndividualPoint.module.css @@ -15,6 +15,6 @@ min-width: 200px; padding: 10px; background-color: white; - border-radius: 4px; - z-index: 1; + border: var(--border); + z-index: 100; } diff --git a/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx b/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx index e7311f7696..2fd2697101 100644 --- a/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx +++ b/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx @@ -1,4 +1,4 @@ -import { useFloating, offset, flip, shift, useInteractions, useHover } from '@floating-ui/react' +import { useFloating, offset, flip, shift, useInteractions, useHover, FloatingPortal } from '@floating-ui/react' import { cloneElement, useState, type ReactElement } from 'react' import cx from 'classnames' import type { ResponsiveVisualizationItem } from '../../types' @@ -31,14 +31,16 @@ export function IndividualPoint({ point, color, tooltip, className }: Individual style={color ? { backgroundColor: color } : {}} > {isOpen && ( -
      - {tooltip ? cloneElement(tooltip, { data: point } as any) : point.name} -
      + +
      + {tooltip ? cloneElement(tooltip, { data: point } as any) : point.name} +
      +
      )} ) From 83bf26391b51f91c4b69bbe4997fab13b4dcf261 Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Fri, 10 Jan 2025 18:16:49 +0100 Subject: [PATCH 13/62] enhance TimeseriesIndividual single time data representation --- .../src/charts/timeseries/TimeseriesIndividual.module.css | 4 ++++ .../src/charts/timeseries/TimeseriesIndividual.tsx | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.module.css b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.module.css index 94887790cc..1acd10a478 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.module.css +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.module.css @@ -6,6 +6,10 @@ padding-inline: 8px; } +.containerSingleTime { + justify-content: space-around; +} + .barContainer { display: flex; height: 100%; diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx index d194ddeae4..7abe724b7e 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx @@ -1,5 +1,6 @@ import { XAxis, ResponsiveContainer, ComposedChart } from 'recharts' import { DateTime } from 'luxon' +import cx from 'classnames' import { useMemo } from 'react' import { getFourwingsInterval } from '@globalfishingwatch/deck-loaders' import type { TimeseriesByTypeProps } from '../types' @@ -85,7 +86,7 @@ export function IndividualTimeseries({ } /> )} */} -
      +
      {data.map((item, index) => { const points = item?.[valueKey] as ResponsiveVisualizationItem[] return ( From 91a9000bf536c641c29fa3b6dfefa200099ad4d9 Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Mon, 13 Jan 2025 12:33:54 +0100 Subject: [PATCH 14/62] use local containerRef instead of passing it as a prop --- .../vessels/VesselGroupReportVesselsGraph.tsx | 46 ++++++------ .../src/charts/barchart/BarChart.module.css | 4 ++ .../src/charts/barchart/BarChart.tsx | 61 ++++++++-------- .../charts/timeseries/Timeseries.module.css | 4 ++ .../src/charts/timeseries/Timeseries.tsx | 71 +++++++++---------- .../src/charts/types.ts | 1 - 6 files changed, 95 insertions(+), 92 deletions(-) create mode 100644 libs/responsive-visualizations/src/charts/barchart/BarChart.module.css create mode 100644 libs/responsive-visualizations/src/charts/timeseries/Timeseries.module.css diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx index f6e82d67a7..0ca9979147 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx @@ -190,7 +190,6 @@ export default function VesselGroupReportVesselsGraph({ filterQueryParam, pageQueryParam, }: VesselGroupReportVesselsGraphProps) { - const ref = useRef(null) const { dispatchQueryParams } = useLocationConnect() const onBarClick: ResponsiveVisualizationInteractionCallback = (e) => { @@ -218,30 +217,27 @@ export default function VesselGroupReportVesselsGraph({ return ( -
      - {data && data.length > 0 && ( - { - return formatI18nNumber(value).toString() - }} - barLabel={ - - } - labelKey='name' - individualTooltip={} - aggregatedTooltip={} - /> - )} +
      + { + return formatI18nNumber(value).toString() + }} + barLabel={ + + } + labelKey="name" + individualTooltip={} + aggregatedTooltip={} + />
      ) diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChart.module.css b/libs/responsive-visualizations/src/charts/barchart/BarChart.module.css new file mode 100644 index 0000000000..cbe226cbcd --- /dev/null +++ b/libs/responsive-visualizations/src/charts/barchart/BarChart.module.css @@ -0,0 +1,4 @@ +.container { + width: 100%; + height: 100%; +} diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx index 533c84b192..3623a65b8a 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx @@ -1,3 +1,4 @@ +import { useRef } from 'react' import type { ResponsiveVisualizationData } from '../../types' import { getIsIndividualBarChartSupported } from '../../lib/density' import type { BaseResponsiveBarChartProps, BaseResponsiveChartProps } from '../types' @@ -9,12 +10,12 @@ import { import { useResponsiveVisualization } from '../hooks' import { IndividualBarChart } from './BarChartIndividual' import { AggregatedBarChart } from './BarChartAggregated' +import styles from './BarChart.module.css' type ResponsiveBarChartProps = BaseResponsiveChartProps & BaseResponsiveBarChartProps & { labelKey?: string } export function ResponsiveBarChart({ - containerRef, getIndividualData, getAggregatedData, color, @@ -28,6 +29,7 @@ export function ResponsiveBarChart({ onIndividualItemClick, onAggregatedItemClick, }: ResponsiveBarChartProps) { + const containerRef = useRef(null) const { data, isIndividualSupported } = useResponsiveVisualization({ containerRef, labelKey, @@ -43,34 +45,33 @@ export function ResponsiveBarChart({ return null } - if (!data) { - return 'Spinner' - } - if (isIndividualSupported && !data) { - return 'Spinner for individual' - } - - return isIndividualSupported ? ( - } - color={color} - valueKey={individualValueKey} - labelKey={labelKey} - onClick={onIndividualItemClick} - barLabel={barLabel} - customTooltip={individualTooltip} - barValueFormatter={barValueFormatter} - /> - ) : ( - } - color={color} - valueKey={aggregatedValueKey} - labelKey={labelKey} - onClick={onAggregatedItemClick} - barLabel={barLabel} - customTooltip={aggregatedTooltip} - barValueFormatter={barValueFormatter} - /> + return ( +
      + {!data ? ( + 'Spinner' + ) : isIndividualSupported ? ( + } + color={color} + valueKey={individualValueKey} + labelKey={labelKey} + onClick={onIndividualItemClick} + barLabel={barLabel} + customTooltip={individualTooltip} + barValueFormatter={barValueFormatter} + /> + ) : ( + } + color={color} + valueKey={aggregatedValueKey} + labelKey={labelKey} + onClick={onAggregatedItemClick} + barLabel={barLabel} + customTooltip={aggregatedTooltip} + barValueFormatter={barValueFormatter} + /> + )} +
      ) } diff --git a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.module.css b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.module.css new file mode 100644 index 0000000000..cbe226cbcd --- /dev/null +++ b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.module.css @@ -0,0 +1,4 @@ +.container { + width: 100%; + height: 100%; +} diff --git a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx index c809bc5bbc..47e874a8fb 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx @@ -1,6 +1,5 @@ -import type { - ResponsiveVisualizationData, -} from '../../types' +import { useRef } from 'react' +import type { ResponsiveVisualizationData } from '../../types' import { getIsIndividualTimeseriesSupported } from '../../lib/density' import type { BaseResponsiveChartProps, BaseResponsiveTimeseriesProps } from '../types' import { useResponsiveVisualization } from '../hooks' @@ -11,6 +10,7 @@ import { } from '../config' import { IndividualTimeseries } from './TimeseriesIndividual' import { AggregatedTimeseries } from './TimeseriesAggregated' +import styles from './Timeseries.module.css' type ResponsiveTimeseriesProps = BaseResponsiveChartProps & BaseResponsiveTimeseriesProps & { @@ -25,7 +25,6 @@ export function ResponsiveTimeseries({ dateKey = DEFAULT_DATE_KEY, aggregatedValueKey = DEFAULT_AGGREGATED_VALUE_KEY, individualValueKey = DEFAULT_INDIVIDUAL_VALUE_KEY, - containerRef, getIndividualData, getAggregatedData, color, @@ -35,6 +34,7 @@ export function ResponsiveTimeseries({ onIndividualItemClick, onAggregatedItemClick, }: ResponsiveTimeseriesProps) { + const containerRef = useRef(null) const { width, data, isIndividualSupported } = useResponsiveVisualization({ containerRef, labelKey: dateKey, @@ -50,37 +50,36 @@ export function ResponsiveTimeseries({ return null } - if (!data) { - return 'Spinner' - } - if (isIndividualSupported && !data) { - return 'Spinner for individual' - } - - return isIndividualSupported ? ( - } - start={start} - end={end} - color={color} - dateKey={dateKey} - valueKey={individualValueKey} - onClick={onIndividualItemClick} - tickLabelFormatter={tickLabelFormatter} - customTooltip={individualTooltip} - /> - ) : ( - } - start={start} - end={end} - color={color} - dateKey={dateKey} - valueKey={aggregatedValueKey} - onClick={onAggregatedItemClick} - tickLabelFormatter={tickLabelFormatter} - customTooltip={aggregatedTooltip} - /> + return ( +
      + {!data ? ( + 'Spinner' + ) : isIndividualSupported ? ( + } + start={start} + end={end} + color={color} + dateKey={dateKey} + valueKey={individualValueKey} + onClick={onIndividualItemClick} + tickLabelFormatter={tickLabelFormatter} + customTooltip={individualTooltip} + /> + ) : ( + } + start={start} + end={end} + color={color} + dateKey={dateKey} + valueKey={aggregatedValueKey} + onClick={onAggregatedItemClick} + tickLabelFormatter={tickLabelFormatter} + customTooltip={aggregatedTooltip} + /> + )} +
      ) } diff --git a/libs/responsive-visualizations/src/charts/types.ts b/libs/responsive-visualizations/src/charts/types.ts index beb20f5b0d..d3620602e8 100644 --- a/libs/responsive-visualizations/src/charts/types.ts +++ b/libs/responsive-visualizations/src/charts/types.ts @@ -13,7 +13,6 @@ export type ResponsiveVisualizationInteractionCallback export type BaseResponsiveChartProps = { - containerRef: ResponsiveVisualizationContainerRef // Aggregated props aggregatedTooltip?: ReactElement onAggregatedItemClick?: ResponsiveVisualizationInteractionCallback From 0396787871895abaa6d124f2481a2cbd84e065f2 Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Mon, 13 Jan 2025 13:13:28 +0100 Subject: [PATCH 15/62] use in vessel-groups coverage insight --- .../VGRInsightCoverageGraph.module.css | 7 -- .../insights/VGRInsightCoverageGraph.tsx | 107 +++++++++++------- .../vessels/VesselGroupReportVesselsGraph.tsx | 33 +----- ...selGroupReportVesselsIndividualTooltip.tsx | 34 ++++++ 4 files changed, 101 insertions(+), 80 deletions(-) create mode 100644 apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.tsx diff --git a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.module.css b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.module.css index 849da41490..1220ca1beb 100644 --- a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.module.css +++ b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.module.css @@ -30,13 +30,6 @@ color: var(--color-secondary-blue); } -.graph tspan.info { - text-transform: lowercase; - font-family: serif; - font-style: italic; - font-weight: 600; -} - .graph :global(.recharts-tooltip-cursor) { fill: var(--color-terthiary-blue); } diff --git a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx index ada2f7c698..4c7db63707 100644 --- a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx @@ -1,12 +1,15 @@ -import React, { Fragment, useMemo } from 'react' +import React, { Fragment, useCallback } from 'react' import { useSelector } from 'react-redux' -import { BarChart, Bar, XAxis, ResponsiveContainer, LabelList } from 'recharts' import { groupBy } from 'es-toolkit' -import type { VesselGroupInsightResponse } from '@globalfishingwatch/api-types' +import { type VesselGroupInsightResponse } from '@globalfishingwatch/api-types' +import type { ResponsiveVisualizationData } from '@globalfishingwatch/responsive-visualizations' +import { ResponsiveBarChart } from '@globalfishingwatch/responsive-visualizations' import { COLOR_PRIMARY_BLUE } from 'features/app/app.config' import { selectVGRData } from 'features/reports/vessel-groups/vessel-group-report.slice' import type { VesselGroupVesselIdentity } from 'features/vessel-groups/vessel-groups-modal.slice' import { weightedMean } from 'utils/statistics' +import { formatI18nNumber } from 'features/i18n/i18nNumber' +import VesselGroupReportVesselsIndividualTooltip from 'features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip' import { selectVGRDataview } from '../vessel-group-report.selectors' import styles from './VGRInsightCoverageGraph.module.css' @@ -22,11 +25,6 @@ const CustomTick = (props: any) => { ) } -type VesselGroupReportCoverageGraphData = { - name: string - value: number -} - const COVERAGE_GRAPH_BUCKETS: Record = { '<20%': 20, '20-40%': 40, @@ -42,10 +40,10 @@ function parseCoverageGraphValueBucket(value: number) { ) } -function parseCoverageGraphData( +function parseCoverageGraphAggregatedData( data: VesselGroupInsightResponse['coverage'], vessels: VesselGroupVesselIdentity[] -): VesselGroupReportCoverageGraphData[] { +): ResponsiveVisualizationData<'aggregated'> { if (!data) return [] const groupedData: Record = {} data.forEach((d) => { @@ -70,20 +68,61 @@ function parseCoverageGraphData( const groupedDataByCoverage = groupBy(dataByCoverage, (entry) => entry.value!) return Object.keys(COVERAGE_GRAPH_BUCKETS).map((key) => ({ - name: key, + label: key, value: groupedDataByCoverage[key]?.length || 0, })) } +function parseCoverageGraphIndividualData( + data: VesselGroupInsightResponse['coverage'], + vessels: VesselGroupVesselIdentity[] +): ResponsiveVisualizationData<'individual'> { + if (!data) return [] + const groupedData: Record = {} + data.forEach((d) => { + const vessel = vessels.find((v) => v.vesselId === d.vesselId) + const { relationId } = vessel || {} + if (!relationId) return + if (!groupedData[relationId]) { + groupedData[relationId] = { + name: relationId, + vessel: vessel, + values: [d.percentage], + counts: [parseInt(d.blocks)], + } + } else { + groupedData[relationId].values.push(d.percentage) + groupedData[relationId].counts.push(parseInt(d.blocks)) + } + }) + + const dataByCoverage = Object.values(groupedData).map((d) => ({ + name: d.name, + vessel: d.vessel, + value: parseCoverageGraphValueBucket(weightedMean(d.values, d.counts)), + })) + + const groupedDataByCoverage = groupBy(dataByCoverage, (entry) => entry.value!) + return Object.keys(COVERAGE_GRAPH_BUCKETS).map((key) => ({ + label: key, + values: groupedDataByCoverage[key].map((d) => d.vessel) || [], + })) +} + export default function VesselGroupReportInsightCoverageGraph({ data, }: { data: VesselGroupInsightResponse['coverage'] }) { const vesselGroup = useSelector(selectVGRData) - const dataGrouped = useMemo(() => { + const getIndividualData = useCallback(async () => { + if (vesselGroup?.vessels.length) { + return parseCoverageGraphIndividualData(data, vesselGroup.vessels) + } else return [] + }, [data, vesselGroup?.vessels]) + const getAggregatedData = useCallback(async () => { if (vesselGroup?.vessels.length) { - return parseCoverageGraphData(data, vesselGroup.vessels) + return parseCoverageGraphAggregatedData(data, vesselGroup.vessels) } else return [] }, [data, vesselGroup?.vessels]) @@ -91,37 +130,17 @@ export default function VesselGroupReportInsightCoverageGraph({ return (
      - {dataGrouped && ( - - - - entry.value} /> - - } - tickMargin={0} - /> - - - )} + { + return formatI18nNumber(value).toString() + }} + barLabel={} + labelKey="label" + individualTooltip={} + />
      ) diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx index 0ca9979147..cd068e6a8f 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useCallback, useRef } from 'react' +import React, { Fragment, useCallback } from 'react' import cx from 'classnames' import { useTranslation } from 'react-i18next' import type { @@ -6,10 +6,9 @@ import type { ResponsiveVisualizationInteractionCallback, } from '@globalfishingwatch/responsive-visualizations' import { ResponsiveBarChart } from '@globalfishingwatch/responsive-visualizations' -import { VesselIdentitySourceEnum } from '@globalfishingwatch/api-types' import I18nNumber, { formatI18nNumber } from 'features/i18n/i18nNumber' import { EMPTY_API_VALUES, OTHERS_CATEGORY_LABEL } from 'features/reports/areas/area-reports.config' -import { formatInfoField, getVesselShipTypeLabel } from 'utils/info' +import { formatInfoField } from 'utils/info' import { useLocationConnect } from 'routes/routes.hook' import type { VesselGroupReportState, @@ -19,9 +18,8 @@ import type { import { COLOR_PRIMARY_BLUE } from 'features/app/app.config' import { OTHER_CATEGORY_LABEL } from 'features/reports/vessel-groups/vessel-group-report.config' import type { PortsReportState } from 'features/reports/ports/ports-report.types' -import { getVesselProperty } from 'features/vessel/vessel.utils' +import VesselGroupReportVesselsIndividualTooltip from 'features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip' import styles from './VesselGroupReportVesselsGraph.module.css' -import type { VesselGroupVesselTableParsed } from './vessel-group-report-vessels.selectors' type ReportGraphTooltipProps = { active: boolean @@ -79,29 +77,6 @@ const ReportBarTooltip = (props: any) => { return null } -const ReportPointTooltip = ({ data }: { type: string; data?: VesselGroupVesselTableParsed }) => { - if (!data?.identity) { - return null - } - const getVesselPropertyParams = { - identitySource: VesselIdentitySourceEnum.SelfReported, - } - const vesselName = formatInfoField( - getVesselProperty(data.identity, 'shipname', getVesselPropertyParams), - 'shipname' - ) - - const vesselFlag = getVesselProperty(data.identity, 'flag', getVesselPropertyParams) - - const vesselType = getVesselShipTypeLabel({ - shiptypes: getVesselProperty(data.identity, 'shiptypes', getVesselPropertyParams), - }) - - return ( - {`${vesselName} ${vesselFlag ? `(${vesselFlag})` : ''} ${vesselType ? `- ${vesselType}` : ''}`} - ) -} - const ReportGraphTick = (props: any) => { const { x, y, payload, width, visibleTicksCount, property, filterQueryParam, pageQueryParam } = props @@ -235,7 +210,7 @@ export default function VesselGroupReportVesselsGraph({ /> } labelKey="name" - individualTooltip={} + individualTooltip={} aggregatedTooltip={} />
      diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.tsx new file mode 100644 index 0000000000..1ad16ea12a --- /dev/null +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.tsx @@ -0,0 +1,34 @@ +import { VesselIdentitySourceEnum } from '@globalfishingwatch/api-types' +import { getVesselShipTypeLabel } from 'utils/info' +import { formatInfoField } from 'utils/info' +import type { VesselGroupVesselTableParsed } from 'features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors' +import { getVesselProperty } from 'features/vessel/vessel.utils' + +const VesselGroupReportVesselsIndividualTooltip = ({ + data, +}: { + data?: VesselGroupVesselTableParsed +}) => { + if (!data?.identity) { + return null + } + const getVesselPropertyParams = { + identitySource: VesselIdentitySourceEnum.SelfReported, + } + const vesselName = formatInfoField( + getVesselProperty(data.identity, 'shipname', getVesselPropertyParams), + 'shipname' + ) + + const vesselFlag = getVesselProperty(data.identity, 'flag', getVesselPropertyParams) + + const vesselType = getVesselShipTypeLabel({ + shiptypes: getVesselProperty(data.identity, 'shiptypes', getVesselPropertyParams), + }) + + return ( + {`${vesselName} ${vesselFlag ? `(${vesselFlag})` : ''} ${vesselType ? `- ${vesselType}` : ''}`} + ) +} + +export default VesselGroupReportVesselsIndividualTooltip From 4c93694afa38e3e6059c44fef860037ed5df0520 Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Mon, 13 Jan 2025 13:36:23 +0100 Subject: [PATCH 16/62] reuse vessel grouping --- .../insights/VGRInsightCoverageGraph.tsx | 47 +++++++------------ 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx index 4c7db63707..8eaeba051a 100644 --- a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx @@ -40,18 +40,20 @@ function parseCoverageGraphValueBucket(value: number) { ) } -function parseCoverageGraphAggregatedData( +function getDataByCoverage( data: VesselGroupInsightResponse['coverage'], vessels: VesselGroupVesselIdentity[] -): ResponsiveVisualizationData<'aggregated'> { - if (!data) return [] +): Record { + if (!data) return {} const groupedData: Record = {} data.forEach((d) => { - const relationId = vessels.find((v) => v.vesselId === d.vesselId)?.relationId + const vessel = vessels.find((v) => v.vesselId === d.vesselId) + const { relationId } = vessel || {} if (!relationId) return if (!groupedData[relationId]) { groupedData[relationId] = { name: relationId, + vessel, values: [d.percentage], counts: [parseInt(d.blocks)], } @@ -63,10 +65,18 @@ function parseCoverageGraphAggregatedData( const dataByCoverage = Object.values(groupedData).map((d) => ({ name: d.name, + vessel: d.vessel, value: parseCoverageGraphValueBucket(weightedMean(d.values, d.counts)), })) - const groupedDataByCoverage = groupBy(dataByCoverage, (entry) => entry.value!) + return groupBy(dataByCoverage, (entry) => entry.value!) +} + +function parseCoverageGraphAggregatedData( + data: VesselGroupInsightResponse['coverage'], + vessels: VesselGroupVesselIdentity[] +): ResponsiveVisualizationData<'aggregated'> { + const groupedDataByCoverage = getDataByCoverage(data, vessels) return Object.keys(COVERAGE_GRAPH_BUCKETS).map((key) => ({ label: key, value: groupedDataByCoverage[key]?.length || 0, @@ -77,32 +87,7 @@ function parseCoverageGraphIndividualData( data: VesselGroupInsightResponse['coverage'], vessels: VesselGroupVesselIdentity[] ): ResponsiveVisualizationData<'individual'> { - if (!data) return [] - const groupedData: Record = {} - data.forEach((d) => { - const vessel = vessels.find((v) => v.vesselId === d.vesselId) - const { relationId } = vessel || {} - if (!relationId) return - if (!groupedData[relationId]) { - groupedData[relationId] = { - name: relationId, - vessel: vessel, - values: [d.percentage], - counts: [parseInt(d.blocks)], - } - } else { - groupedData[relationId].values.push(d.percentage) - groupedData[relationId].counts.push(parseInt(d.blocks)) - } - }) - - const dataByCoverage = Object.values(groupedData).map((d) => ({ - name: d.name, - vessel: d.vessel, - value: parseCoverageGraphValueBucket(weightedMean(d.values, d.counts)), - })) - - const groupedDataByCoverage = groupBy(dataByCoverage, (entry) => entry.value!) + const groupedDataByCoverage = getDataByCoverage(data, vessels) return Object.keys(COVERAGE_GRAPH_BUCKETS).map((key) => ({ label: key, values: groupedDataByCoverage[key].map((d) => d.vessel) || [], From f23b25a382f6a851312a97a6e73d2eb80e105151 Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Mon, 13 Jan 2025 13:36:52 +0100 Subject: [PATCH 17/62] match covegare opacity in individual graph --- .../insights/VGRInsightCoverageGraph.module.css | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.module.css b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.module.css index 1220ca1beb..4b56616e13 100644 --- a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.module.css +++ b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.module.css @@ -38,18 +38,22 @@ transition: fill 300ms linear; } -.graph :global(.recharts-bar-rectangle):nth-child(1) { +.graph :global(.recharts-bar-rectangle):nth-child(1), +.graph foreignObject > div > div:nth-child(1) { opacity: 0.3; } -.graph :global(.recharts-bar-rectangle):nth-child(2) { +.graph :global(.recharts-bar-rectangle):nth-child(2), +.graph foreignObject > div > div:nth-child(2) { opacity: 0.5; } -.graph :global(.recharts-bar-rectangle):nth-child(3) { +.graph :global(.recharts-bar-rectangle):nth-child(3), +.graph foreignObject > div > div:nth-child(3) { opacity: 0.7; } -.graph :global(.recharts-bar-rectangle):nth-child(4) { +.graph :global(.recharts-bar-rectangle):nth-child(4), +.graph foreignObject > div > div:nth-child(4) { opacity: 0.85; } From c8710eab6e33008c4bb98c5a66194ac8b4e2578b Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Mon, 13 Jan 2025 14:12:22 +0100 Subject: [PATCH 18/62] fix crashes and warnings --- .../insights/VGRInsightCoverageGraph.tsx | 2 +- .../charts/barchart/BarChartIndividual.tsx | 4 +-- .../src/lib/density.ts | 36 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx index 8eaeba051a..0bb9a4865b 100644 --- a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx @@ -90,7 +90,7 @@ function parseCoverageGraphIndividualData( const groupedDataByCoverage = getDataByCoverage(data, vessels) return Object.keys(COVERAGE_GRAPH_BUCKETS).map((key) => ({ label: key, - values: groupedDataByCoverage[key].map((d) => d.vessel) || [], + values: (groupedDataByCoverage[key] || []).map((d) => d.vessel), })) } diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx index 5d8415a2fa..c84084fe83 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx @@ -36,11 +36,11 @@ export function IndividualBarChart({ {data.map((item, index) => { const points = item?.[valueKey] as ResponsiveVisualizationItem[] return ( -
      +
      -
        +
          {points?.map((point, pointIndex) => ( { + return data.reduce((acc, column) => { + const value = (column as ResponsiveVisualizationIndividualItem)[aggregatedValueKey] + ? (column as ResponsiveVisualizationIndividualItem)[aggregatedValueKey] + : (column as ResponsiveVisualizationAggregatedItem)[individualValueKey]?.length || 0 + if (value > acc) { + return value + } + return acc + }, 0) +} + type IsIndividualSupportedParams = { data: ResponsiveVisualizationData width: number @@ -32,15 +48,7 @@ export function getIsIndividualBarChartSupported({ individualValueKey, }: IsIndividualSupportedParams): boolean { const { pointsByRow } = getBarProps(data, width) - const biggestColumnValue = data.reduce((acc, column) => { - const value = (column as ResponsiveVisualizationIndividualItem)[aggregatedValueKey] - ? (column as ResponsiveVisualizationIndividualItem)[aggregatedValueKey] - : (column as ResponsiveVisualizationAggregatedItem)[individualValueKey].length - if (value > acc) { - return value - } - return acc - }, 0) + const biggestColumnValue = getBiggestColumnValue(data, aggregatedValueKey, individualValueKey) const rowsInBiggestColumn = Math.ceil(biggestColumnValue / pointsByRow) const heightNeeded = rowsInBiggestColumn * POINT_SIZE return heightNeeded < height - AXIS_LABEL_PADDING - COLUMN_PADDING - COLUMN_LABEL_SIZE @@ -60,15 +68,7 @@ export function getIsIndividualTimeseriesSupported({ aggregatedValueKey, individualValueKey, }: getIsIndividualTimeseriesSupportedParams): boolean { - const biggestColumnValue = data.reduce((acc, column) => { - const value = (column as ResponsiveVisualizationIndividualItem)[aggregatedValueKey] - ? (column as ResponsiveVisualizationIndividualItem)[aggregatedValueKey] - : (column as ResponsiveVisualizationAggregatedItem)[individualValueKey].length - if (value > acc) { - return value - } - return acc - }, 0) + const biggestColumnValue = getBiggestColumnValue(data, aggregatedValueKey, individualValueKey) const heightNeeded = biggestColumnValue * POINT_SIZE return heightNeeded < height - AXIS_LABEL_PADDING } From 70df1bfcc9a866a7ffccf63d8b34276575de7821 Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Mon, 13 Jan 2025 14:15:48 +0100 Subject: [PATCH 19/62] make opacity change only the list --- .../insights/VGRInsightCoverageGraph.module.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.module.css b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.module.css index 4b56616e13..462e547d88 100644 --- a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.module.css +++ b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.module.css @@ -39,21 +39,21 @@ } .graph :global(.recharts-bar-rectangle):nth-child(1), -.graph foreignObject > div > div:nth-child(1) { +.graph foreignObject > div > div:nth-child(1) ul { opacity: 0.3; } .graph :global(.recharts-bar-rectangle):nth-child(2), -.graph foreignObject > div > div:nth-child(2) { +.graph foreignObject > div > div:nth-child(2) ul { opacity: 0.5; } .graph :global(.recharts-bar-rectangle):nth-child(3), -.graph foreignObject > div > div:nth-child(3) { +.graph foreignObject > div > div:nth-child(3) ul { opacity: 0.7; } .graph :global(.recharts-bar-rectangle):nth-child(4), -.graph foreignObject > div > div:nth-child(4) { +.graph foreignObject > div > div:nth-child(4) ul { opacity: 0.85; } From a8627a923b5f625a54b5bb1af53c92ff83301e2c Mon Sep 17 00:00:00 2001 From: j8seangel Date: Mon, 13 Jan 2025 19:16:36 +0100 Subject: [PATCH 20/62] individual point events --- .../features/reports/ports/PortsReport.tsx | 4 +- .../shared/events/EventsReportGraph.tsx | 52 ++++++++++--- .../vessel-groups/events/VGREvents.tsx | 29 ++++---- apps/fishing-map/utils/dates.ts | 18 +++++ .../src/charts/barchart/BarChart.tsx | 11 ++- .../barchart/BarChartIndividual.module.css | 1 - .../charts/barchart/BarChartIndividual.tsx | 4 +- .../src/charts/config.ts | 9 ++- .../src/charts/hooks.ts | 48 ++++++------ .../charts/points/IndividualPoint.module.css | 8 +- .../src/charts/points/IndividualPoint.tsx | 10 ++- .../src/charts/timeseries/Timeseries.tsx | 9 ++- .../timeseries/TimeseriesAggregated.tsx | 71 +++++++----------- .../TimeseriesIndividual.module.css | 5 -- .../timeseries/TimeseriesIndividual.tsx | 72 ++++++------------ .../src/charts/timeseries/timeseries.hooks.ts | 73 +++++++++++++++++++ .../src/charts/types.ts | 2 +- .../src/lib/density.ts | 34 ++++++++- 18 files changed, 296 insertions(+), 164 deletions(-) create mode 100644 libs/responsive-visualizations/src/charts/timeseries/timeseries.hooks.ts diff --git a/apps/fishing-map/features/reports/ports/PortsReport.tsx b/apps/fishing-map/features/reports/ports/PortsReport.tsx index 4ec32697ad..4c8f56ee63 100644 --- a/apps/fishing-map/features/reports/ports/PortsReport.tsx +++ b/apps/fishing-map/features/reports/ports/PortsReport.tsx @@ -4,8 +4,8 @@ import { Fragment } from 'react' import { useTranslation } from 'react-i18next' import parse from 'html-react-parser' import { DateTime } from 'luxon' -import { Button } from '@globalfishingwatch/ui-components' import { useGetReportEventsStatsQuery } from 'queries/report-events-stats-api' +import { Button } from '@globalfishingwatch/ui-components' import EventsReportGraph from 'features/reports/shared/events/EventsReportGraph' import { selectReportPortId } from 'routes/routes.selectors' import EventsReportVesselPropertySelector from 'features/reports/shared/events/EventsReportVesselPropertySelector' @@ -155,6 +155,8 @@ function PortsReport() { color={color} start={start} end={end} + filters={{ 'port-ids': [portId] }} + datasetId={datasetId} timeseries={data.timeseries || []} /> )} diff --git a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx index 42f975c4a9..f1291af700 100644 --- a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx +++ b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx @@ -1,12 +1,19 @@ import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import { type FourwingsInterval } from '@globalfishingwatch/deck-loaders' +import { DateTime } from 'luxon' +import { groupBy } from 'es-toolkit' +import { stringify } from 'qs' +import { getFourwingsInterval, type FourwingsInterval } from '@globalfishingwatch/deck-loaders' import type { BaseResponsiveTimeseriesProps } from '@globalfishingwatch/responsive-visualizations' import { ResponsiveTimeseries } from '@globalfishingwatch/responsive-visualizations' +import { GFWAPI } from '@globalfishingwatch/api-client' +import type { ApiEvent, APIPagination } from '@globalfishingwatch/api-types' +import { useMemoCompare } from '@globalfishingwatch/react-hooks' import i18n from 'features/i18n/i18n' -import { formatDateForInterval, getUTCDateTime } from 'utils/dates' +import { formatDateForInterval, getISODateByInterval, getUTCDateTime } from 'utils/dates' import { formatI18nNumber } from 'features/i18n/i18nNumber' import { COLOR_PRIMARY_BLUE } from 'features/app/app.config' +import { formatInfoField } from 'utils/info' import styles from './EventsReportGraph.module.css' type EventsReportGraphTooltipProps = { @@ -44,7 +51,10 @@ const AggregatedGraphTooltip = (props: any) => { } const IndividualGraphTooltip = ({ data }: { data?: any }) => { - return data.label + if (!data?.vessel?.name) { + return null + } + return formatInfoField(data.vessel.name, 'shipname') } const formatDateTicks: BaseResponsiveTimeseriesProps['tickLabelFormatter'] = ( @@ -56,11 +66,15 @@ const formatDateTicks: BaseResponsiveTimeseriesProps['tickLabelFormatter'] = ( } export default function EventsReportGraph({ + datasetId, + filters, color = COLOR_PRIMARY_BLUE, end, start, timeseries, }: { + datasetId: string + filters?: Record color?: string end: string start: string @@ -69,13 +83,31 @@ export default function EventsReportGraph({ const containerRef = React.useRef(null) const { t } = useTranslation() + const startMillis = DateTime.fromISO(start).toMillis() + const endMillis = DateTime.fromISO(end).toMillis() + const interval = getFourwingsInterval(startMillis, endMillis) + const filtersMemo = useMemoCompare(filters) + const getAggregatedData = useCallback(async () => timeseries, [timeseries]) const getIndividualData = useCallback(async () => { - return timeseries.map((t) => ({ - date: t.date, - values: [...new Array(t.value)].map((v, i) => ({ label: t.date, value: i })), - })) - }, [timeseries]) + // TODO add includes to fetch only the information needed + const params = { + 'start-date': start, + 'end-date': end, + 'time-filter-mode': 'START-DATE', + ...(filtersMemo && { ...filtersMemo }), + datasets: [datasetId], + limit: 1000, + offset: 0, + } + const data = await GFWAPI.fetch>(`/v3/events?${stringify(params)}`) + const groupedData = groupBy(data.entries, (item) => + getISODateByInterval(DateTime.fromISO(item.start as string), interval) + ) + return Object.entries(groupedData) + .map(([date, events]) => ({ date, values: events })) + .sort((a, b) => a.date.localeCompare(b.date)) + }, [datasetId, end, interval, filtersMemo, start]) if (!timeseries.length) { return null @@ -87,11 +119,11 @@ export default function EventsReportGraph({ } + aggregatedTooltip={} individualTooltip={} color={color} /> diff --git a/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx b/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx index b0928026e2..2099561a0e 100644 --- a/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx @@ -16,7 +16,7 @@ import type { import { Icon } from '@globalfishingwatch/ui-components' import { DatasetTypes } from '@globalfishingwatch/api-types' import VGREventsSubsectionSelector from 'features/reports/vessel-groups/events/VGREventsSubsectionSelector' -import VGREventsGraph from 'features/reports/shared/events/EventsReportGraph' +import EventsReportGraph from 'features/reports/shared/events/EventsReportGraph' import { selectVGREventsVesselFilter, selectVGREventsVesselsProperty, @@ -129,9 +129,8 @@ function VGREvents() { ) } - const subCategoryDataset = eventsDataview?.datasets?.find( - (d) => d.type === DatasetTypes.Events - )?.subcategory + const eventDataset = eventsDataview?.datasets?.find((d) => d.type === DatasetTypes.Events) + const subCategoryDatasetCategory = eventDataset?.subcategory const totalEvents = data.timeseries.reduce((acc, group) => acc + group.value, 0) return ( @@ -150,10 +149,10 @@ function VGREvents() { flags: vesselFlags?.size, activityQuantity: totalEvents, activityUnit: `${ - subCategoryDataset !== undefined + subCategoryDatasetCategory !== undefined ? t( - `common.eventLabels.${subCategoryDataset.toLowerCase()}`, - lowerCase(subCategoryDataset) + `common.eventLabels.${subCategoryDatasetCategory.toLowerCase()}`, + lowerCase(subCategoryDatasetCategory) ) : '' } ${(t('common.events', 'events') as string).toLowerCase()}`, @@ -166,12 +165,16 @@ function VGREvents() { }) )} - + {eventDataset?.id && ( + + )}
      diff --git a/apps/fishing-map/utils/dates.ts b/apps/fishing-map/utils/dates.ts index 917198eab2..5e0082178c 100644 --- a/apps/fishing-map/utils/dates.ts +++ b/apps/fishing-map/utils/dates.ts @@ -24,6 +24,24 @@ export const getUTCDateTime = (d: SupportedDateType) => { return DateTime.fromMillis(d, { zone: 'utc' }) } +export const getISODateByInterval = (date: DateTime, timeChunkInterval: FourwingsInterval) => { + if (!date) { + return '' + } + switch (timeChunkInterval) { + case 'YEAR': + return date.toFormat('yyyy') as string + case 'MONTH': + return date.toFormat('yyyy-MM') as string + case 'HOUR': + return date.toFormat('yyyy-MM-ddTHH:00:00') as string + case 'DAY': + return date.toFormat('yyyy-MM-dd') as string + default: + return date.toISO() as string + } +} + export const formatDateForInterval = (date: DateTime, timeChunkInterval: FourwingsInterval) => { let formattedTick = '' switch (timeChunkInterval) { diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx index 3623a65b8a..609d1d920f 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx @@ -1,7 +1,11 @@ import { useRef } from 'react' import type { ResponsiveVisualizationData } from '../../types' import { getIsIndividualBarChartSupported } from '../../lib/density' -import type { BaseResponsiveBarChartProps, BaseResponsiveChartProps } from '../types' +import type { + BaseResponsiveBarChartProps, + BaseResponsiveChartProps, + ResponsiveVisualizationAnyItemKey, +} from '../types' import { DEFAULT_AGGREGATED_VALUE_KEY, DEFAULT_INDIVIDUAL_VALUE_KEY, @@ -13,7 +17,7 @@ import { AggregatedBarChart } from './BarChartAggregated' import styles from './BarChart.module.css' type ResponsiveBarChartProps = BaseResponsiveChartProps & - BaseResponsiveBarChartProps & { labelKey?: string } + BaseResponsiveBarChartProps & { labelKey?: ResponsiveVisualizationAnyItemKey } export function ResponsiveBarChart({ getIndividualData, @@ -30,8 +34,7 @@ export function ResponsiveBarChart({ onAggregatedItemClick, }: ResponsiveBarChartProps) { const containerRef = useRef(null) - const { data, isIndividualSupported } = useResponsiveVisualization({ - containerRef, + const { data, isIndividualSupported } = useResponsiveVisualization(containerRef, { labelKey, aggregatedValueKey, individualValueKey, diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.module.css b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.module.css index fbac2183ed..9cde90289f 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.module.css +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.module.css @@ -17,7 +17,6 @@ .bar { display: flex; - gap: 3px; flex-wrap: wrap-reverse; justify-content: center; } diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx index c84084fe83..a2ac2a06b1 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx @@ -3,7 +3,7 @@ import React from 'react' import type { ResponsiveVisualizationItem } from '../../types' import type { BarChartByTypeProps } from '../types' import { IndividualPoint } from '../points/IndividualPoint' -import { AXIS_LABEL_PADDING } from '../config' +import { AXIS_LABEL_PADDING, POINT_GAP } from '../config' import styles from './BarChartIndividual.module.css' type IndividualBarChartProps = BarChartByTypeProps<'individual'> @@ -40,7 +40,7 @@ export function IndividualBarChart({ -
        +
          {points?.map((point, pointIndex) => ( export function useResponsiveDimensions(containerRef: ResponsiveVisualizationContainerRef) { const [dimensions, setDimensions] = useState({ width: 0, height: 0 }) @@ -41,6 +39,9 @@ export function useResponsiveDimensions(containerRef: ResponsiveVisualizationCon } type UseResponsiveVisualizationDataProps = { + start?: string + end?: string + timeseriesInterval?: FourwingsInterval labelKey: ResponsiveVisualizationAnyItemKey individualValueKey: BaseResponsiveChartProps['individualValueKey'] aggregatedValueKey: BaseResponsiveChartProps['aggregatedValueKey'] @@ -54,6 +55,9 @@ export function useResponsiveVisualizationData({ labelKey = DEFAULT_LABEL_KEY, individualValueKey = DEFAULT_INDIVIDUAL_VALUE_KEY, aggregatedValueKey = DEFAULT_AGGREGATED_VALUE_KEY, + start, + end, + timeseriesInterval, getAggregatedData, getIndividualData, getIsIndividualSupported, @@ -74,6 +78,9 @@ export function useResponsiveVisualizationData({ data: aggregatedData, width, height, + start, + end, + timeseriesInterval, individualValueKey, aggregatedValueKey, }) @@ -100,6 +107,9 @@ export function useResponsiveVisualizationData({ data: individualData, width, height, + start, + end, + timeseriesInterval, individualValueKey, aggregatedValueKey, }) @@ -123,6 +133,9 @@ export function useResponsiveVisualizationData({ getAggregatedData, getIndividualData, getIsIndividualSupported, + start, + end, + timeseriesInterval, individualValueKey, aggregatedValueKey, labelKey, @@ -135,27 +148,12 @@ export function useResponsiveVisualizationData({ ) } -type UseResponsiveVisualizationProps = { - containerRef: ResponsiveVisualizationContainerRef -} & UseResponsiveVisualizationDataProps -export function useResponsiveVisualization({ - containerRef, - labelKey, - aggregatedValueKey, - individualValueKey, - getAggregatedData, - getIndividualData, - getIsIndividualSupported, -}: UseResponsiveVisualizationProps) { +export function useResponsiveVisualization( + containerRef: ResponsiveVisualizationContainerRef, + params: UseResponsiveVisualizationDataProps +) { const dimensions = useResponsiveDimensions(containerRef) - const { data, isIndividualSupported, loadData } = useResponsiveVisualizationData({ - labelKey, - aggregatedValueKey, - individualValueKey, - getAggregatedData, - getIndividualData, - getIsIndividualSupported, - }) + const { data, isIndividualSupported, loadData } = useResponsiveVisualizationData(params) useEffect(() => { if (dimensions.width && dimensions.height) { diff --git a/libs/responsive-visualizations/src/charts/points/IndividualPoint.module.css b/libs/responsive-visualizations/src/charts/points/IndividualPoint.module.css index cbd90bfac1..585ec0051e 100644 --- a/libs/responsive-visualizations/src/charts/points/IndividualPoint.module.css +++ b/libs/responsive-visualizations/src/charts/points/IndividualPoint.module.css @@ -1,14 +1,14 @@ .point { display: block; - width: 12px; - height: 12px; background-color: red; - border-radius: 6px; + border-radius: 50%; position: relative; + border: 2px solid transparent; + transition: border-color 0.3s linear; } .point:hover { - border: 1px solid black; + border: var(--border-thick); } .tooltip { diff --git a/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx b/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx index 2fd2697101..12b61d2594 100644 --- a/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx +++ b/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx @@ -2,6 +2,7 @@ import { useFloating, offset, flip, shift, useInteractions, useHover, FloatingPo import { cloneElement, useState, type ReactElement } from 'react' import cx from 'classnames' import type { ResponsiveVisualizationItem } from '../../types' +import { POINT_SIZE } from '../config' import styles from './IndividualPoint.module.css' type IndividualPointProps = { @@ -23,12 +24,19 @@ export function IndividualPoint({ point, color, tooltip, className }: Individual const hover = useHover(context) const { getReferenceProps, getFloatingProps } = useInteractions([hover]) + return (
        • {isOpen && ( diff --git a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx index 47e874a8fb..0f767da93a 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx @@ -25,6 +25,7 @@ export function ResponsiveTimeseries({ dateKey = DEFAULT_DATE_KEY, aggregatedValueKey = DEFAULT_AGGREGATED_VALUE_KEY, individualValueKey = DEFAULT_INDIVIDUAL_VALUE_KEY, + timeseriesInterval, getIndividualData, getAggregatedData, color, @@ -35,8 +36,10 @@ export function ResponsiveTimeseries({ onAggregatedItemClick, }: ResponsiveTimeseriesProps) { const containerRef = useRef(null) - const { width, data, isIndividualSupported } = useResponsiveVisualization({ - containerRef, + const { width, data, isIndividualSupported } = useResponsiveVisualization(containerRef, { + start, + end, + timeseriesInterval, labelKey: dateKey, individualValueKey, aggregatedValueKey, @@ -62,6 +65,7 @@ export function ResponsiveTimeseries({ end={end} color={color} dateKey={dateKey} + timeseriesInterval={timeseriesInterval} valueKey={individualValueKey} onClick={onIndividualItemClick} tickLabelFormatter={tickLabelFormatter} @@ -74,6 +78,7 @@ export function ResponsiveTimeseries({ end={end} color={color} dateKey={dateKey} + timeseriesInterval={timeseriesInterval} valueKey={aggregatedValueKey} onClick={onAggregatedItemClick} tickLabelFormatter={tickLabelFormatter} diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx index 067ecacf98..a3da63e7d4 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx @@ -1,11 +1,16 @@ -import { XAxis, ResponsiveContainer, YAxis, CartesianGrid, ComposedChart, Line } from 'recharts' +import { + XAxis, + ResponsiveContainer, + YAxis, + CartesianGrid, + ComposedChart, + Line, + Tooltip, +} from 'recharts' import min from 'lodash/min' import max from 'lodash/max' -import type { DurationUnit } from 'luxon' -import { DateTime, Duration } from 'luxon' -import { useMemo } from 'react' -import { getFourwingsInterval } from '@globalfishingwatch/deck-loaders' import type { TimeseriesByTypeProps } from '../types' +import { useFullTimeseries, useTimeseriesDomain } from './timeseries.hooks' const graphMargin = { top: 0, right: 0, left: -20, bottom: -10 } @@ -17,16 +22,10 @@ export function AggregatedTimeseries({ end, dateKey, valueKey, + customTooltip, + timeseriesInterval, tickLabelFormatter, }: AggregatedTimeseriesProps) { - const startMillis = DateTime.fromISO(start).toMillis() - const endMillis = DateTime.fromISO(end).toMillis() - const interval = getFourwingsInterval(startMillis, endMillis) - - const intervalDiff = Math.floor( - Duration.fromMillis(endMillis - startMillis).as(interval.toLowerCase() as DurationUnit) - ) - const dataMin: number = data.length ? (min(data.map((item) => item[valueKey])) as number) : 0 const dataMax: number = data.length ? (max(data.map((item) => item[valueKey])) as number) : 0 @@ -36,42 +35,29 @@ export function AggregatedTimeseries({ Math.ceil(dataMax + domainPadding), ] - const domain = useMemo(() => { - if (start && end && interval) { - const cleanEnd = DateTime.fromISO(end, { zone: 'utc' }) - .minus({ [interval]: 1 }) - .toISO() as string - return [new Date(start).getTime(), new Date(cleanEnd).getTime()] - } - }, [start, end, interval]) + const domain = useTimeseriesDomain({ start, end, timeseriesInterval }) + const fullTimeseries = useFullTimeseries({ + start, + end, + data, + timeseriesInterval, + dateKey, + valueKey, + }) - const fullTimeseries = useMemo(() => { - if (!data || !domain) { - return [] - } - return Array(intervalDiff) - .fill(0) - .map((_, i) => i) - .map((i) => { - const d = DateTime.fromMillis(startMillis, { zone: 'UTC' }) - .plus({ [interval]: i }) - .toISO() - return { - [dateKey]: d, - [valueKey]: data.find((item) => item[dateKey] === d)?.[valueKey] || 0, - } - }) - }, [data, domain, intervalDiff, startMillis, interval, valueKey, dateKey]) + if (!fullTimeseries.length) { + return null + } return ( - + tickLabelFormatter?.(tick, interval) || tick} + tickFormatter={(tick: string) => tickLabelFormatter?.(tick, timeseriesInterval) || tick} axisLine={paddedDomain[0] === 0} /> - {/* {fullTimeseries?.length && ( - } /> - )} */} + {data?.length && customTooltip ? : null} { - if (start && end && interval) { - const cleanEnd = DateTime.fromISO(end, { zone: 'utc' }) - .minus({ [interval]: 1 }) - .toISO() as string - return [new Date(start).getTime(), new Date(cleanEnd).getTime()] - } - }, [start, end, interval]) - - // const fullTimeseries = useMemo(() => { - // if (!data || !domain) { - // return [] - // } - // return Array(intervalDiff) - // .fill(0) - // .map((_, i) => i) - // .map((i) => { - // const d = DateTime.fromMillis(startMillis, { zone: 'UTC' }) - // .plus({ [interval]: i }) - // .toISO() - // return { - // [dateKey]: d, - // [valueKey]: data.find((item) => item[dateKey] === d)?.[valueKey] || 0, - // } - // }) - // }, [data, domain, intervalDiff, startMillis, interval, dateKey, valueKey]) + const domain = useTimeseriesDomain({ start, end, timeseriesInterval }) + const fullTimeseries = useFullTimeseries({ + start, + end, + data, + timeseriesInterval, + dateKey, + valueKey, + aggregated: false, + }) return ( - + {/* */} tickLabelFormatter?.(tick, interval) || tick} + tickFormatter={(tick: string) => tickLabelFormatter?.(tick, timeseriesInterval) || tick} axisLine={true} /> {/* TODO: restore this and align with the points */} @@ -82,16 +57,17 @@ export function IndividualTimeseries({ tickLine={false} tickCount={4} /> */} - {/* {fullTimeseries?.length && ( - } /> - )} */} + {fullTimeseries?.length && } -
          - {data.map((item, index) => { +
          + {fullTimeseries.map((item, index) => { const points = item?.[valueKey] as ResponsiveVisualizationItem[] return ( -
          -
            +
            +
              {points?.map((point, pointIndex) => ( { + if (start && end && timeseriesInterval) { + const cleanEnd = DateTime.fromISO(end, { zone: 'utc' }) + .minus({ [timeseriesInterval]: 1 }) + .toISO() as string + return [new Date(start).getTime(), new Date(cleanEnd).getTime()] + } + return [] + }, [start, end, timeseriesInterval]) +} + +type UseFullTimeseriesProps = { + start: string + end: string + data: ResponsiveVisualizationData + timeseriesInterval: FourwingsInterval + dateKey: ResponsiveVisualizationAnyItemKey + valueKey: ResponsiveVisualizationAnyItemKey + aggregated?: boolean +} +export function useFullTimeseries({ + start, + end, + data, + timeseriesInterval, + dateKey, + valueKey, + aggregated = true, +}: UseFullTimeseriesProps) { + return useMemo(() => { + if (!data) { + return [] + } + + const startMillis = DateTime.fromISO(start).toMillis() + const endMillis = DateTime.fromISO(end).toMillis() + + const intervalDiff = Math.floor( + Duration.fromMillis(endMillis - startMillis).as( + timeseriesInterval.toLowerCase() as DurationUnit + ) + ) + + return Array(intervalDiff) + .fill(0) + .map((_, i) => { + const d = DateTime.fromMillis(startMillis, { zone: 'UTC' }) + .plus({ [timeseriesInterval]: i }) + .toISO() + const dataValue = data.find((item) => d?.startsWith(item[dateKey]))?.[valueKey] + return { + [dateKey]: d, + [valueKey]: dataValue ? dataValue : aggregated ? 0 : [], + } + }) + }, [aggregated, data, dateKey, end, start, timeseriesInterval, valueKey]) +} diff --git a/libs/responsive-visualizations/src/charts/types.ts b/libs/responsive-visualizations/src/charts/types.ts index d3620602e8..a02db32c9a 100644 --- a/libs/responsive-visualizations/src/charts/types.ts +++ b/libs/responsive-visualizations/src/charts/types.ts @@ -11,7 +11,6 @@ export type ResponsiveVisualizationInteractionCallback void -export type ResponsiveVisualizationContainerRef = React.RefObject export type BaseResponsiveChartProps = { // Aggregated props aggregatedTooltip?: ReactElement @@ -51,6 +50,7 @@ export type BaseResponsiveTimeseriesProps = { start: string end: string color: string + timeseriesInterval: FourwingsInterval tickLabelFormatter?: (item: string, interval: FourwingsInterval) => string } diff --git a/libs/responsive-visualizations/src/lib/density.ts b/libs/responsive-visualizations/src/lib/density.ts index 7a8a91438c..5b556d106a 100644 --- a/libs/responsive-visualizations/src/lib/density.ts +++ b/libs/responsive-visualizations/src/lib/density.ts @@ -1,5 +1,14 @@ +import type { DurationUnit } from 'luxon' +import { DateTime, Duration } from 'luxon' +import type { FourwingsInterval } from '@globalfishingwatch/deck-loaders' import type { ResponsiveVisualizationAnyItemKey } from '../charts' -import { COLUMN_PADDING, POINT_SIZE, AXIS_LABEL_PADDING, COLUMN_LABEL_SIZE } from '../charts/config' +import { + COLUMN_PADDING, + POINT_SIZE, + AXIS_LABEL_PADDING, + COLUMN_LABEL_SIZE, + TIMESERIES_PADDING, +} from '../charts/config' import type { ResponsiveVisualizationAggregatedItem, ResponsiveVisualizationData, @@ -56,6 +65,9 @@ export function getIsIndividualBarChartSupported({ type getIsIndividualTimeseriesSupportedParams = { data: ResponsiveVisualizationData + start?: string + end?: string + timeseriesInterval?: FourwingsInterval width: number height: number aggregatedValueKey: string @@ -64,11 +76,29 @@ type getIsIndividualTimeseriesSupportedParams = { export function getIsIndividualTimeseriesSupported({ data, + width, height, + start, + end, + timeseriesInterval, aggregatedValueKey, individualValueKey, }: getIsIndividualTimeseriesSupportedParams): boolean { const biggestColumnValue = getBiggestColumnValue(data, aggregatedValueKey, individualValueKey) const heightNeeded = biggestColumnValue * POINT_SIZE - return heightNeeded < height - AXIS_LABEL_PADDING + const matchesHeight = heightNeeded < height - AXIS_LABEL_PADDING + if (!matchesHeight) { + return false + } + if (start && end && timeseriesInterval) { + const startMillis = DateTime.fromISO(start).toMillis() + const endMillis = DateTime.fromISO(end).toMillis() + const intervalDiff = Math.floor( + Duration.fromMillis(endMillis - startMillis).as( + timeseriesInterval.toLowerCase() as DurationUnit + ) + ) + return intervalDiff * POINT_SIZE <= width - TIMESERIES_PADDING * 2 + } + return true } From 344d962cf7fda89bf6f93e9c40f9a16903c84aca Mon Sep 17 00:00:00 2001 From: j8seangel Date: Mon, 13 Jan 2025 20:17:07 +0100 Subject: [PATCH 21/62] fix typing --- .../vessels/VesselGroupReportVesselsGraph.tsx | 2 +- .../vessel-group-report-vessels.selectors.ts | 13 ++++++++++--- .../src/charts/barchart/BarChart.tsx | 12 ++++++------ .../src/charts/barchart/BarChartAggregated.tsx | 2 +- .../src/charts/hooks.ts | 10 +++++----- .../src/charts/timeseries/Timeseries.tsx | 10 ++++++---- .../charts/timeseries/TimeseriesAggregated.tsx | 4 ++-- .../charts/timeseries/TimeseriesIndividual.tsx | 4 ++-- .../src/charts/timeseries/timeseries.hooks.ts | 4 ++-- .../src/charts/types.ts | 6 ++---- .../src/lib/density.ts | 18 ++++++++---------- 11 files changed, 45 insertions(+), 40 deletions(-) diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx index cd068e6a8f..130c90007e 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx @@ -209,7 +209,7 @@ export default function VesselGroupReportVesselsGraph({ pageQueryParam={pageQueryParam} /> } - labelKey="name" + labelKey={'name'} individualTooltip={} aggregatedTooltip={} /> diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts b/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts index f635904ca1..e1094fa46c 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts @@ -277,7 +277,10 @@ export const selectVGRVesselsGraphIndividualData = createSelector( case 'source': vesselsGrouped = groupBy(vessels, (vessel) => vessel.source) } - const orderedGroups: ResponsiveVisualizationData<'individual'> = Object.entries(vesselsGrouped) + const orderedGroups: ResponsiveVisualizationData< + 'individual', + { name: string; values: any[] } + > = Object.entries(vesselsGrouped) .map(([key, value]) => ({ name: key, values: value as any[], @@ -285,8 +288,12 @@ export const selectVGRVesselsGraphIndividualData = createSelector( .sort((a, b) => { return b.values.length - a.values.length }) - const groupsWithoutOther: ResponsiveVisualizationData<'individual'> = [] - const otherGroups: ResponsiveVisualizationData<'individual'> = [] + const groupsWithoutOther: ResponsiveVisualizationData< + 'individual', + { name: string; values: any[] } + > = [] + const otherGroups: ResponsiveVisualizationData<'individual', { name: string; values: any[] }> = + [] orderedGroups.forEach((group) => { if ( group.name === 'null' || diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx index 609d1d920f..cb6579c870 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx @@ -17,14 +17,14 @@ import { AggregatedBarChart } from './BarChartAggregated' import styles from './BarChart.module.css' type ResponsiveBarChartProps = BaseResponsiveChartProps & - BaseResponsiveBarChartProps & { labelKey?: ResponsiveVisualizationAnyItemKey } + BaseResponsiveBarChartProps & { labelKey?: ResponsiveVisualizationAnyItemKey | string } export function ResponsiveBarChart({ getIndividualData, getAggregatedData, color, - aggregatedValueKey = DEFAULT_AGGREGATED_VALUE_KEY, - individualValueKey = DEFAULT_INDIVIDUAL_VALUE_KEY, + aggregatedValueKey = DEFAULT_AGGREGATED_VALUE_KEY as keyof ResponsiveVisualizationData<'aggregated'>[0], + individualValueKey = DEFAULT_INDIVIDUAL_VALUE_KEY as keyof ResponsiveVisualizationData<'individual'>[0], labelKey = DEFAULT_LABEL_KEY, barLabel, aggregatedTooltip, @@ -35,7 +35,7 @@ export function ResponsiveBarChart({ }: ResponsiveBarChartProps) { const containerRef = useRef(null) const { data, isIndividualSupported } = useResponsiveVisualization(containerRef, { - labelKey, + labelKey: labelKey as ResponsiveVisualizationAnyItemKey, aggregatedValueKey, individualValueKey, getAggregatedData, @@ -57,7 +57,7 @@ export function ResponsiveBarChart({ data={data as ResponsiveVisualizationData<'individual'>} color={color} valueKey={individualValueKey} - labelKey={labelKey} + labelKey={labelKey as ResponsiveVisualizationAnyItemKey} onClick={onIndividualItemClick} barLabel={barLabel} customTooltip={individualTooltip} @@ -68,7 +68,7 @@ export function ResponsiveBarChart({ data={data as ResponsiveVisualizationData<'aggregated'>} color={color} valueKey={aggregatedValueKey} - labelKey={labelKey} + labelKey={labelKey as ResponsiveVisualizationAnyItemKey} onClick={onAggregatedItemClick} barLabel={barLabel} customTooltip={aggregatedTooltip} diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx index c29fbe2448..a25f27e5f7 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx @@ -33,7 +33,7 @@ export function AggregatedBarChart({ [0]) => - barValueFormatter?.(entry[valueKey]) || entry[valueKey] + barValueFormatter?.(entry[valueKey] as number) || entry[valueKey] } /> diff --git a/libs/responsive-visualizations/src/charts/hooks.ts b/libs/responsive-visualizations/src/charts/hooks.ts index 35c8405827..6e81ecc401 100644 --- a/libs/responsive-visualizations/src/charts/hooks.ts +++ b/libs/responsive-visualizations/src/charts/hooks.ts @@ -81,8 +81,8 @@ export function useResponsiveVisualizationData({ start, end, timeseriesInterval, - individualValueKey, - aggregatedValueKey, + individualValueKey: individualValueKey as ResponsiveVisualizationAnyItemKey, + aggregatedValueKey: aggregatedValueKey as ResponsiveVisualizationAnyItemKey, }) ) { const individualData = await getIndividualData() @@ -110,8 +110,8 @@ export function useResponsiveVisualizationData({ start, end, timeseriesInterval, - individualValueKey, - aggregatedValueKey, + individualValueKey: individualValueKey as ResponsiveVisualizationAnyItemKey, + aggregatedValueKey: aggregatedValueKey as ResponsiveVisualizationAnyItemKey, }) ) { setIsIndividualSupported(true) @@ -123,7 +123,7 @@ export function useResponsiveVisualizationData({ [labelKey]: item[labelKey], [individualValueKey]: value.length, } - }) + }) as ResponsiveVisualizationData setIsIndividualSupported(false) setData(aggregatedData) } diff --git a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx index 0f767da93a..77d23a996f 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx @@ -1,7 +1,11 @@ import { useRef } from 'react' import type { ResponsiveVisualizationData } from '../../types' import { getIsIndividualTimeseriesSupported } from '../../lib/density' -import type { BaseResponsiveChartProps, BaseResponsiveTimeseriesProps } from '../types' +import type { + BaseResponsiveChartProps, + BaseResponsiveTimeseriesProps, + ResponsiveVisualizationAnyItemKey, +} from '../types' import { useResponsiveVisualization } from '../hooks' import { DEFAULT_AGGREGATED_VALUE_KEY, @@ -14,9 +18,7 @@ import styles from './Timeseries.module.css' type ResponsiveTimeseriesProps = BaseResponsiveChartProps & BaseResponsiveTimeseriesProps & { - dateKey?: - | keyof ResponsiveVisualizationData<'aggregated'>[0] - | keyof ResponsiveVisualizationData<'individual'>[0] + dateKey?: ResponsiveVisualizationAnyItemKey } export function ResponsiveTimeseries({ diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx index a3da63e7d4..1a2f6aff2a 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx @@ -9,7 +9,7 @@ import { } from 'recharts' import min from 'lodash/min' import max from 'lodash/max' -import type { TimeseriesByTypeProps } from '../types' +import type { ResponsiveVisualizationAnyItemKey, TimeseriesByTypeProps } from '../types' import { useFullTimeseries, useTimeseriesDomain } from './timeseries.hooks' const graphMargin = { top: 0, right: 0, left: -20, bottom: -10 } @@ -42,7 +42,7 @@ export function AggregatedTimeseries({ data, timeseriesInterval, dateKey, - valueKey, + valueKey: valueKey as ResponsiveVisualizationAnyItemKey, }) if (!fullTimeseries.length) { diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx index dad69018c6..87493cd9f7 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx @@ -1,6 +1,6 @@ import { XAxis, ResponsiveContainer, ComposedChart, Tooltip } from 'recharts' import cx from 'classnames' -import type { TimeseriesByTypeProps } from '../types' +import type { ResponsiveVisualizationAnyItemKey, TimeseriesByTypeProps } from '../types' import type { ResponsiveVisualizationItem } from '../../types' import { IndividualPoint } from '../points/IndividualPoint' import { AXIS_LABEL_PADDING, POINT_GAP, POINT_SIZE, TIMESERIES_PADDING } from '../config' @@ -31,7 +31,7 @@ export function IndividualTimeseries({ data, timeseriesInterval, dateKey, - valueKey, + valueKey: valueKey as ResponsiveVisualizationAnyItemKey, aggregated: false, }) diff --git a/libs/responsive-visualizations/src/charts/timeseries/timeseries.hooks.ts b/libs/responsive-visualizations/src/charts/timeseries/timeseries.hooks.ts index 57cc027588..4f1ffd83c1 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/timeseries.hooks.ts +++ b/libs/responsive-visualizations/src/charts/timeseries/timeseries.hooks.ts @@ -44,7 +44,7 @@ export function useFullTimeseries({ aggregated = true, }: UseFullTimeseriesProps) { return useMemo(() => { - if (!data) { + if (!data || !dateKey || !valueKey) { return [] } @@ -63,7 +63,7 @@ export function useFullTimeseries({ const d = DateTime.fromMillis(startMillis, { zone: 'UTC' }) .plus({ [timeseriesInterval]: i }) .toISO() - const dataValue = data.find((item) => d?.startsWith(item[dateKey]))?.[valueKey] + const dataValue = data.find((item) => d?.startsWith(item[dateKey] as string))?.[valueKey] return { [dateKey]: d, [valueKey]: dataValue ? dataValue : aggregated ? 0 : [], diff --git a/libs/responsive-visualizations/src/charts/types.ts b/libs/responsive-visualizations/src/charts/types.ts index a02db32c9a..d13a4db1f1 100644 --- a/libs/responsive-visualizations/src/charts/types.ts +++ b/libs/responsive-visualizations/src/charts/types.ts @@ -24,10 +24,8 @@ export type BaseResponsiveChartProps = { individualValueKey?: keyof ResponsiveVisualizationData<'individual'>[0] } -// TODO: remove this -export type ResponsiveVisualizationAnyItemKey = - | keyof ResponsiveVisualizationData<'aggregated'>[0] - | keyof ResponsiveVisualizationData<'individual'>[0] +export type ResponsiveVisualizationAnyItemKey = keyof ResponsiveVisualizationData<'aggregated'>[0] & + keyof ResponsiveVisualizationData<'individual'>[0] // Shared types within the BarChart export type BaseResponsiveBarChartProps = { diff --git a/libs/responsive-visualizations/src/lib/density.ts b/libs/responsive-visualizations/src/lib/density.ts index 5b556d106a..ac766055f4 100644 --- a/libs/responsive-visualizations/src/lib/density.ts +++ b/libs/responsive-visualizations/src/lib/density.ts @@ -9,11 +9,7 @@ import { COLUMN_LABEL_SIZE, TIMESERIES_PADDING, } from '../charts/config' -import type { - ResponsiveVisualizationAggregatedItem, - ResponsiveVisualizationData, - ResponsiveVisualizationIndividualItem, -} from '../types' +import type { ResponsiveVisualizationData } from '../types' export const getBarProps = ( data: ResponsiveVisualizationData, @@ -32,9 +28,11 @@ export const getBiggestColumnValue = ( individualValueKey: ResponsiveVisualizationAnyItemKey ): number => { return data.reduce((acc, column) => { - const value = (column as ResponsiveVisualizationIndividualItem)[aggregatedValueKey] - ? (column as ResponsiveVisualizationIndividualItem)[aggregatedValueKey] - : (column as ResponsiveVisualizationAggregatedItem)[individualValueKey]?.length || 0 + const value = ( + column[aggregatedValueKey] + ? column[aggregatedValueKey] + : column[individualValueKey]?.length || 0 + ) as number if (value > acc) { return value } @@ -70,8 +68,8 @@ type getIsIndividualTimeseriesSupportedParams = { timeseriesInterval?: FourwingsInterval width: number height: number - aggregatedValueKey: string - individualValueKey: string + aggregatedValueKey: ResponsiveVisualizationAnyItemKey + individualValueKey: ResponsiveVisualizationAnyItemKey } export function getIsIndividualTimeseriesSupported({ From 69a2ce0f9b7641514bb3cd81ca71b2bed4f9d0ca Mon Sep 17 00:00:00 2001 From: j8seangel Date: Mon, 13 Jan 2025 20:36:41 +0100 Subject: [PATCH 22/62] better typings --- .../shared/events/EventsReportGraph.tsx | 1 - .../src/charts/hooks.ts | 10 +++++----- .../src/charts/timeseries/Timeseries.tsx | 8 ++++---- .../charts/timeseries/TimeseriesAggregated.tsx | 3 ++- .../charts/timeseries/TimeseriesIndividual.tsx | 6 +++--- .../src/charts/timeseries/timeseries.hooks.ts | 6 ++++-- .../src/charts/types.ts | 5 +++-- .../src/lib/density.ts | 18 +++++++++--------- 8 files changed, 30 insertions(+), 27 deletions(-) diff --git a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx index f1291af700..d1c3e0896c 100644 --- a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx +++ b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx @@ -114,7 +114,6 @@ export default function EventsReportGraph({ } return ( - // TODO: remove this ref and move it inside
              { const value = item[individualValueKey] as ResponsiveVisualizationItem[] return { - [labelKey]: item[labelKey], + [labelKey]: item[labelKey as keyof typeof item], [individualValueKey]: value.length, } }) as ResponsiveVisualizationData diff --git a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx index 77d23a996f..913f106226 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx @@ -18,7 +18,7 @@ import styles from './Timeseries.module.css' type ResponsiveTimeseriesProps = BaseResponsiveChartProps & BaseResponsiveTimeseriesProps & { - dateKey?: ResponsiveVisualizationAnyItemKey + dateKey?: ResponsiveVisualizationAnyItemKey | string } export function ResponsiveTimeseries({ @@ -42,7 +42,7 @@ export function ResponsiveTimeseries({ start, end, timeseriesInterval, - labelKey: dateKey, + labelKey: dateKey as ResponsiveVisualizationAnyItemKey, individualValueKey, aggregatedValueKey, getAggregatedData, @@ -66,7 +66,7 @@ export function ResponsiveTimeseries({ start={start} end={end} color={color} - dateKey={dateKey} + dateKey={dateKey as ResponsiveVisualizationAnyItemKey} timeseriesInterval={timeseriesInterval} valueKey={individualValueKey} onClick={onIndividualItemClick} @@ -79,7 +79,7 @@ export function ResponsiveTimeseries({ start={start} end={end} color={color} - dateKey={dateKey} + dateKey={dateKey as ResponsiveVisualizationAnyItemKey} timeseriesInterval={timeseriesInterval} valueKey={aggregatedValueKey} onClick={onAggregatedItemClick} diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx index 1a2f6aff2a..d387055a04 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx @@ -10,6 +10,7 @@ import { import min from 'lodash/min' import max from 'lodash/max' import type { ResponsiveVisualizationAnyItemKey, TimeseriesByTypeProps } from '../types' +import type { ResponsiveVisualizationData } from '../../types' import { useFullTimeseries, useTimeseriesDomain } from './timeseries.hooks' const graphMargin = { top: 0, right: 0, left: -20, bottom: -10 } @@ -42,7 +43,7 @@ export function AggregatedTimeseries({ data, timeseriesInterval, dateKey, - valueKey: valueKey as ResponsiveVisualizationAnyItemKey, + valueKey: valueKey as keyof ResponsiveVisualizationData[0], }) if (!fullTimeseries.length) { diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx index 87493cd9f7..dd0548ae0b 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx @@ -1,7 +1,7 @@ import { XAxis, ResponsiveContainer, ComposedChart, Tooltip } from 'recharts' import cx from 'classnames' -import type { ResponsiveVisualizationAnyItemKey, TimeseriesByTypeProps } from '../types' -import type { ResponsiveVisualizationItem } from '../../types' +import type { TimeseriesByTypeProps } from '../types' +import type { ResponsiveVisualizationData, ResponsiveVisualizationItem } from '../../types' import { IndividualPoint } from '../points/IndividualPoint' import { AXIS_LABEL_PADDING, POINT_GAP, POINT_SIZE, TIMESERIES_PADDING } from '../config' import styles from './TimeseriesIndividual.module.css' @@ -31,7 +31,7 @@ export function IndividualTimeseries({ data, timeseriesInterval, dateKey, - valueKey: valueKey as ResponsiveVisualizationAnyItemKey, + valueKey: valueKey as keyof ResponsiveVisualizationData[0], aggregated: false, }) diff --git a/libs/responsive-visualizations/src/charts/timeseries/timeseries.hooks.ts b/libs/responsive-visualizations/src/charts/timeseries/timeseries.hooks.ts index 4f1ffd83c1..a0949f068b 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/timeseries.hooks.ts +++ b/libs/responsive-visualizations/src/charts/timeseries/timeseries.hooks.ts @@ -31,7 +31,7 @@ type UseFullTimeseriesProps = { data: ResponsiveVisualizationData timeseriesInterval: FourwingsInterval dateKey: ResponsiveVisualizationAnyItemKey - valueKey: ResponsiveVisualizationAnyItemKey + valueKey: keyof ResponsiveVisualizationData[0] aggregated?: boolean } export function useFullTimeseries({ @@ -63,7 +63,9 @@ export function useFullTimeseries({ const d = DateTime.fromMillis(startMillis, { zone: 'UTC' }) .plus({ [timeseriesInterval]: i }) .toISO() - const dataValue = data.find((item) => d?.startsWith(item[dateKey] as string))?.[valueKey] + const dataValue = data.find((item) => + d?.startsWith(item[dateKey as keyof typeof item] as any) + )?.[valueKey] return { [dateKey]: d, [valueKey]: dataValue ? dataValue : aggregated ? 0 : [], diff --git a/libs/responsive-visualizations/src/charts/types.ts b/libs/responsive-visualizations/src/charts/types.ts index d13a4db1f1..1c8441a346 100644 --- a/libs/responsive-visualizations/src/charts/types.ts +++ b/libs/responsive-visualizations/src/charts/types.ts @@ -24,8 +24,9 @@ export type BaseResponsiveChartProps = { individualValueKey?: keyof ResponsiveVisualizationData<'individual'>[0] } -export type ResponsiveVisualizationAnyItemKey = keyof ResponsiveVisualizationData<'aggregated'>[0] & - keyof ResponsiveVisualizationData<'individual'>[0] +export type ResponsiveVisualizationAnyItemKey = + | keyof ResponsiveVisualizationData<'aggregated'>[0] + | keyof ResponsiveVisualizationData<'individual'>[0] // Shared types within the BarChart export type BaseResponsiveBarChartProps = { diff --git a/libs/responsive-visualizations/src/lib/density.ts b/libs/responsive-visualizations/src/lib/density.ts index ac766055f4..1d79f1b502 100644 --- a/libs/responsive-visualizations/src/lib/density.ts +++ b/libs/responsive-visualizations/src/lib/density.ts @@ -24,14 +24,14 @@ export const getBarProps = ( export const getBiggestColumnValue = ( data: ResponsiveVisualizationData, - aggregatedValueKey: ResponsiveVisualizationAnyItemKey, - individualValueKey: ResponsiveVisualizationAnyItemKey + aggregatedValueKey: keyof ResponsiveVisualizationData<'aggregated'>[0], + individualValueKey: keyof ResponsiveVisualizationData<'individual'>[0] ): number => { return data.reduce((acc, column) => { const value = ( - column[aggregatedValueKey] - ? column[aggregatedValueKey] - : column[individualValueKey]?.length || 0 + column[aggregatedValueKey as keyof typeof column] + ? column[aggregatedValueKey as keyof typeof column] + : column[individualValueKey as keyof typeof column]?.length || 0 ) as number if (value > acc) { return value @@ -44,8 +44,8 @@ type IsIndividualSupportedParams = { data: ResponsiveVisualizationData width: number height: number - aggregatedValueKey: ResponsiveVisualizationAnyItemKey - individualValueKey: ResponsiveVisualizationAnyItemKey + aggregatedValueKey: keyof ResponsiveVisualizationData<'aggregated'>[0] + individualValueKey: keyof ResponsiveVisualizationData<'individual'>[0] } export function getIsIndividualBarChartSupported({ data, @@ -68,8 +68,8 @@ type getIsIndividualTimeseriesSupportedParams = { timeseriesInterval?: FourwingsInterval width: number height: number - aggregatedValueKey: ResponsiveVisualizationAnyItemKey - individualValueKey: ResponsiveVisualizationAnyItemKey + aggregatedValueKey: keyof ResponsiveVisualizationData<'aggregated'>[0] + individualValueKey: keyof ResponsiveVisualizationData<'individual'>[0] } export function getIsIndividualTimeseriesSupported({ From 20b1102de8f95fe79c131a8ba906eb2128a4aff4 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Mon, 13 Jan 2025 20:42:12 +0100 Subject: [PATCH 23/62] add validation for MAX_INDIVIDUAL_ITEMS --- .../src/lib/density.ts | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/libs/responsive-visualizations/src/lib/density.ts b/libs/responsive-visualizations/src/lib/density.ts index 1d79f1b502..906d61e663 100644 --- a/libs/responsive-visualizations/src/lib/density.ts +++ b/libs/responsive-visualizations/src/lib/density.ts @@ -1,13 +1,13 @@ import type { DurationUnit } from 'luxon' import { DateTime, Duration } from 'luxon' import type { FourwingsInterval } from '@globalfishingwatch/deck-loaders' -import type { ResponsiveVisualizationAnyItemKey } from '../charts' import { COLUMN_PADDING, POINT_SIZE, AXIS_LABEL_PADDING, COLUMN_LABEL_SIZE, TIMESERIES_PADDING, + MAX_INDIVIDUAL_ITEMS, } from '../charts/config' import type { ResponsiveVisualizationData } from '../types' @@ -22,22 +22,26 @@ export const getBarProps = ( return { columnsNumber, columnsWidth, pointsByRow } } -export const getBiggestColumnValue = ( +type ColumnsStats = { + total: number + max: number +} +export const getColumnsStats = ( data: ResponsiveVisualizationData, aggregatedValueKey: keyof ResponsiveVisualizationData<'aggregated'>[0], individualValueKey: keyof ResponsiveVisualizationData<'individual'>[0] -): number => { - return data.reduce((acc, column) => { - const value = ( - column[aggregatedValueKey as keyof typeof column] - ? column[aggregatedValueKey as keyof typeof column] - : column[individualValueKey as keyof typeof column]?.length || 0 - ) as number - if (value > acc) { - return value - } - return acc - }, 0) +): ColumnsStats => { + return data.reduce( + (acc, column) => { + const value = ( + column[aggregatedValueKey as keyof typeof column] + ? column[aggregatedValueKey as keyof typeof column] + : column[individualValueKey as keyof typeof column]?.length || 0 + ) as number + return { total: acc.total + value, max: Math.max(acc.max, value) } + }, + { total: 0, max: 0 } as ColumnsStats + ) } type IsIndividualSupportedParams = { @@ -54,9 +58,12 @@ export function getIsIndividualBarChartSupported({ aggregatedValueKey, individualValueKey, }: IsIndividualSupportedParams): boolean { + const { total, max } = getColumnsStats(data, aggregatedValueKey, individualValueKey) + if (total > MAX_INDIVIDUAL_ITEMS) { + return false + } const { pointsByRow } = getBarProps(data, width) - const biggestColumnValue = getBiggestColumnValue(data, aggregatedValueKey, individualValueKey) - const rowsInBiggestColumn = Math.ceil(biggestColumnValue / pointsByRow) + const rowsInBiggestColumn = Math.ceil(max / pointsByRow) const heightNeeded = rowsInBiggestColumn * POINT_SIZE return heightNeeded < height - AXIS_LABEL_PADDING - COLUMN_PADDING - COLUMN_LABEL_SIZE } @@ -82,8 +89,11 @@ export function getIsIndividualTimeseriesSupported({ aggregatedValueKey, individualValueKey, }: getIsIndividualTimeseriesSupportedParams): boolean { - const biggestColumnValue = getBiggestColumnValue(data, aggregatedValueKey, individualValueKey) - const heightNeeded = biggestColumnValue * POINT_SIZE + const { total, max } = getColumnsStats(data, aggregatedValueKey, individualValueKey) + if (total > MAX_INDIVIDUAL_ITEMS) { + return false + } + const heightNeeded = max * POINT_SIZE const matchesHeight = heightNeeded < height - AXIS_LABEL_PADDING if (!matchesHeight) { return false From 2f7882c4d3f594edb0283736bcbf1d67bf127424 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Tue, 14 Jan 2025 09:24:08 +0100 Subject: [PATCH 24/62] ensure isIndividualSupported when received individualData --- .../src/charts/hooks.ts | 34 +++++++++++-------- .../src/lib/density.ts | 18 +++------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/libs/responsive-visualizations/src/charts/hooks.ts b/libs/responsive-visualizations/src/charts/hooks.ts index 770700a582..0dad7dd4d5 100644 --- a/libs/responsive-visualizations/src/charts/hooks.ts +++ b/libs/responsive-visualizations/src/charts/hooks.ts @@ -4,6 +4,7 @@ import type { ResponsiveVisualizationData, ResponsiveVisualizationItem } from '. import type { getIsIndividualBarChartSupported, getIsIndividualTimeseriesSupported, + IsIndividualSupportedParams, } from '../lib/density' import type { BaseResponsiveChartProps, ResponsiveVisualizationAnyItemKey } from './types' import { @@ -67,6 +68,15 @@ export function useResponsiveVisualizationData({ const loadData = useCallback( async ({ width, height }: { width: number; height: number }) => { + const isIndividualParams: Omit = { + width, + height, + start, + end, + timeseriesInterval, + individualValueKey, + aggregatedValueKey, + } if (getAggregatedData) { const aggregatedData = await getAggregatedData() if (!aggregatedData) { @@ -76,17 +86,17 @@ export function useResponsiveVisualizationData({ getIndividualData && getIsIndividualSupported({ data: aggregatedData, - width, - height, - start, - end, - timeseriesInterval, - individualValueKey, - aggregatedValueKey, + ...isIndividualParams, }) ) { const individualData = await getIndividualData() - if (individualData) { + if ( + individualData && + getIsIndividualSupported({ + data: individualData, + ...isIndividualParams, + }) + ) { setIsIndividualSupported(true) setData(individualData) } else { @@ -105,13 +115,7 @@ export function useResponsiveVisualizationData({ if ( getIsIndividualSupported({ data: individualData, - width, - height, - start, - end, - timeseriesInterval, - individualValueKey, - aggregatedValueKey, + ...isIndividualParams, }) ) { setIsIndividualSupported(true) diff --git a/libs/responsive-visualizations/src/lib/density.ts b/libs/responsive-visualizations/src/lib/density.ts index 906d61e663..1d0b7335ec 100644 --- a/libs/responsive-visualizations/src/lib/density.ts +++ b/libs/responsive-visualizations/src/lib/density.ts @@ -44,8 +44,11 @@ export const getColumnsStats = ( ) } -type IsIndividualSupportedParams = { +export type IsIndividualSupportedParams = { data: ResponsiveVisualizationData + start?: string + end?: string + timeseriesInterval?: FourwingsInterval width: number height: number aggregatedValueKey: keyof ResponsiveVisualizationData<'aggregated'>[0] @@ -68,17 +71,6 @@ export function getIsIndividualBarChartSupported({ return heightNeeded < height - AXIS_LABEL_PADDING - COLUMN_PADDING - COLUMN_LABEL_SIZE } -type getIsIndividualTimeseriesSupportedParams = { - data: ResponsiveVisualizationData - start?: string - end?: string - timeseriesInterval?: FourwingsInterval - width: number - height: number - aggregatedValueKey: keyof ResponsiveVisualizationData<'aggregated'>[0] - individualValueKey: keyof ResponsiveVisualizationData<'individual'>[0] -} - export function getIsIndividualTimeseriesSupported({ data, width, @@ -88,7 +80,7 @@ export function getIsIndividualTimeseriesSupported({ timeseriesInterval, aggregatedValueKey, individualValueKey, -}: getIsIndividualTimeseriesSupportedParams): boolean { +}: IsIndividualSupportedParams): boolean { const { total, max } = getColumnsStats(data, aggregatedValueKey, individualValueKey) if (total > MAX_INDIVIDUAL_ITEMS) { return false From 8e01b3e6cadd0231c44c37ad2ac9dad586847a12 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Tue, 14 Jan 2025 09:24:59 +0100 Subject: [PATCH 25/62] add includes in individual data requests --- apps/fishing-map/features/reports/ports/PortsReport.tsx | 1 + .../features/reports/shared/events/EventsReportGraph.tsx | 6 +++++- .../features/reports/vessel-groups/events/VGREvents.tsx | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/fishing-map/features/reports/ports/PortsReport.tsx b/apps/fishing-map/features/reports/ports/PortsReport.tsx index 4c8f56ee63..9b77b3b762 100644 --- a/apps/fishing-map/features/reports/ports/PortsReport.tsx +++ b/apps/fishing-map/features/reports/ports/PortsReport.tsx @@ -156,6 +156,7 @@ function PortsReport() { start={start} end={end} filters={{ 'port-ids': [portId] }} + includes={['id', 'start', 'end', 'vessel']} datasetId={datasetId} timeseries={data.timeseries || []} /> diff --git a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx index d1c3e0896c..a6446f34fd 100644 --- a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx +++ b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx @@ -68,6 +68,7 @@ const formatDateTicks: BaseResponsiveTimeseriesProps['tickLabelFormatter'] = ( export default function EventsReportGraph({ datasetId, filters, + includes, color = COLOR_PRIMARY_BLUE, end, start, @@ -75,6 +76,7 @@ export default function EventsReportGraph({ }: { datasetId: string filters?: Record + includes?: string[] color?: string end: string start: string @@ -87,6 +89,7 @@ export default function EventsReportGraph({ const endMillis = DateTime.fromISO(end).toMillis() const interval = getFourwingsInterval(startMillis, endMillis) const filtersMemo = useMemoCompare(filters) + const includesMemo = useMemoCompare(includes) const getAggregatedData = useCallback(async () => timeseries, [timeseries]) const getIndividualData = useCallback(async () => { @@ -96,6 +99,7 @@ export default function EventsReportGraph({ 'end-date': end, 'time-filter-mode': 'START-DATE', ...(filtersMemo && { ...filtersMemo }), + ...(includesMemo && { includes: includesMemo }), datasets: [datasetId], limit: 1000, offset: 0, @@ -107,7 +111,7 @@ export default function EventsReportGraph({ return Object.entries(groupedData) .map(([date, events]) => ({ date, values: events })) .sort((a, b) => a.date.localeCompare(b.date)) - }, [datasetId, end, interval, filtersMemo, start]) + }, [start, end, filtersMemo, includesMemo, datasetId, interval]) if (!timeseries.length) { return null diff --git a/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx b/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx index 2099561a0e..94bd4605c8 100644 --- a/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx @@ -169,6 +169,7 @@ function VGREvents() { Date: Tue, 14 Jan 2025 09:39:23 +0100 Subject: [PATCH 26/62] responsive visualiztion placeholders --- .../src/charts/barchart/BarChart.tsx | 3 +- .../placeholders/BarChartPlaceholder.tsx | 58 +++++++ .../placeholders/TimeseriesPlaceholder.tsx | 91 ++++++++++ .../placeholders/placeholders.module.css | 162 ++++++++++++++++++ .../src/charts/timeseries/Timeseries.tsx | 3 +- 5 files changed, 315 insertions(+), 2 deletions(-) create mode 100644 libs/responsive-visualizations/src/charts/placeholders/BarChartPlaceholder.tsx create mode 100644 libs/responsive-visualizations/src/charts/placeholders/TimeseriesPlaceholder.tsx create mode 100644 libs/responsive-visualizations/src/charts/placeholders/placeholders.module.css diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx index cb6579c870..836df04b16 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx @@ -12,6 +12,7 @@ import { DEFAULT_LABEL_KEY, } from '../config' import { useResponsiveVisualization } from '../hooks' +import { BarChartPlaceholder } from '../placeholders/BarChartPlaceholder' import { IndividualBarChart } from './BarChartIndividual' import { AggregatedBarChart } from './BarChartAggregated' import styles from './BarChart.module.css' @@ -51,7 +52,7 @@ export function ResponsiveBarChart({ return (
              {!data ? ( - 'Spinner' + ) : isIndividualSupported ? ( } diff --git a/libs/responsive-visualizations/src/charts/placeholders/BarChartPlaceholder.tsx b/libs/responsive-visualizations/src/charts/placeholders/BarChartPlaceholder.tsx new file mode 100644 index 0000000000..1d99330f4b --- /dev/null +++ b/libs/responsive-visualizations/src/charts/placeholders/BarChartPlaceholder.tsx @@ -0,0 +1,58 @@ +import cx from 'classnames' +import styles from './placeholders.module.css' + +export function BarChartPlaceholder({ + animate = true, + children, +}: { + animate?: boolean + children?: React.ReactNode +}) { + return ( +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              + {children &&
              {children}
              } +
              + ) +} diff --git a/libs/responsive-visualizations/src/charts/placeholders/TimeseriesPlaceholder.tsx b/libs/responsive-visualizations/src/charts/placeholders/TimeseriesPlaceholder.tsx new file mode 100644 index 0000000000..f5bcdc1b9c --- /dev/null +++ b/libs/responsive-visualizations/src/charts/placeholders/TimeseriesPlaceholder.tsx @@ -0,0 +1,91 @@ +import cx from 'classnames' +import styles from './placeholders.module.css' + +export default function TimeseriesPlaceholder({ + animate = true, + children = null, +}: { + animate?: boolean + children?: React.ReactNode +}) { + return ( +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              + {children &&
              {children}
              } +
              + ) +} diff --git a/libs/responsive-visualizations/src/charts/placeholders/placeholders.module.css b/libs/responsive-visualizations/src/charts/placeholders/placeholders.module.css new file mode 100644 index 0000000000..83bd703f08 --- /dev/null +++ b/libs/responsive-visualizations/src/charts/placeholders/placeholders.module.css @@ -0,0 +1,162 @@ +.container { + padding: var(--space-M); + position: relative; +} + +.relative { + position: relative; + width: 100%; + height: 100%; +} + +.cover { + position: absolute; + inset: 0; +} + +.faint { + opacity: 0.5; + filter: blur(1px); +} + +.flex { + display: flex; + gap: 1rem; + align-items: center; +} + +.center { + display: flex; + justify-content: center; + align-items: center; +} + +.column { + flex-direction: column; +} + +.spaceBetween { + justify-content: space-between; +} + +.spaceAround { + justify-content: space-around; +} + +.end { + align-items: flex-end; +} + +.paragraph { + display: flex; + flex-direction: column; + gap: 1rem; + margin-bottom: 1rem; +} + +.paragraphS { + gap: 0.2rem; +} + +.sentence { + display: flex; + gap: 1rem; +} + +.block { + height: 1.6rem; + border-radius: 0.8rem; + background: linear-gradient( + 90deg, + rgba(var(--primary-blue-rgb), 0.1) 0%, + rgba(var(--primary-blue-rgb), 0.2) 40%, + rgba(var(--primary-blue-rgb), 0.1) 70% + ); + background-size: 1000px 100%; +} + +.animate { + animation: placeHolderShimmer 2s linear forwards infinite; +} + +.grow { + flex: 1; +} + +.XS { + height: 1rem; + border-radius: 0.5rem; + margin-block: 0.3rem; +} + +.S { + height: 1.2rem; + border-radius: 0.6rem; + margin-block: 0.3rem; +} + +.L { + height: 1.8rem; + border-radius: 1rem; + margin-block: 0.1rem; +} + +.XL { + height: 2.2rem; + border-radius: 1.2rem; +} + +.short { + flex: 0.5; +} + +.thick { + min-height: 4rem; + border-radius: 2rem; +} + +.line { + height: 1px; + min-height: 1px; +} + +.marginH { + margin-left: var(--space-S); + margin-right: var(--space-S); +} + +.marginL { + margin-left: var(--space-S); +} + +.marginV { + margin-top: var(--space-S); + margin-bottom: var(--space-S); +} + +.marginBottom { + margin-bottom: var(--space-S); +} + +.tag { + margin-bottom: 0.7rem; +} + +@keyframes placeHolderShimmer { + 0% { + background-position: -500px 0; + } + + 100% { + background-position: 500px 0; + } +} + +.children { + position: absolute; + inset: 0; + top: -3rem; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx index 913f106226..d5b2c1c248 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx @@ -12,6 +12,7 @@ import { DEFAULT_INDIVIDUAL_VALUE_KEY, DEFAULT_DATE_KEY, } from '../config' +import TimeseriesPlaceholder from '../placeholders/TimeseriesPlaceholder' import { IndividualTimeseries } from './TimeseriesIndividual' import { AggregatedTimeseries } from './TimeseriesAggregated' import styles from './Timeseries.module.css' @@ -58,7 +59,7 @@ export function ResponsiveTimeseries({ return (
              {!data ? ( - 'Spinner' + ) : isIndividualSupported ? ( Date: Tue, 14 Jan 2025 10:02:32 +0100 Subject: [PATCH 27/62] adjust density calculation --- libs/responsive-visualizations/src/lib/density.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libs/responsive-visualizations/src/lib/density.ts b/libs/responsive-visualizations/src/lib/density.ts index 1d0b7335ec..305b48d173 100644 --- a/libs/responsive-visualizations/src/lib/density.ts +++ b/libs/responsive-visualizations/src/lib/density.ts @@ -8,6 +8,7 @@ import { COLUMN_LABEL_SIZE, TIMESERIES_PADDING, MAX_INDIVIDUAL_ITEMS, + POINT_GAP, } from '../charts/config' import type { ResponsiveVisualizationData } from '../types' @@ -17,7 +18,7 @@ export const getBarProps = ( ): { columnsNumber: number; columnsWidth: number; pointsByRow: number } => { const columnsNumber = data.length const columnsWidth = width / columnsNumber - COLUMN_PADDING * 2 - const pointsByRow = Math.floor(columnsWidth / POINT_SIZE) + const pointsByRow = Math.floor(columnsWidth / (POINT_SIZE + POINT_GAP)) return { columnsNumber, columnsWidth, pointsByRow } } @@ -67,7 +68,7 @@ export function getIsIndividualBarChartSupported({ } const { pointsByRow } = getBarProps(data, width) const rowsInBiggestColumn = Math.ceil(max / pointsByRow) - const heightNeeded = rowsInBiggestColumn * POINT_SIZE + const heightNeeded = rowsInBiggestColumn * (POINT_SIZE + POINT_GAP) return heightNeeded < height - AXIS_LABEL_PADDING - COLUMN_PADDING - COLUMN_LABEL_SIZE } @@ -85,7 +86,7 @@ export function getIsIndividualTimeseriesSupported({ if (total > MAX_INDIVIDUAL_ITEMS) { return false } - const heightNeeded = max * POINT_SIZE + const heightNeeded = max * (POINT_SIZE + POINT_GAP) const matchesHeight = heightNeeded < height - AXIS_LABEL_PADDING if (!matchesHeight) { return false From 49e84de2da7d92a2772ab6d07f81762fa17e3e7a Mon Sep 17 00:00:00 2001 From: j8seangel Date: Tue, 14 Jan 2025 10:32:36 +0100 Subject: [PATCH 28/62] include individual items filters in request --- .../features/reports/ports/PortsReport.tsx | 6 ++- .../shared/events/EventsReportGraph.tsx | 16 ++++---- .../vessel-groups/events/VGREvents.tsx | 7 +++- .../queries/report-events-stats-api.ts | 38 +++++++++++-------- 4 files changed, 42 insertions(+), 25 deletions(-) diff --git a/apps/fishing-map/features/reports/ports/PortsReport.tsx b/apps/fishing-map/features/reports/ports/PortsReport.tsx index 9b77b3b762..fc900e59fe 100644 --- a/apps/fishing-map/features/reports/ports/PortsReport.tsx +++ b/apps/fishing-map/features/reports/ports/PortsReport.tsx @@ -6,6 +6,7 @@ import parse from 'html-react-parser' import { DateTime } from 'luxon' import { useGetReportEventsStatsQuery } from 'queries/report-events-stats-api' import { Button } from '@globalfishingwatch/ui-components' +import { getDataviewFilters } from '@globalfishingwatch/dataviews-client' import EventsReportGraph from 'features/reports/shared/events/EventsReportGraph' import { selectReportPortId } from 'routes/routes.selectors' import EventsReportVesselPropertySelector from 'features/reports/shared/events/EventsReportVesselPropertySelector' @@ -155,7 +156,10 @@ function PortsReport() { color={color} start={start} end={end} - filters={{ 'port-ids': [portId] }} + filters={{ + portId, + ...(dataview && { ...getDataviewFilters(dataview) }), + }} includes={['id', 'start', 'end', 'vessel']} datasetId={datasetId} timeseries={data.timeseries || []} diff --git a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx index a6446f34fd..1dfa434a15 100644 --- a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx +++ b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx @@ -3,6 +3,8 @@ import { useTranslation } from 'react-i18next' import { DateTime } from 'luxon' import { groupBy } from 'es-toolkit' import { stringify } from 'qs' +import type { BaseReportEventsVesselsParamsFilters } from 'queries/report-events-stats-api' +import { getEventsStatsQuery } from 'queries/report-events-stats-api' import { getFourwingsInterval, type FourwingsInterval } from '@globalfishingwatch/deck-loaders' import type { BaseResponsiveTimeseriesProps } from '@globalfishingwatch/responsive-visualizations' import { ResponsiveTimeseries } from '@globalfishingwatch/responsive-visualizations' @@ -75,7 +77,7 @@ export default function EventsReportGraph({ timeseries, }: { datasetId: string - filters?: Record + filters?: BaseReportEventsVesselsParamsFilters includes?: string[] color?: string end: string @@ -93,14 +95,14 @@ export default function EventsReportGraph({ const getAggregatedData = useCallback(async () => timeseries, [timeseries]) const getIndividualData = useCallback(async () => { - // TODO add includes to fetch only the information needed const params = { - 'start-date': start, - 'end-date': end, - 'time-filter-mode': 'START-DATE', - ...(filtersMemo && { ...filtersMemo }), + ...getEventsStatsQuery({ + start, + end, + filters: filtersMemo, + dataset: datasetId, + }), ...(includesMemo && { includes: includesMemo }), - datasets: [datasetId], limit: 1000, offset: 0, } diff --git a/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx b/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx index 94bd4605c8..14ff2558c1 100644 --- a/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx @@ -15,6 +15,7 @@ import type { } from 'queries/report-events-stats-api' import { Icon } from '@globalfishingwatch/ui-components' import { DatasetTypes } from '@globalfishingwatch/api-types' +import { getDataviewFilters } from '@globalfishingwatch/dataviews-client' import VGREventsSubsectionSelector from 'features/reports/vessel-groups/events/VGREventsSubsectionSelector' import EventsReportGraph from 'features/reports/shared/events/EventsReportGraph' import { @@ -53,6 +54,7 @@ function VGREvents() { const vesselGroupId = useSelector(selectReportVesselGroupId) const filter = useSelector(selectVGREventsVesselFilter) const eventsDataview = useSelector(selectVGREventsSubsectionDataview) + console.log('🚀 ~ VGREvents ~ eventsDataview:', eventsDataview) const vesselsGroupByProperty = useSelector(selectVGREventsVesselsProperty) const vesselsWithEvents = useSelector(selectVGREventsVessels) const vesselFlags = useSelector(selectVGREventsVesselsFlags) @@ -168,7 +170,10 @@ function VGREvents() { {eventDataset?.id && ( Date: Tue, 14 Jan 2025 11:15:39 +0100 Subject: [PATCH 29/62] refactor dates parse fn --- .../shared/events/EventsReportGraph.tsx | 9 +- apps/fishing-map/utils/dates.ts | 60 +-------- libs/data-transforms/src/dates/dates.ts | 117 ++++++++++++++++++ libs/data-transforms/src/dates/index.ts | 1 + libs/data-transforms/src/index.ts | 1 + .../list-to-track-segments.test.ts | 2 +- .../list-to-track-segments.ts | 3 +- .../src/points/points-to-geojson.ts | 3 +- libs/data-transforms/src/schema/schema.ts | 31 ----- tsconfig.base.json | 1 + 10 files changed, 131 insertions(+), 97 deletions(-) create mode 100644 libs/data-transforms/src/dates/dates.ts create mode 100644 libs/data-transforms/src/dates/index.ts diff --git a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx index 1dfa434a15..5b63381fee 100644 --- a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx +++ b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx @@ -3,16 +3,17 @@ import { useTranslation } from 'react-i18next' import { DateTime } from 'luxon' import { groupBy } from 'es-toolkit' import { stringify } from 'qs' -import type { BaseReportEventsVesselsParamsFilters } from 'queries/report-events-stats-api' import { getEventsStatsQuery } from 'queries/report-events-stats-api' +import type { BaseReportEventsVesselsParamsFilters } from 'queries/report-events-stats-api' import { getFourwingsInterval, type FourwingsInterval } from '@globalfishingwatch/deck-loaders' import type { BaseResponsiveTimeseriesProps } from '@globalfishingwatch/responsive-visualizations' import { ResponsiveTimeseries } from '@globalfishingwatch/responsive-visualizations' import { GFWAPI } from '@globalfishingwatch/api-client' import type { ApiEvent, APIPagination } from '@globalfishingwatch/api-types' import { useMemoCompare } from '@globalfishingwatch/react-hooks' +import { getISODateByInterval } from '@globalfishingwatch/data-transforms' import i18n from 'features/i18n/i18n' -import { formatDateForInterval, getISODateByInterval, getUTCDateTime } from 'utils/dates' +import { formatDateForInterval, getUTCDateTime } from 'utils/dates' import { formatI18nNumber } from 'features/i18n/i18nNumber' import { COLOR_PRIMARY_BLUE } from 'features/app/app.config' import { formatInfoField } from 'utils/info' @@ -107,9 +108,7 @@ export default function EventsReportGraph({ offset: 0, } const data = await GFWAPI.fetch>(`/v3/events?${stringify(params)}`) - const groupedData = groupBy(data.entries, (item) => - getISODateByInterval(DateTime.fromISO(item.start as string), interval) - ) + const groupedData = groupBy(data.entries, (item) => getISODateByInterval(item.start, interval)) return Object.entries(groupedData) .map(([date, events]) => ({ date, values: events })) .sort((a, b) => a.date.localeCompare(b.date)) diff --git a/apps/fishing-map/utils/dates.ts b/apps/fishing-map/utils/dates.ts index 5e0082178c..b8ffb451e1 100644 --- a/apps/fishing-map/utils/dates.ts +++ b/apps/fishing-map/utils/dates.ts @@ -1,65 +1,9 @@ -import { DateTime, Duration } from 'luxon' +import { Duration } from 'luxon' import type { TFunction } from 'i18next' import type { Dataset, Report, VesselGroup } from '@globalfishingwatch/api-types' -import type { FourwingsInterval } from '@globalfishingwatch/deck-loaders' import type { AppWorkspace } from 'features/workspaces-list/workspaces-list.slice' -export type SupportedDateType = string | number -export const getUTCDateTime = (d: SupportedDateType) => { - if (!d || (typeof d !== 'string' && typeof d !== 'number' && typeof d !== 'object')) { - console.warn('Not a valid date', typeof d, d) - return DateTime.utc() - } - if (typeof d === 'object') { - try { - return DateTime.fromJSDate(d, { zone: 'utc' }) - } catch (error) { - console.warn('Not a valid date', typeof d, d) - return DateTime.utc() - } - } - if (typeof d === 'string') { - return DateTime.fromISO(d, { zone: 'utc' }) - } - return DateTime.fromMillis(d, { zone: 'utc' }) -} - -export const getISODateByInterval = (date: DateTime, timeChunkInterval: FourwingsInterval) => { - if (!date) { - return '' - } - switch (timeChunkInterval) { - case 'YEAR': - return date.toFormat('yyyy') as string - case 'MONTH': - return date.toFormat('yyyy-MM') as string - case 'HOUR': - return date.toFormat('yyyy-MM-ddTHH:00:00') as string - case 'DAY': - return date.toFormat('yyyy-MM-dd') as string - default: - return date.toISO() as string - } -} - -export const formatDateForInterval = (date: DateTime, timeChunkInterval: FourwingsInterval) => { - let formattedTick = '' - switch (timeChunkInterval) { - case 'YEAR': - formattedTick = date.year.toString() - break - case 'MONTH': - formattedTick = date.toFormat('LLL y') - break - case 'HOUR': - formattedTick = date.toLocaleString(DateTime.DATETIME_MED) - break - default: - formattedTick = date.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY) - break - } - return formattedTick -} +export { getUTCDateTime, formatDateForInterval } from '@globalfishingwatch/data-transforms' type UserCreatedEntities = Dataset | AppWorkspace | VesselGroup | Report diff --git a/libs/data-transforms/src/dates/dates.ts b/libs/data-transforms/src/dates/dates.ts new file mode 100644 index 0000000000..1ca343851d --- /dev/null +++ b/libs/data-transforms/src/dates/dates.ts @@ -0,0 +1,117 @@ +import toNumber from 'lodash/toNumber' +import type { DateTimeOptions } from 'luxon' +import { DateTime } from 'luxon' +import type { FourwingsInterval } from '@globalfishingwatch/deck-loaders' + +type DateTimeParseFunction = { (timestamp: string, opts: DateTimeOptions | undefined): DateTime } + +export const getUTCDate = (timestamp: string | number = Date.now()) => { + // it could receive a timestamp as a string + const millis = toNumber(timestamp) + if (typeof timestamp === 'number' || !isNaN(millis)) + return DateTime.fromMillis(millis, { zone: 'UTC' }).toJSDate() + + const tryParseMethods: DateTimeParseFunction[] = [ + DateTime.fromISO, + DateTime.fromSQL, + DateTime.fromRFC2822, + ] + let result + for (let index = 0; index < tryParseMethods.length; index++) { + const parse = tryParseMethods[index] + try { + result = parse(timestamp, { zone: 'UTC' }) + if (result.isValid) { + return result.toJSDate() + } + } catch (e) { + return new Date('Invalid Date') + } + } + return new Date('Invalid Date') +} + +export type SupportedDateType = string | number | Date +export const getUTCDateTime = (d: SupportedDateType): DateTime => { + if (!d || (typeof d !== 'string' && typeof d !== 'number' && typeof d !== 'object')) { + console.warn('Not a valid date', typeof d, d) + return DateTime.utc() + } + if (typeof d === 'object') { + try { + return DateTime.fromJSDate(d, { zone: 'utc' }) + } catch (error) { + console.warn('Not a valid date', typeof d, d) + return DateTime.utc() + } + } + if (typeof d === 'number') { + const millis = toNumber(d) + if (isNaN(millis)) { + console.warn('Not a valid date', typeof d, d) + return DateTime.utc() + } + return DateTime.fromMillis(millis, { zone: 'UTC' }) + } + const tryParseMethods: DateTimeParseFunction[] = [ + DateTime.fromISO, + DateTime.fromSQL, + DateTime.fromRFC2822, + ] + let result + for (let index = 0; index < tryParseMethods.length; index++) { + const parse = tryParseMethods[index] + try { + result = parse(d, { zone: 'UTC' }) + if (result.isValid) { + return result + } + } catch (e) { + console.warn('Not a valid date', typeof d, d) + return DateTime.utc() + } + } + console.warn('Not a valid date', typeof d, d) + return DateTime.utc() +} + +export const getISODateByInterval = ( + date: SupportedDateType, + timeChunkInterval: FourwingsInterval +) => { + if (!date) { + return '' + } + const dateTime = getUTCDateTime(date) + switch (timeChunkInterval) { + case 'YEAR': + return dateTime.toFormat('yyyy') as string + case 'MONTH': + return dateTime.toFormat('yyyy-MM') as string + case 'DAY': + return dateTime.toFormat('yyyy-MM-dd') as string + case 'HOUR': + return dateTime.toFormat('yyyy-MM-ddTHH:00:00') as string + default: + return dateTime.toISO() as string + } +} + +export const formatDateForInterval = (date: DateTime, timeChunkInterval: FourwingsInterval) => { + let formattedTick = '' + switch (timeChunkInterval) { + case 'YEAR': + formattedTick = date.year.toString() + break + case 'MONTH': + formattedTick = date.toFormat('LLL y') + break + case 'HOUR': + formattedTick = date.toLocaleString(DateTime.DATETIME_MED) + break + default: + formattedTick = date.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY) + break + } + return formattedTick +} diff --git a/libs/data-transforms/src/dates/index.ts b/libs/data-transforms/src/dates/index.ts new file mode 100644 index 0000000000..33acf19ed6 --- /dev/null +++ b/libs/data-transforms/src/dates/index.ts @@ -0,0 +1 @@ +export * from './dates' diff --git a/libs/data-transforms/src/index.ts b/libs/data-transforms/src/index.ts index 76f7c95b41..7f878c0248 100644 --- a/libs/data-transforms/src/index.ts +++ b/libs/data-transforms/src/index.ts @@ -1,5 +1,6 @@ export * from './buffer' export * from './coordinates' +export * from './dates' export * from './dissolve' export * from './events' export * from './features' diff --git a/libs/data-transforms/src/list-to-track-segments/list-to-track-segments.test.ts b/libs/data-transforms/src/list-to-track-segments/list-to-track-segments.test.ts index d3a53cad3d..5feae035b4 100644 --- a/libs/data-transforms/src/list-to-track-segments/list-to-track-segments.test.ts +++ b/libs/data-transforms/src/list-to-track-segments/list-to-track-segments.test.ts @@ -2,7 +2,7 @@ import fs from 'fs' import path from 'path' import { parse } from 'papaparse' import { guessColumn } from '../schema/guess-columns' -import { getUTCDate } from '../schema' +import { getUTCDate } from '../dates' import { listToTrackSegments } from './list-to-track-segments' import { checkRecordValidity } from './check-record-validity' diff --git a/libs/data-transforms/src/list-to-track-segments/list-to-track-segments.ts b/libs/data-transforms/src/list-to-track-segments/list-to-track-segments.ts index 9b632ce0a0..2692a9f509 100644 --- a/libs/data-transforms/src/list-to-track-segments/list-to-track-segments.ts +++ b/libs/data-transforms/src/list-to-track-segments/list-to-track-segments.ts @@ -2,7 +2,8 @@ import { groupBy } from 'es-toolkit' import type { TrackSegment } from '@globalfishingwatch/api-types' import type { SegmentColumns } from '../types' import { parseCoords } from '../coordinates' -import { getUTCDate, normalizePropertiesKeys } from '../schema' +import { normalizePropertiesKeys } from '../schema' +import { getUTCDate } from '../dates' type Args = SegmentColumns & { records: Record[] diff --git a/libs/data-transforms/src/points/points-to-geojson.ts b/libs/data-transforms/src/points/points-to-geojson.ts index ec74dbdb52..55fef054b5 100644 --- a/libs/data-transforms/src/points/points-to-geojson.ts +++ b/libs/data-transforms/src/points/points-to-geojson.ts @@ -2,7 +2,8 @@ import type { Feature, FeatureCollection, GeoJsonProperties, Point } from 'geojs import type { DatasetSchema, DatasetSchemaItem } from '@globalfishingwatch/api-types' import type { PointColumns } from '../types' import { parseCoords } from '../coordinates' -import { getUTCDate, normalizePropertiesKeys } from '../schema' +import { normalizePropertiesKeys } from '../schema' +import { getUTCDate } from '../dates' export const cleanProperties = ( object: GeoJsonProperties, diff --git a/libs/data-transforms/src/schema/schema.ts b/libs/data-transforms/src/schema/schema.ts index f40cdac26d..7ddd646f3c 100644 --- a/libs/data-transforms/src/schema/schema.ts +++ b/libs/data-transforms/src/schema/schema.ts @@ -1,11 +1,8 @@ import { uniq } from 'es-toolkit' import snakeCase from 'lodash/snakeCase' -import toNumber from 'lodash/toNumber' import max from 'lodash/max' import min from 'lodash/min' import type { FeatureCollection } from 'geojson' -import type { DateTimeOptions } from 'luxon' -import { DateTime } from 'luxon' import type { Dataset, DatasetConfigurationUI, @@ -21,34 +18,6 @@ type GetFieldSchemaParams = { } const MAX_SCHEMA_ENUM_VALUES = 100 -type DateTimeParseFunction = { (timestamp: string, opts: DateTimeOptions | undefined): DateTime } - -export const getUTCDate = (timestamp: string | number = Date.now()) => { - // it could receive a timestamp as a string - const millis = toNumber(timestamp) - if (typeof timestamp === 'number' || !isNaN(millis)) - return DateTime.fromMillis(millis, { zone: 'UTC' }).toJSDate() - - const tryParseMethods: DateTimeParseFunction[] = [ - DateTime.fromISO, - DateTime.fromSQL, - DateTime.fromRFC2822, - ] - let result - for (let index = 0; index < tryParseMethods.length; index++) { - const parse = tryParseMethods[index] - try { - result = parse(timestamp, { zone: 'UTC' }) - if (result.isValid) { - return result.toJSDate() - } - } catch (e) { - return new Date('Invalid Date') - } - } - return new Date('Invalid Date') -} - export const normalizePropertiesKeys = (object: Record | null) => { return Object.entries(object || {}).reduce( (acc, [key, value]) => { diff --git a/tsconfig.base.json b/tsconfig.base.json index 2fe81651c9..2799cfb481 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -23,6 +23,7 @@ "@globalfishingwatch/api-client": ["libs/api-client/src/index.ts"], "@globalfishingwatch/api-types": ["libs/api-types/src/index.ts"], "@globalfishingwatch/data-transforms": ["libs/data-transforms/src/index.ts"], + "@globalfishingwatch/data-transforms/*": ["libs/data-transforms/src/*"], "@globalfishingwatch/datasets-client": ["libs/datasets-client/src/index.ts"], "@globalfishingwatch/dataviews-client": ["libs/dataviews-client/src/index.ts"], "@globalfishingwatch/deck-layer-composer": ["libs/deck-layer-composer/src/index.ts"], From 31b3c92d44790342514b7043d0612b0f1f01ef67 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Tue, 14 Jan 2025 11:15:57 +0100 Subject: [PATCH 30/62] fix timeseries in hour --- libs/data-transforms/src/dates/dates.ts | 2 +- .../responsive-visualizations/src/charts/config.ts | 1 - .../src/charts/timeseries/TimeseriesIndividual.tsx | 12 ------------ .../src/charts/timeseries/timeseries.hooks.ts | 14 +++++++------- 4 files changed, 8 insertions(+), 21 deletions(-) diff --git a/libs/data-transforms/src/dates/dates.ts b/libs/data-transforms/src/dates/dates.ts index 1ca343851d..b45e7ce3ce 100644 --- a/libs/data-transforms/src/dates/dates.ts +++ b/libs/data-transforms/src/dates/dates.ts @@ -91,7 +91,7 @@ export const getISODateByInterval = ( case 'DAY': return dateTime.toFormat('yyyy-MM-dd') as string case 'HOUR': - return dateTime.toFormat('yyyy-MM-ddTHH:00:00') as string + return dateTime.toFormat("yyyy-MM-dd'T'HH:00:00") as string default: return dateTime.toISO() as string } diff --git a/libs/responsive-visualizations/src/charts/config.ts b/libs/responsive-visualizations/src/charts/config.ts index 7e7f57754c..f752fc6df9 100644 --- a/libs/responsive-visualizations/src/charts/config.ts +++ b/libs/responsive-visualizations/src/charts/config.ts @@ -11,5 +11,4 @@ export const POINT_GAP = 3 export const AXIS_LABEL_PADDING = 34 export const TIMESERIES_PADDING = 6 -// TODO use this also in the isIndividualSupported export const MAX_INDIVIDUAL_ITEMS = 1000 diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx index dd0548ae0b..dbb182a631 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx @@ -38,7 +38,6 @@ export function IndividualTimeseries({ return ( - {/* */} tickLabelFormatter?.(tick, timeseriesInterval) || tick} axisLine={true} /> - {/* TODO: restore this and align with the points */} - {/* */} {fullTimeseries?.length && }
              { if (start && end && timeseriesInterval) { - const cleanEnd = DateTime.fromISO(end, { zone: 'utc' }) + const cleanEnd = getUTCDateTime(end) .minus({ [timeseriesInterval]: 1 }) - .toISO() as string - return [new Date(start).getTime(), new Date(cleanEnd).getTime()] + .toMillis() as number + return [getUTCDateTime(end).toMillis(), cleanEnd] } return [] }, [start, end, timeseriesInterval]) @@ -47,9 +48,8 @@ export function useFullTimeseries({ if (!data || !dateKey || !valueKey) { return [] } - - const startMillis = DateTime.fromISO(start).toMillis() - const endMillis = DateTime.fromISO(end).toMillis() + const startMillis = getUTCDateTime(start).toMillis() + const endMillis = getUTCDateTime(end).toMillis() const intervalDiff = Math.floor( Duration.fromMillis(endMillis - startMillis).as( @@ -60,7 +60,7 @@ export function useFullTimeseries({ return Array(intervalDiff) .fill(0) .map((_, i) => { - const d = DateTime.fromMillis(startMillis, { zone: 'UTC' }) + const d = getUTCDateTime(startMillis) .plus({ [timeseriesInterval]: i }) .toISO() const dataValue = data.find((item) => From ad22fc1e8b58674790a83e245c8a256a4b02843c Mon Sep 17 00:00:00 2001 From: j8seangel Date: Tue, 14 Jan 2025 12:15:51 +0100 Subject: [PATCH 31/62] render individual points in vessel group events vessels --- .../vessel-groups/events/VGREvents.tsx | 8 +- ...selGroupReportVesselsIndividualTooltip.tsx | 20 ++- .../vessel-group-report-vessels.selectors.ts | 159 +++++++++++------- 3 files changed, 119 insertions(+), 68 deletions(-) diff --git a/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx b/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx index 14ff2558c1..376c1447fc 100644 --- a/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx @@ -39,7 +39,10 @@ import { selectVGREventsVesselsPagination, } from 'features/reports/vessel-groups/events/vgr-events.selectors' import { formatI18nNumber } from 'features/i18n/i18nNumber' -import { selectVGRVesselDatasetsWithoutEventsRelated } from 'features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors' +import { + selectVGRVesselDatasetsWithoutEventsRelated, + selectVGREventsVesselsIndividualData, +} from 'features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors' import { selectVesselsDatasets } from 'features/datasets/datasets.selectors' import { getDatasetLabel } from 'features/datasets/datasets.utils' import EventsEmptyState from 'assets/images/emptyState-events@2x.png' @@ -54,11 +57,11 @@ function VGREvents() { const vesselGroupId = useSelector(selectReportVesselGroupId) const filter = useSelector(selectVGREventsVesselFilter) const eventsDataview = useSelector(selectVGREventsSubsectionDataview) - console.log('🚀 ~ VGREvents ~ eventsDataview:', eventsDataview) const vesselsGroupByProperty = useSelector(selectVGREventsVesselsProperty) const vesselsWithEvents = useSelector(selectVGREventsVessels) const vesselFlags = useSelector(selectVGREventsVesselsFlags) const vesselGroups = useSelector(selectVGREventsVesselsGrouped) + const individualData = useSelector(selectVGREventsVesselsIndividualData) const vesselsPaginated = useSelector(selectVGREventsVesselsPaginated) const { start, end } = useSelector(selectTimeRange) const vesselDatasets = useSelector(selectVesselsDatasets) @@ -192,6 +195,7 @@ function VGREvents() {
              { - if (!data?.identity) { + if (!data) { return null } + const getVesselPropertyParams = { identitySource: VesselIdentitySourceEnum.SelfReported, } + const vesselName = formatInfoField( - getVesselProperty(data.identity, 'shipname', getVesselPropertyParams), + data.identity + ? getVesselProperty(data.identity, 'shipname', getVesselPropertyParams) + : data.shipName, 'shipname' ) - const vesselFlag = getVesselProperty(data.identity, 'flag', getVesselPropertyParams) + const vesselFlag = data.identity + ? getVesselProperty(data.identity, 'flag', getVesselPropertyParams) + : data.flag - const vesselType = getVesselShipTypeLabel({ - shiptypes: getVesselProperty(data.identity, 'shiptypes', getVesselPropertyParams), - }) + const vesselType = data.identity + ? getVesselShipTypeLabel({ + shiptypes: getVesselProperty(data.identity, 'shiptypes', getVesselPropertyParams), + }) + : data.vesselType return ( {`${vesselName} ${vesselFlag ? `(${vesselFlag})` : ''} ${vesselType ? `- ${vesselType}` : ''}`} diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts b/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts index e1094fa46c..8e8adb7f77 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts @@ -1,6 +1,6 @@ import { createSelector } from '@reduxjs/toolkit' import { groupBy } from 'es-toolkit' -import type { Dataset, IdentityVessel } from '@globalfishingwatch/api-types' +import type { Dataset, IdentityVessel, Vessel } from '@globalfishingwatch/api-types' import { DatasetTypes, VesselIdentitySourceEnum } from '@globalfishingwatch/api-types' import type { ResponsiveVisualizationData } from '@globalfishingwatch/responsive-visualizations' import { OTHER_CATEGORY_LABEL } from 'features/reports/vessel-groups/vessel-group-report.config' @@ -9,6 +9,7 @@ import { selectVGRVesselsResultsPerPage, selectVGRVesselFilter, selectVGRVesselPage, + selectVGREventsVesselsProperty, } from 'features/reports/vessel-groups/vessel-group.config.selectors' import { EMPTY_FIELD_PLACEHOLDER, @@ -30,7 +31,13 @@ import type { VesselGroupVesselIdentity } from 'features/vessel-groups/vessel-gr import { MAX_CATEGORIES } from 'features/reports/areas/area-reports.config' import { selectVesselsDatasets } from 'features/datasets/datasets.selectors' import { getRelatedDatasetByType } from 'features/datasets/datasets.utils' +import type { + VGREventsVesselsProperty, + VGRSubsection, +} from 'features/vessel-groups/vessel-groups.types' +import type { EventsStatsVessel } from 'features/reports/ports/ports-report.slice' import { selectVGRVessels } from '../vessel-group-report.slice' +import { selectVGREventsVesselsFiltered } from '../events/vgr-events.selectors' import type { VesselGroupReportVesselParsed } from './vessel-group-report-vessels.types' const getVesselSource = (vessel: IdentityVessel) => { @@ -259,28 +266,46 @@ export const selectVGRVesselsGraphAggregatedData = createSelector( } ) -export const selectVGRVesselsGraphIndividualData = createSelector( - [selectVGRVesselsFiltered, selectVGRVesselsSubsection], - (vessels, subsection) => { - if (!vessels) return [] - let vesselsGrouped = {} - switch (subsection) { - case 'flag': - vesselsGrouped = groupBy(vessels, (vessel) => vessel.flagTranslatedClean) - break - case 'shiptypes': - vesselsGrouped = groupBy(vessels, (vessel) => vessel.vesselType.split(', ')[0]) - break - case 'geartypes': - vesselsGrouped = groupBy(vessels, (vessel) => vessel.geartype.split(', ')[0]) - break - case 'source': - vesselsGrouped = groupBy(vessels, (vessel) => vessel.source) +function getVesselIndividualGroupedData( + vessels: (EventsStatsVessel | VesselGroupVesselTableParsed)[], + groupByProperty: VGRSubsection | VGREventsVesselsProperty +) { + if (!vessels?.length) { + return [] + } + let vesselsGrouped = {} + switch (groupByProperty) { + case 'flag': { + vesselsGrouped = groupBy( + vessels, + (vessel) => + (vessel as VesselGroupVesselTableParsed).flagTranslatedClean || + vessel.flagTranslated || + vessel.flag + ) + break + } + case 'shiptype': + case 'shiptypes': { + vesselsGrouped = groupBy(vessels, (vessel) => + (vessel as VesselGroupVesselTableParsed).vesselType + ? (vessel as VesselGroupVesselTableParsed).vesselType.split(', ')[0] + : (vessel as EventsStatsVessel).shiptypes[0] + ) + break + } + case 'geartype': + case 'geartypes': { + vesselsGrouped = groupBy(vessels, (vessel) => vessel.geartype.split(', ')[0]) + break + } + case 'source': { + vesselsGrouped = groupBy(vessels, (vessel) => (vessel as VesselGroupVesselTableParsed).source) + break } - const orderedGroups: ResponsiveVisualizationData< - 'individual', - { name: string; values: any[] } - > = Object.entries(vesselsGrouped) + } + const orderedGroups: ResponsiveVisualizationData<'individual', { name: string; values: any[] }> = + Object.entries(vesselsGrouped) .map(([key, value]) => ({ name: key, values: value as any[], @@ -288,46 +313,60 @@ export const selectVGRVesselsGraphIndividualData = createSelector( .sort((a, b) => { return b.values.length - a.values.length }) - const groupsWithoutOther: ResponsiveVisualizationData< - 'individual', - { name: string; values: any[] } - > = [] - const otherGroups: ResponsiveVisualizationData<'individual', { name: string; values: any[] }> = - [] - orderedGroups.forEach((group) => { - if ( - group.name === 'null' || - group.name.toLowerCase() === OTHER_CATEGORY_LABEL.toLowerCase() || - group.name === EMPTY_FIELD_PLACEHOLDER - ) { - otherGroups.push(group) - } else { - groupsWithoutOther.push(group) - } - }) - const allGroups = - otherGroups.length > 0 - ? [ - ...groupsWithoutOther, - { - name: OTHER_CATEGORY_LABEL, - values: otherGroups, - }, - ] - : groupsWithoutOther - if (allGroups.length <= MAX_CATEGORIES) { - return allGroups as ResponsiveVisualizationData<'individual'> + const groupsWithoutOther: ResponsiveVisualizationData< + 'individual', + { name: string; values: any[] } + > = [] + const otherGroups: ResponsiveVisualizationData<'individual', { name: string; values: any[] }> = [] + orderedGroups.forEach((group) => { + if ( + group.name === 'null' || + group.name.toLowerCase() === OTHER_CATEGORY_LABEL.toLowerCase() || + group.name === EMPTY_FIELD_PLACEHOLDER + ) { + otherGroups.push(group) + } else { + groupsWithoutOther.push(group) } - const firstGroups = allGroups.slice(0, MAX_CATEGORIES) - const restOfGroups = allGroups.slice(MAX_CATEGORIES) + }) + const allGroups = + otherGroups.length > 0 + ? [ + ...groupsWithoutOther, + { + name: OTHER_CATEGORY_LABEL, + values: otherGroups.flatMap((group) => group.values), + }, + ] + : groupsWithoutOther + if (allGroups.length <= MAX_CATEGORIES) { + return allGroups as ResponsiveVisualizationData<'individual'> + } + const firstGroups = allGroups.slice(0, MAX_CATEGORIES) + const restOfGroups = allGroups.slice(MAX_CATEGORIES) - return [ - ...firstGroups, - { - name: OTHER_CATEGORY_LABEL, - values: restOfGroups, - }, - ] as ResponsiveVisualizationData<'individual'> + return [ + ...firstGroups, + { + name: OTHER_CATEGORY_LABEL, + values: restOfGroups.flatMap((group) => group.values), + }, + ] as ResponsiveVisualizationData<'individual'> +} + +export const selectVGRVesselsGraphIndividualData = createSelector( + [selectVGRVesselsFiltered, selectVGRVesselsSubsection], + (vessels, groupBy) => { + if (!vessels || !groupBy) return [] + return getVesselIndividualGroupedData(vessels, groupBy) + } +) + +export const selectVGREventsVesselsIndividualData = createSelector( + [selectVGREventsVesselsFiltered, selectVGREventsVesselsProperty], + (vessels, groupBy) => { + if (!vessels || !groupBy) return [] + return getVesselIndividualGroupedData(vessels, groupBy) } ) From 18f13c41246aa21d11a8a663a9bc622bbc5bc1a9 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Tue, 14 Jan 2025 12:20:30 +0100 Subject: [PATCH 32/62] render individual points in port report events vessels --- .../features/reports/ports/PortsReport.tsx | 3 +++ .../reports/ports/ports-report.selectors.ts | 13 ++++++++++++- .../vessel-group-report-vessels.selectors.ts | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/fishing-map/features/reports/ports/PortsReport.tsx b/apps/fishing-map/features/reports/ports/PortsReport.tsx index fc900e59fe..98e0875b18 100644 --- a/apps/fishing-map/features/reports/ports/PortsReport.tsx +++ b/apps/fishing-map/features/reports/ports/PortsReport.tsx @@ -31,6 +31,7 @@ import { useFetchPortsReport } from './ports-report.hooks' import { selectPortReportsDataview, selectPortReportVesselsGrouped, + selectPortReportVesselsIndividualData, selectPortReportVesselsPaginated, selectPortReportVesselsPagination, } from './ports-report.selectors' @@ -66,6 +67,7 @@ function PortsReport() { const portsReportData = useSelector(selectPortsReportData) const portsReportDataStatus = useSelector(selectPortsReportStatus) const portsReportVesselsGrouped = useSelector(selectPortReportVesselsGrouped) + const portReportIndividualData = useSelector(selectPortReportVesselsIndividualData) const portsReportVesselsPaginated = useSelector(selectPortReportVesselsPaginated) const { data, @@ -222,6 +224,7 @@ function PortsReport() {
              { + if (!vessels || !groupBy) return [] + return getVesselIndividualGroupedData(vessels, groupBy) + } +) + export const selectPortReportVesselsPaginated = createSelector( [ selectPortReportVesselsFiltered, diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts b/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts index 8e8adb7f77..4e9181a90e 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts @@ -266,7 +266,7 @@ export const selectVGRVesselsGraphAggregatedData = createSelector( } ) -function getVesselIndividualGroupedData( +export function getVesselIndividualGroupedData( vessels: (EventsStatsVessel | VesselGroupVesselTableParsed)[], groupByProperty: VGRSubsection | VGREventsVesselsProperty ) { From e74da3eaded9f6162555345d95eb8793083bb6d5 Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Tue, 14 Jan 2025 13:06:24 +0100 Subject: [PATCH 33/62] use event icons in individual timeseries --- .../shared/events/EventsReportGraph.tsx | 8 ++++--- .../shared/events/icons/event-encounter.svg | 7 ++++++ .../shared/events/icons/event-loitering.svg | 7 ++++++ .../shared/events/icons/event-port.svg | 11 +++++++++ .../vessel-groups/events/VGREvents.tsx | 21 +++++++++++++---- .../charts/points/IndividualPoint.module.css | 9 ++++---- .../src/charts/points/IndividualPoint.tsx | 23 ++++++++++++++----- .../src/charts/timeseries/Timeseries.tsx | 2 ++ .../timeseries/TimeseriesIndividual.tsx | 4 ++++ .../src/charts/types.ts | 1 + 10 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 apps/fishing-map/features/reports/shared/events/icons/event-encounter.svg create mode 100644 apps/fishing-map/features/reports/shared/events/icons/event-loitering.svg create mode 100644 apps/fishing-map/features/reports/shared/events/icons/event-port.svg diff --git a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx index 5b63381fee..47c2df4181 100644 --- a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx +++ b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx @@ -1,10 +1,10 @@ +import type { ReactElement } from 'react' import React, { useCallback } from 'react' -import { useTranslation } from 'react-i18next' import { DateTime } from 'luxon' import { groupBy } from 'es-toolkit' import { stringify } from 'qs' -import { getEventsStatsQuery } from 'queries/report-events-stats-api' import type { BaseReportEventsVesselsParamsFilters } from 'queries/report-events-stats-api' +import { getEventsStatsQuery } from 'queries/report-events-stats-api' import { getFourwingsInterval, type FourwingsInterval } from '@globalfishingwatch/deck-loaders' import type { BaseResponsiveTimeseriesProps } from '@globalfishingwatch/responsive-visualizations' import { ResponsiveTimeseries } from '@globalfishingwatch/responsive-visualizations' @@ -76,6 +76,7 @@ export default function EventsReportGraph({ end, start, timeseries, + icon, }: { datasetId: string filters?: BaseReportEventsVesselsParamsFilters @@ -84,9 +85,9 @@ export default function EventsReportGraph({ end: string start: string timeseries: { date: string; value: number }[] + icon?: ReactElement }) { const containerRef = React.useRef(null) - const { t } = useTranslation() const startMillis = DateTime.fromISO(start).toMillis() const endMillis = DateTime.fromISO(end).toMillis() @@ -130,6 +131,7 @@ export default function EventsReportGraph({ aggregatedTooltip={} individualTooltip={} color={color} + individualIcon={icon} />
              ) diff --git a/apps/fishing-map/features/reports/shared/events/icons/event-encounter.svg b/apps/fishing-map/features/reports/shared/events/icons/event-encounter.svg new file mode 100644 index 0000000000..fb3c3821eb --- /dev/null +++ b/apps/fishing-map/features/reports/shared/events/icons/event-encounter.svg @@ -0,0 +1,7 @@ + + + Rectangle Copy 4 + + + + \ No newline at end of file diff --git a/apps/fishing-map/features/reports/shared/events/icons/event-loitering.svg b/apps/fishing-map/features/reports/shared/events/icons/event-loitering.svg new file mode 100644 index 0000000000..fb22d59d69 --- /dev/null +++ b/apps/fishing-map/features/reports/shared/events/icons/event-loitering.svg @@ -0,0 +1,7 @@ + + + Combined Shape Copy 3 + + + + \ No newline at end of file diff --git a/apps/fishing-map/features/reports/shared/events/icons/event-port.svg b/apps/fishing-map/features/reports/shared/events/icons/event-port.svg new file mode 100644 index 0000000000..31d5aba2dd --- /dev/null +++ b/apps/fishing-map/features/reports/shared/events/icons/event-port.svg @@ -0,0 +1,11 @@ + + + + square + Created with Sketch. + + + + + + \ No newline at end of file diff --git a/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx b/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx index 376c1447fc..9742863453 100644 --- a/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx @@ -1,18 +1,19 @@ import { useSelector } from 'react-redux' +import type { ReactElement } from 'react' import { Fragment } from 'react' import parse from 'html-react-parser' import { DateTime } from 'luxon' import { useTranslation } from 'react-i18next' import { lowerCase } from 'es-toolkit' -import { - useGetReportEventsStatsQuery, - useGetReportEventsVesselsQuery, -} from 'queries/report-events-stats-api' import type { ReportEventsStatsResponseGroups, ReportEventsVesselsParams, ReportEventsStatsParams, } from 'queries/report-events-stats-api' +import { + useGetReportEventsStatsQuery, + useGetReportEventsVesselsQuery, +} from 'queries/report-events-stats-api' import { Icon } from '@globalfishingwatch/ui-components' import { DatasetTypes } from '@globalfishingwatch/api-types' import { getDataviewFilters } from '@globalfishingwatch/dataviews-client' @@ -50,6 +51,9 @@ import ReportEventsPlaceholder from 'features/reports/shared/placeholders/Report import { selectVGREventsVesselsPaginated } from 'features/reports/vessel-groups/events/vgr-events.selectors' import { selectTimeRange } from 'features/app/selectors/app.timebar.selectors' import VGREventsVesselsTableFooter from '../../shared/events/EventsReportVesselsTableFooter' +import EncounterIcon from '../../shared/events/icons/event-encounter.svg' +import LoiteringIcon from '../../shared/events/icons/event-loitering.svg' +import PortVisitIcon from '../../shared/events/icons/event-port.svg' import styles from './VGREvents.module.css' function VGREvents() { @@ -136,6 +140,14 @@ function VGREvents() { } const eventDataset = eventsDataview?.datasets?.find((d) => d.type === DatasetTypes.Events) const subCategoryDatasetCategory = eventDataset?.subcategory + let icon: ReactElement | undefined + if (subCategoryDatasetCategory === 'encounter') { + icon = + } else if (subCategoryDatasetCategory === 'loitering') { + icon = + } else if (subCategoryDatasetCategory === 'port_visit') { + icon = + } const totalEvents = data.timeseries.reduce((acc, group) => acc + group.value, 0) return ( @@ -182,6 +194,7 @@ function VGREvents() { start={start} end={end} timeseries={data.timeseries || []} + icon={icon} /> )}
              diff --git a/libs/responsive-visualizations/src/charts/points/IndividualPoint.module.css b/libs/responsive-visualizations/src/charts/points/IndividualPoint.module.css index 585ec0051e..7e77342bf9 100644 --- a/libs/responsive-visualizations/src/charts/points/IndividualPoint.module.css +++ b/libs/responsive-visualizations/src/charts/points/IndividualPoint.module.css @@ -1,13 +1,14 @@ .point { - display: block; - background-color: red; - border-radius: 50%; position: relative; border: 2px solid transparent; transition: border-color 0.3s linear; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; } -.point:hover { +.point:not(.withIcon):hover { border: var(--border-thick); } diff --git a/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx b/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx index 12b61d2594..3b0517f5ac 100644 --- a/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx +++ b/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx @@ -1,4 +1,12 @@ -import { useFloating, offset, flip, shift, useInteractions, useHover, FloatingPortal } from '@floating-ui/react' +import { + useFloating, + offset, + flip, + shift, + useInteractions, + useHover, + FloatingPortal, +} from '@floating-ui/react' import { cloneElement, useState, type ReactElement } from 'react' import cx from 'classnames' import type { ResponsiveVisualizationItem } from '../../types' @@ -10,9 +18,10 @@ type IndividualPointProps = { point: ResponsiveVisualizationItem tooltip?: ReactElement className?: string + icon?: ReactElement } -export function IndividualPoint({ point, color, tooltip, className }: IndividualPointProps) { +export function IndividualPoint({ point, color, tooltip, className, icon }: IndividualPointProps) { const [isOpen, setIsOpen] = useState(false) const { refs, floatingStyles, context } = useFloating({ @@ -29,13 +38,14 @@ export function IndividualPoint({ point, color, tooltip, className }: Individual
            • {isOpen && ( @@ -50,6 +60,7 @@ export function IndividualPoint({ point, color, tooltip, className }: Individual
            • )} + {icon && {icon}} ) } diff --git a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx index d5b2c1c248..e41cee96fa 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx @@ -37,6 +37,7 @@ export function ResponsiveTimeseries({ individualTooltip, onIndividualItemClick, onAggregatedItemClick, + individualIcon, }: ResponsiveTimeseriesProps) { const containerRef = useRef(null) const { width, data, isIndividualSupported } = useResponsiveVisualization(containerRef, { @@ -73,6 +74,7 @@ export function ResponsiveTimeseries({ onClick={onIndividualItemClick} tickLabelFormatter={tickLabelFormatter} customTooltip={individualTooltip} + icon={individualIcon} /> ) : ( & { width: number + icon?: ReactElement } export function IndividualTimeseries({ @@ -23,6 +25,7 @@ export function IndividualTimeseries({ timeseriesInterval, tickLabelFormatter, customTooltip, + icon, }: IndividualTimeseriesProps) { const domain = useTimeseriesDomain({ start, end, timeseriesInterval }) const fullTimeseries = useFullTimeseries({ @@ -62,6 +65,7 @@ export function IndividualTimeseries({ point={point} color={color} tooltip={customTooltip} + icon={icon} /> ))}
            diff --git a/libs/responsive-visualizations/src/charts/types.ts b/libs/responsive-visualizations/src/charts/types.ts index 1c8441a346..eea1871879 100644 --- a/libs/responsive-visualizations/src/charts/types.ts +++ b/libs/responsive-visualizations/src/charts/types.ts @@ -22,6 +22,7 @@ export type BaseResponsiveChartProps = { onIndividualItemClick?: ResponsiveVisualizationInteractionCallback getIndividualData?: () => Promise | undefined> individualValueKey?: keyof ResponsiveVisualizationData<'individual'>[0] + individualIcon?: ReactElement } export type ResponsiveVisualizationAnyItemKey = From 0fd97db9df6bcd64e4450bbbc8990d05c41abd17 Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Tue, 14 Jan 2025 13:22:44 +0100 Subject: [PATCH 34/62] vessel tooltip --- ...pReportVesselsIndividualTooltip.module.css | 19 +++++++++++ ...selGroupReportVesselsIndividualTooltip.tsx | 32 +++++++++++++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.module.css diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.module.css b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.module.css new file mode 100644 index 0000000000..9bbbec8507 --- /dev/null +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.module.css @@ -0,0 +1,19 @@ +.properties { + margin-top: var(--space-S); + margin-bottom: var(--space-XS); + display: flex; + gap: var(--space-S); +} + +.property label { + font: var(--font-XS); +} + +.property span { + font: var(--font-S); +} + +.cta { + color: var(--color-secondary-blue); + font: var(--font-S); +} diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.tsx index a54e19339c..1e5009368f 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.tsx @@ -1,14 +1,17 @@ +import { useTranslation } from 'react-i18next' import { VesselIdentitySourceEnum } from '@globalfishingwatch/api-types' -import { getVesselShipTypeLabel } from 'utils/info' +import { EMPTY_FIELD_PLACEHOLDER, getVesselShipTypeLabel } from 'utils/info' import { formatInfoField } from 'utils/info' import type { VesselGroupVesselTableParsed } from 'features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors' import { getVesselProperty } from 'features/vessel/vessel.utils' +import styles from './VesselGroupReportVesselsIndividualTooltip.module.css' const VesselGroupReportVesselsIndividualTooltip = ({ data, }: { data?: VesselGroupVesselTableParsed }) => { + const { t } = useTranslation() if (!data) { return null } @@ -24,6 +27,10 @@ const VesselGroupReportVesselsIndividualTooltip = ({ 'shipname' ) + const mmsi = data.identity + ? getVesselProperty(data.identity, 'ssvid', getVesselPropertyParams) + : data.ssvid + const vesselFlag = data.identity ? getVesselProperty(data.identity, 'flag', getVesselPropertyParams) : data.flag @@ -32,10 +39,29 @@ const VesselGroupReportVesselsIndividualTooltip = ({ ? getVesselShipTypeLabel({ shiptypes: getVesselProperty(data.identity, 'shiptypes', getVesselPropertyParams), }) - : data.vesselType + : data.vesselType || data.geartype return ( - {`${vesselName} ${vesselFlag ? `(${vesselFlag})` : ''} ${vesselType ? `- ${vesselType}` : ''}`} +
            + {vesselName} +
            +
            + + {mmsi || EMPTY_FIELD_PLACEHOLDER} +
            +
            + + {formatInfoField(vesselFlag, 'flag') || EMPTY_FIELD_PLACEHOLDER} +
            +
            + + {formatInfoField(vesselType, 'shiptypes') || EMPTY_FIELD_PLACEHOLDER} +
            +
            +
            + {t('vessel.clickToSeeMore', 'Click to see more information')} +
            +
            ) } From 0802a68a6a81f9b8bbe2bda2ddd5fec6a1b2984f Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Tue, 14 Jan 2025 14:00:52 +0100 Subject: [PATCH 35/62] encounter tooltip --- .../events/EventsReportGraph.module.css | 19 ++++++ .../shared/events/EventsReportGraph.tsx | 61 +++++++++++++++++-- .../vessel-groups/events/VGREvents.tsx | 40 +++++------- apps/fishing-map/utils/events.tsx | 38 ++++++------ .../src/charts/points/IndividualPoint.tsx | 4 +- 5 files changed, 112 insertions(+), 50 deletions(-) diff --git a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.module.css b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.module.css index 646f38de19..a88d795ee1 100644 --- a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.module.css +++ b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.module.css @@ -36,3 +36,22 @@ .graph :global(.recharts-tooltip-cursor) { stroke: var(--color-secondary-blue); } + +.event { + display: flex; + flex-direction: column; + gap: var(--space-S); +} + +.properties { + display: flex; + gap: var(--space-S); +} + +.property label { + font: var(--font-XS); +} + +.property span { + font: var(--font-S); +} diff --git a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx index 47c2df4181..e2c045c7ae 100644 --- a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx +++ b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx @@ -3,13 +3,14 @@ import React, { useCallback } from 'react' import { DateTime } from 'luxon' import { groupBy } from 'es-toolkit' import { stringify } from 'qs' -import type { BaseReportEventsVesselsParamsFilters } from 'queries/report-events-stats-api' +import { useTranslation } from 'react-i18next' import { getEventsStatsQuery } from 'queries/report-events-stats-api' +import type { BaseReportEventsVesselsParamsFilters } from 'queries/report-events-stats-api' import { getFourwingsInterval, type FourwingsInterval } from '@globalfishingwatch/deck-loaders' import type { BaseResponsiveTimeseriesProps } from '@globalfishingwatch/responsive-visualizations' import { ResponsiveTimeseries } from '@globalfishingwatch/responsive-visualizations' import { GFWAPI } from '@globalfishingwatch/api-client' -import type { ApiEvent, APIPagination } from '@globalfishingwatch/api-types' +import type { ApiEvent, APIPagination, EventType } from '@globalfishingwatch/api-types' import { useMemoCompare } from '@globalfishingwatch/react-hooks' import { getISODateByInterval } from '@globalfishingwatch/data-transforms' import i18n from 'features/i18n/i18n' @@ -17,6 +18,10 @@ import { formatDateForInterval, getUTCDateTime } from 'utils/dates' import { formatI18nNumber } from 'features/i18n/i18nNumber' import { COLOR_PRIMARY_BLUE } from 'features/app/app.config' import { formatInfoField } from 'utils/info' +import { getTimeLabels } from 'utils/events' +import EncounterIcon from '../../shared/events/icons/event-encounter.svg' +import LoiteringIcon from '../../shared/events/icons/event-loitering.svg' +import PortVisitIcon from '../../shared/events/icons/event-port.svg' import styles from './EventsReportGraph.module.css' type EventsReportGraphTooltipProps = { @@ -53,10 +58,39 @@ const AggregatedGraphTooltip = (props: any) => { return null } -const IndividualGraphTooltip = ({ data }: { data?: any }) => { +const IndividualGraphTooltip = ({ data, eventType }: { data?: any; eventType?: EventType }) => { + const { t } = useTranslation() if (!data?.vessel?.name) { return null } + console.log('data:', data) + if (eventType === 'encounter') { + const { start, duration } = getTimeLabels({ start: data.start, end: data.end }) + return ( +
            +
            +
            + + {start} +
            +
            + + {duration} +
            +
            +
            +
            + + {formatInfoField(data.vessel?.name, 'shipname')} +
            +
            + + {formatInfoField(data.encounter?.vessel?.name, 'shipname')} +
            +
            +
            + ) + } return formatInfoField(data.vessel.name, 'shipname') } @@ -76,7 +110,7 @@ export default function EventsReportGraph({ end, start, timeseries, - icon, + eventType, }: { datasetId: string filters?: BaseReportEventsVesselsParamsFilters @@ -85,7 +119,7 @@ export default function EventsReportGraph({ end: string start: string timeseries: { date: string; value: number }[] - icon?: ReactElement + eventType?: EventType }) { const containerRef = React.useRef(null) @@ -95,6 +129,15 @@ export default function EventsReportGraph({ const filtersMemo = useMemoCompare(filters) const includesMemo = useMemoCompare(includes) + let icon: ReactElement | undefined + if (eventType === 'encounter') { + icon = + } else if (eventType === 'loitering') { + icon = + } else if (eventType === 'port_visit') { + icon = + } + const getAggregatedData = useCallback(async () => timeseries, [timeseries]) const getIndividualData = useCallback(async () => { const params = { @@ -110,6 +153,12 @@ export default function EventsReportGraph({ } const data = await GFWAPI.fetch>(`/v3/events?${stringify(params)}`) const groupedData = groupBy(data.entries, (item) => getISODateByInterval(item.start, interval)) + console.log( + Object.entries(groupedData) + .map(([date, events]) => ({ date, values: events })) + .sort((a, b) => a.date.localeCompare(b.date)) + ) + return Object.entries(groupedData) .map(([date, events]) => ({ date, values: events })) .sort((a, b) => a.date.localeCompare(b.date)) @@ -129,7 +178,7 @@ export default function EventsReportGraph({ getIndividualData={getIndividualData} tickLabelFormatter={formatDateTicks} aggregatedTooltip={} - individualTooltip={} + individualTooltip={} color={color} individualIcon={icon} /> diff --git a/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx b/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx index 9742863453..3f2c38ffd4 100644 --- a/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/events/VGREvents.tsx @@ -1,20 +1,20 @@ import { useSelector } from 'react-redux' -import type { ReactElement } from 'react' import { Fragment } from 'react' import parse from 'html-react-parser' import { DateTime } from 'luxon' import { useTranslation } from 'react-i18next' import { lowerCase } from 'es-toolkit' +import { + useGetReportEventsStatsQuery, + useGetReportEventsVesselsQuery, +} from 'queries/report-events-stats-api' import type { ReportEventsStatsResponseGroups, ReportEventsVesselsParams, ReportEventsStatsParams, } from 'queries/report-events-stats-api' -import { - useGetReportEventsStatsQuery, - useGetReportEventsVesselsQuery, -} from 'queries/report-events-stats-api' import { Icon } from '@globalfishingwatch/ui-components' +import type { EventType } from '@globalfishingwatch/api-types' import { DatasetTypes } from '@globalfishingwatch/api-types' import { getDataviewFilters } from '@globalfishingwatch/dataviews-client' import VGREventsSubsectionSelector from 'features/reports/vessel-groups/events/VGREventsSubsectionSelector' @@ -51,9 +51,6 @@ import ReportEventsPlaceholder from 'features/reports/shared/placeholders/Report import { selectVGREventsVesselsPaginated } from 'features/reports/vessel-groups/events/vgr-events.selectors' import { selectTimeRange } from 'features/app/selectors/app.timebar.selectors' import VGREventsVesselsTableFooter from '../../shared/events/EventsReportVesselsTableFooter' -import EncounterIcon from '../../shared/events/icons/event-encounter.svg' -import LoiteringIcon from '../../shared/events/icons/event-loitering.svg' -import PortVisitIcon from '../../shared/events/icons/event-port.svg' import styles from './VGREvents.module.css' function VGREvents() { @@ -139,15 +136,7 @@ function VGREvents() { ) } const eventDataset = eventsDataview?.datasets?.find((d) => d.type === DatasetTypes.Events) - const subCategoryDatasetCategory = eventDataset?.subcategory - let icon: ReactElement | undefined - if (subCategoryDatasetCategory === 'encounter') { - icon = - } else if (subCategoryDatasetCategory === 'loitering') { - icon = - } else if (subCategoryDatasetCategory === 'port_visit') { - icon = - } + const eventType = eventDataset?.subcategory as EventType const totalEvents = data.timeseries.reduce((acc, group) => acc + group.value, 0) return ( @@ -166,11 +155,8 @@ function VGREvents() { flags: vesselFlags?.size, activityQuantity: totalEvents, activityUnit: `${ - subCategoryDatasetCategory !== undefined - ? t( - `common.eventLabels.${subCategoryDatasetCategory.toLowerCase()}`, - lowerCase(subCategoryDatasetCategory) - ) + eventType !== undefined + ? t(`common.eventLabels.${eventType.toLowerCase()}`, lowerCase(eventType)) : '' } ${(t('common.events', 'events') as string).toLowerCase()}`, start: formatI18nDate(start, { @@ -189,12 +175,18 @@ function VGREvents() { vesselGroupId, ...(eventsDataview && { ...getDataviewFilters(eventsDataview) }), }} - includes={['id', 'start', 'end', 'vessel']} + includes={[ + 'id', + 'start', + 'end', + 'vessel', + ...(eventType === 'encounter' ? ['encounter.vessel'] : []), + ]} color={color} start={start} end={end} timeseries={data.timeseries || []} - icon={icon} + eventType={eventType} /> )}
            diff --git a/apps/fishing-map/utils/events.tsx b/apps/fishing-map/utils/events.tsx index 63caa1013b..834323c8de 100644 --- a/apps/fishing-map/utils/events.tsx +++ b/apps/fishing-map/utils/events.tsx @@ -4,13 +4,13 @@ import { DateTime } from 'luxon' import { Trans } from 'react-i18next' import type { ApiEvent } from '@globalfishingwatch/api-types' import { EventTypes } from '@globalfishingwatch/api-types' +import type { SupportedDateType } from '@globalfishingwatch/data-transforms' import { t } from 'features/i18n/i18n' import { formatI18nDate } from 'features/i18n/i18nDate' import { EVENTS_COLORS } from 'data/config' import { formatInfoField } from 'utils/info' import VesselPin from 'features/vessel/VesselPin' import { DEFAULT_VESSEL_IDENTITY_ID } from 'features/vessel/vessel.config' -import type { SupportedDateType } from './dates' import { getUTCDateTime } from './dates' const getEventColors = ({ type }: { type: ApiEvent['type'] }) => { @@ -27,11 +27,28 @@ const getEventColors = ({ type }: { type: ApiEvent['type'] }) => { } } +const getEventDurationLabel = ({ durationRaw }: { durationRaw: Duration }): string => { + const duration = durationRaw.toObject() + return [ + duration.days && duration.days > 0 + ? t('event.dayAbbreviated', '{{count}}d', { count: duration.days }) + : '', + duration.hours && duration.hours > 0 && durationRaw.as('days') < 10 + ? t('event.hourAbbreviated', '{{count}}h', { count: duration.hours }) + : '', + duration.minutes && duration.minutes > 0 && durationRaw.as('hours') < 10 + ? t('event.minuteAbbreviated', '{{count}}m', { + count: Math.round(duration.minutes as number), + }) + : '', + ].join(' ') +} + type TimeLabels = { start: string duration: string } -const getTimeLabels = ({ +export const getTimeLabels = ({ start, end, }: { @@ -51,23 +68,6 @@ const getTimeLabels = ({ } } -const getEventDurationLabel = ({ durationRaw }: { durationRaw: Duration }): string => { - const duration = durationRaw.toObject() - return [ - duration.days && duration.days > 0 - ? t('event.dayAbbreviated', '{{count}}d', { count: duration.days }) - : '', - duration.hours && duration.hours > 0 && durationRaw.as('days') < 10 - ? t('event.hourAbbreviated', '{{count}}h', { count: duration.hours }) - : '', - duration.minutes && duration.minutes > 0 && durationRaw.as('hours') < 10 - ? t('event.minuteAbbreviated', '{{count}}m', { - count: Math.round(duration.minutes as number), - }) - : '', - ].join(' ') -} - export const getEventDescription = ({ start, end, diff --git a/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx b/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx index 3b0517f5ac..b2064d51f7 100644 --- a/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx +++ b/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx @@ -56,7 +56,9 @@ export function IndividualPoint({ point, color, tooltip, className, icon }: Indi style={floatingStyles} {...getFloatingProps()} > - {tooltip ? cloneElement(tooltip, { data: point } as any) : point.name} + {tooltip + ? cloneElement(tooltip, { ...(tooltip.props || {}), data: point } as any) + : point.name}
          )} From f625c642acc3ee00497d9d376f4e1f35428f8a11 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Tue, 14 Jan 2025 17:28:32 +0100 Subject: [PATCH 36/62] fix build --- apps/fishing-map/features/i18n/i18nDate.tsx | 2 +- apps/fishing-map/tsconfig.json | 1 + .../src/charts/timeseries/timeseries.hooks.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/fishing-map/features/i18n/i18nDate.tsx b/apps/fishing-map/features/i18n/i18nDate.tsx index eaf5f069ef..104b2d1d95 100644 --- a/apps/fishing-map/features/i18n/i18nDate.tsx +++ b/apps/fishing-map/features/i18n/i18nDate.tsx @@ -2,8 +2,8 @@ import { Fragment } from 'react' import type { DateTimeFormatOptions } from 'luxon' import { DateTime } from 'luxon' import { useTranslation } from 'react-i18next' +import type { SupportedDateType } from '@globalfishingwatch/data-transforms' import type { Locale } from 'types' -import type { SupportedDateType } from 'utils/dates' import { getUTCDateTime } from 'utils/dates' import i18n from './i18n' diff --git a/apps/fishing-map/tsconfig.json b/apps/fishing-map/tsconfig.json index 0229d060cd..1aff8d629e 100644 --- a/apps/fishing-map/tsconfig.json +++ b/apps/fishing-map/tsconfig.json @@ -21,6 +21,7 @@ "@globalfishingwatch/api-client": ["../../libs/api-client/src/index.ts"], "@globalfishingwatch/api-types": ["../../libs/api-types/src/index.ts"], "@globalfishingwatch/data-transforms": ["../../libs/data-transforms/src/index.ts"], + "@globalfishingwatch/data-transforms/*": ["../../libs/data-transforms/src/*"], "@globalfishingwatch/datasets-client": ["../../libs/datasets-client/src/index.ts"], "@globalfishingwatch/dataviews-client": ["../../libs/dataviews-client/src/index.ts"], "@globalfishingwatch/deck-layer-composer": ["../../libs/deck-layer-composer/src/index.ts"], diff --git a/libs/responsive-visualizations/src/charts/timeseries/timeseries.hooks.ts b/libs/responsive-visualizations/src/charts/timeseries/timeseries.hooks.ts index da901b54bc..f37b970de6 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/timeseries.hooks.ts +++ b/libs/responsive-visualizations/src/charts/timeseries/timeseries.hooks.ts @@ -1,5 +1,5 @@ import type { DurationUnit } from 'luxon' -import { DateTime, Duration } from 'luxon' +import { Duration } from 'luxon' import { useMemo } from 'react' import type { FourwingsInterval } from '@globalfishingwatch/deck-loaders' import { getUTCDateTime } from '@globalfishingwatch/data-transforms/dates' From 5bb0e00831bcef0f2983352e4dc14e7430651daa Mon Sep 17 00:00:00 2001 From: j8seangel Date: Tue, 14 Jan 2025 18:08:05 +0100 Subject: [PATCH 37/62] calculate pointSize based on space --- .../reports/ports/ports-report.selectors.ts | 6 +- .../events/vgr-events.selectors.ts | 10 +-- .../vessel-group-report.config.ts | 8 +++ .../vessel-group-report-vessels.selectors.ts | 16 ++--- .../src/charts/barchart/BarChart.tsx | 20 +++--- .../charts/barchart/BarChartIndividual.tsx | 4 +- .../src/charts/config.ts | 6 +- .../src/charts/hooks.ts | 63 +++++++++++-------- .../src/charts/points/IndividualPoint.tsx | 16 +++-- .../timeseries/TimeseriesIndividual.tsx | 10 ++- .../src/lib/density.ts | 39 +++++++----- 11 files changed, 121 insertions(+), 77 deletions(-) diff --git a/apps/fishing-map/features/reports/ports/ports-report.selectors.ts b/apps/fishing-map/features/reports/ports/ports-report.selectors.ts index 109dd3a134..cc7c5361a0 100644 --- a/apps/fishing-map/features/reports/ports/ports-report.selectors.ts +++ b/apps/fishing-map/features/reports/ports/ports-report.selectors.ts @@ -10,11 +10,11 @@ import { TEMPLATE_VESSEL_DATAVIEW_SLUG, } from 'data/workspaces' import { selectAllDataviews } from 'features/dataviews/dataviews.slice' +import { getVesselIndividualGroupedData } from '../vessel-groups/vessels/vessel-group-report-vessels.selectors' import { - getVesselIndividualGroupedData, + OTHER_CATEGORY_LABEL, REPORT_FILTER_PROPERTIES, -} from '../vessel-groups/vessels/vessel-group-report-vessels.selectors' -import { OTHER_CATEGORY_LABEL } from '../vessel-groups/vessel-group-report.config' +} from '../vessel-groups/vessel-group-report.config' import { selectPortsReportVessels } from './ports-report.slice' import { selectPortReportVesselsFilter, diff --git a/apps/fishing-map/features/reports/vessel-groups/events/vgr-events.selectors.ts b/apps/fishing-map/features/reports/vessel-groups/events/vgr-events.selectors.ts index c50e930b2e..00226e6e19 100644 --- a/apps/fishing-map/features/reports/vessel-groups/events/vgr-events.selectors.ts +++ b/apps/fishing-map/features/reports/vessel-groups/events/vgr-events.selectors.ts @@ -1,7 +1,5 @@ import { createSelector } from '@reduxjs/toolkit' import { groupBy } from 'es-toolkit' -import { DatasetTypes } from '@globalfishingwatch/api-types' -import { getDataviewFilters } from '@globalfishingwatch/dataviews-client' import { selectReportEventsStatsApiSlice, selectReportEventsVessels, @@ -10,6 +8,8 @@ import type { ReportEventsVesselsParams, ReportEventsVesselsResponseItem, } from 'queries/report-events-stats-api' +import { DatasetTypes } from '@globalfishingwatch/api-types' +import { getDataviewFilters } from '@globalfishingwatch/dataviews-client' import { selectVGRData } from 'features/reports/vessel-groups/vessel-group-report.slice' import { getSearchIdentityResolved } from 'features/vessel/vessel.utils' import { selectTimeRange } from 'features/app/selectors/app.timebar.selectors' @@ -21,9 +21,11 @@ import { selectVGREventsVesselsProperty, } from 'features/reports/vessel-groups/vessel-group.config.selectors' import { getVesselsFiltered } from 'features/reports/areas/area-reports.utils' -import { REPORT_FILTER_PROPERTIES } from 'features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors' import { selectVGREventsSubsectionDataview } from 'features/reports/vessel-groups/vessel-group-report.selectors' -import { OTHER_CATEGORY_LABEL } from 'features/reports/vessel-groups/vessel-group-report.config' +import { + OTHER_CATEGORY_LABEL, + REPORT_FILTER_PROPERTIES, +} from 'features/reports/vessel-groups/vessel-group-report.config' import { EMPTY_FIELD_PLACEHOLDER, formatInfoField } from 'utils/info' import { MAX_CATEGORIES } from 'features/reports/areas/area-reports.config' import { t } from 'features/i18n/i18n' diff --git a/apps/fishing-map/features/reports/vessel-groups/vessel-group-report.config.ts b/apps/fishing-map/features/reports/vessel-groups/vessel-group-report.config.ts index c9f7cfc77f..b221d5d256 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessel-group-report.config.ts +++ b/apps/fishing-map/features/reports/vessel-groups/vessel-group-report.config.ts @@ -1,5 +1,7 @@ import { REPORT_VESSELS_PER_PAGE } from 'data/config' import type { VesselGroupReportState } from 'features/vessel-groups/vessel-groups.types' +import { FILTER_PROPERTIES } from '../areas/area-reports.utils' +import type { FilterProperty } from '../areas/area-reports.utils' export const OTHER_CATEGORY_LABEL = 'OTHER' @@ -17,3 +19,9 @@ export const DEFAULT_VESSEL_GROUP_REPORT_STATE: VesselGroupReportState = { vGRVesselsOrderProperty: 'shipname', vGRVesselsOrderDirection: 'asc', } + +type ReportFilterProperty = FilterProperty | 'source' +export const REPORT_FILTER_PROPERTIES: Record = { + ...FILTER_PROPERTIES, + source: ['source'], +} diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts b/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts index 4e9181a90e..38f45cb9c8 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts @@ -1,9 +1,12 @@ import { createSelector } from '@reduxjs/toolkit' import { groupBy } from 'es-toolkit' -import type { Dataset, IdentityVessel, Vessel } from '@globalfishingwatch/api-types' +import type { Dataset, IdentityVessel } from '@globalfishingwatch/api-types' import { DatasetTypes, VesselIdentitySourceEnum } from '@globalfishingwatch/api-types' import type { ResponsiveVisualizationData } from '@globalfishingwatch/responsive-visualizations' -import { OTHER_CATEGORY_LABEL } from 'features/reports/vessel-groups/vessel-group-report.config' +import { + OTHER_CATEGORY_LABEL, + REPORT_FILTER_PROPERTIES, +} from 'features/reports/vessel-groups/vessel-group-report.config' import { getSearchIdentityResolved, getVesselProperty } from 'features/vessel/vessel.utils' import { selectVGRVesselsResultsPerPage, @@ -18,8 +21,7 @@ import { getVesselShipTypeLabel, } from 'utils/info' import { t } from 'features/i18n/i18n' -import type { FilterProperty } from 'features/reports/areas/area-reports.utils' -import { FILTER_PROPERTIES, getVesselsFiltered } from 'features/reports/areas/area-reports.utils' +import { getVesselsFiltered } from 'features/reports/areas/area-reports.utils' import { selectVGRVesselsOrderDirection, selectVGRVesselsOrderProperty, @@ -94,12 +96,6 @@ export const selectVGRVesselsParsed = createSelector([selectVGRUniqVessels], (ve }) }) -type ReportFilterProperty = FilterProperty | 'source' -export const REPORT_FILTER_PROPERTIES: Record = { - ...FILTER_PROPERTIES, - source: ['source'], -} - export const selectVGRVesselsTimeRange = createSelector([selectVGRVesselsParsed], (vessels) => { if (!vessels?.length) return null let start: string = '' diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx index 836df04b16..fe32b31f68 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx @@ -35,14 +35,17 @@ export function ResponsiveBarChart({ onAggregatedItemClick, }: ResponsiveBarChartProps) { const containerRef = useRef(null) - const { data, isIndividualSupported } = useResponsiveVisualization(containerRef, { - labelKey: labelKey as ResponsiveVisualizationAnyItemKey, - aggregatedValueKey, - individualValueKey, - getAggregatedData, - getIndividualData, - getIsIndividualSupported: getIsIndividualBarChartSupported, - }) + const { data, isIndividualSupported, individualItemSize } = useResponsiveVisualization( + containerRef, + { + labelKey: labelKey as ResponsiveVisualizationAnyItemKey, + aggregatedValueKey, + individualValueKey, + getAggregatedData, + getIndividualData, + getIsIndividualSupported: getIsIndividualBarChartSupported, + } + ) if (!getAggregatedData && !getIndividualData) { console.warn('No data getters functions provided') @@ -57,6 +60,7 @@ export function ResponsiveBarChart({ } color={color} + pointSize={individualItemSize} valueKey={individualValueKey} labelKey={labelKey as ResponsiveVisualizationAnyItemKey} onClick={onIndividualItemClick} diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx index a2ac2a06b1..41f7a91a9c 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx @@ -6,7 +6,7 @@ import { IndividualPoint } from '../points/IndividualPoint' import { AXIS_LABEL_PADDING, POINT_GAP } from '../config' import styles from './BarChartIndividual.module.css' -type IndividualBarChartProps = BarChartByTypeProps<'individual'> +type IndividualBarChartProps = BarChartByTypeProps<'individual'> & { pointSize?: number } export function IndividualBarChart({ data, @@ -16,6 +16,7 @@ export function IndividualBarChart({ labelKey, barValueFormatter, customTooltip, + pointSize, }: IndividualBarChartProps) { return ( @@ -44,6 +45,7 @@ export function IndividualBarChart({ {points?.map((point, pointIndex) => ( @@ -65,6 +66,7 @@ export function useResponsiveVisualizationData({ }: UseResponsiveVisualizationDataProps) { const [data, setData] = useState(null) const [isIndividualSupported, setIsIndividualSupported] = useState(false) + const [individualItemSize, setIndividualItemSize] = useState(DEFAULT_POINT_SIZE) const loadData = useCallback( async ({ width, height }: { width: number; height: number }) => { @@ -82,29 +84,32 @@ export function useResponsiveVisualizationData({ if (!aggregatedData) { return } - if ( - getIndividualData && - getIsIndividualSupported({ - data: aggregatedData, + const { isSupported } = getIsIndividualSupported({ + data: aggregatedData, + ...isIndividualParams, + }) + if (getIndividualData && isSupported) { + const individualData = await getIndividualData() + if (!individualData) { + setIsIndividualSupported(false) + setIndividualItemSize(DEFAULT_POINT_SIZE) + setData(aggregatedData) + return + } + const { isSupported, individualItemSize } = getIsIndividualSupported({ + data: individualData, ...isIndividualParams, }) - ) { - const individualData = await getIndividualData() - if ( - individualData && - getIsIndividualSupported({ - data: individualData, - ...isIndividualParams, - }) - ) { + if (isSupported) { setIsIndividualSupported(true) + if (individualItemSize) { + setIndividualItemSize(individualItemSize) + } setData(individualData) - } else { - setIsIndividualSupported(false) - setData(aggregatedData) } } else { setIsIndividualSupported(false) + setIndividualItemSize(DEFAULT_POINT_SIZE) setData(aggregatedData) } } else if (getIndividualData) { @@ -112,13 +117,15 @@ export function useResponsiveVisualizationData({ if (!individualData) { return } - if ( - getIsIndividualSupported({ - data: individualData, - ...isIndividualParams, - }) - ) { + const { isSupported, individualItemSize } = getIsIndividualSupported({ + data: individualData, + ...isIndividualParams, + }) + if (isSupported) { setIsIndividualSupported(true) + if (individualItemSize) { + setIndividualItemSize(individualItemSize) + } setData(individualData) } else { const aggregatedData = individualData.map((item) => { @@ -129,6 +136,7 @@ export function useResponsiveVisualizationData({ } }) as ResponsiveVisualizationData setIsIndividualSupported(false) + setIndividualItemSize(DEFAULT_POINT_SIZE) setData(aggregatedData) } } @@ -147,8 +155,8 @@ export function useResponsiveVisualizationData({ ) return useMemo( - () => ({ data, isIndividualSupported, loadData }), - [data, isIndividualSupported, loadData] + () => ({ data, isIndividualSupported, loadData, individualItemSize }), + [data, isIndividualSupported, loadData, individualItemSize] ) } @@ -157,7 +165,8 @@ export function useResponsiveVisualization( params: UseResponsiveVisualizationDataProps ) { const dimensions = useResponsiveDimensions(containerRef) - const { data, isIndividualSupported, loadData } = useResponsiveVisualizationData(params) + const { data, isIndividualSupported, individualItemSize, loadData } = + useResponsiveVisualizationData(params) useEffect(() => { if (dimensions.width && dimensions.height) { @@ -166,7 +175,7 @@ export function useResponsiveVisualization( }, [dimensions, loadData]) return useMemo( - () => ({ ...dimensions, data, isIndividualSupported }), - [data, isIndividualSupported, dimensions] + () => ({ ...dimensions, data, isIndividualSupported, individualItemSize }), + [data, isIndividualSupported, dimensions, individualItemSize] ) } diff --git a/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx b/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx index b2064d51f7..bc8a038c1d 100644 --- a/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx +++ b/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx @@ -10,7 +10,7 @@ import { import { cloneElement, useState, type ReactElement } from 'react' import cx from 'classnames' import type { ResponsiveVisualizationItem } from '../../types' -import { POINT_SIZE } from '../config' +import { DEFAULT_POINT_SIZE } from '../config' import styles from './IndividualPoint.module.css' type IndividualPointProps = { @@ -19,9 +19,17 @@ type IndividualPointProps = { tooltip?: ReactElement className?: string icon?: ReactElement + pointSize?: number } -export function IndividualPoint({ point, color, tooltip, className, icon }: IndividualPointProps) { +export function IndividualPoint({ + point, + color, + tooltip, + className, + icon, + pointSize = DEFAULT_POINT_SIZE, +}: IndividualPointProps) { const [isOpen, setIsOpen] = useState(false) const { refs, floatingStyles, context } = useFloating({ @@ -40,8 +48,8 @@ export function IndividualPoint({ point, color, tooltip, className, icon }: Indi {...getReferenceProps()} className={cx(styles.point, { [styles.withIcon]: icon })} style={{ - width: POINT_SIZE, - height: POINT_SIZE, + width: pointSize, + height: pointSize, ...(color && !icon && { backgroundColor: color, diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx index 358116792b..9fc3f1b48e 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx @@ -4,11 +4,11 @@ import type { ReactElement } from 'react' import type { TimeseriesByTypeProps } from '../types' import type { ResponsiveVisualizationData, ResponsiveVisualizationItem } from '../../types' import { IndividualPoint } from '../points/IndividualPoint' -import { AXIS_LABEL_PADDING, POINT_GAP, POINT_SIZE, TIMESERIES_PADDING } from '../config' +import { AXIS_LABEL_PADDING, POINT_GAP, DEFAULT_POINT_SIZE, TIMESERIES_PADDING } from '../config' import styles from './TimeseriesIndividual.module.css' import { useFullTimeseries, useTimeseriesDomain } from './timeseries.hooks' -const graphMargin = { top: 0, right: POINT_SIZE, left: POINT_SIZE, bottom: 0 } +const graphMargin = { top: 0, right: DEFAULT_POINT_SIZE, left: DEFAULT_POINT_SIZE, bottom: 0 } type IndividualTimeseriesProps = TimeseriesByTypeProps<'individual'> & { width: number @@ -57,7 +57,11 @@ export function IndividualTimeseries({ {fullTimeseries.map((item, index) => { const points = item?.[valueKey] as ResponsiveVisualizationItem[] return ( -
          +
            {points?.map((point, pointIndex) => ( { const columnsNumber = data.length const columnsWidth = width / columnsNumber - COLUMN_PADDING * 2 - const pointsByRow = Math.floor(columnsWidth / (POINT_SIZE + POINT_GAP)) + const pointsByRow = Math.floor(columnsWidth / (pointSize + POINT_GAP)) return { columnsNumber, columnsWidth, pointsByRow } } @@ -55,21 +57,28 @@ export type IsIndividualSupportedParams = { aggregatedValueKey: keyof ResponsiveVisualizationData<'aggregated'>[0] individualValueKey: keyof ResponsiveVisualizationData<'individual'>[0] } +type IsIndividualSupportedResult = { + isSupported: boolean + individualItemSize?: number +} export function getIsIndividualBarChartSupported({ data, width, height, aggregatedValueKey, individualValueKey, -}: IsIndividualSupportedParams): boolean { +}: IsIndividualSupportedParams): IsIndividualSupportedResult { const { total, max } = getColumnsStats(data, aggregatedValueKey, individualValueKey) if (total > MAX_INDIVIDUAL_ITEMS) { - return false + return { isSupported: false } } - const { pointsByRow } = getBarProps(data, width) - const rowsInBiggestColumn = Math.ceil(max / pointsByRow) - const heightNeeded = rowsInBiggestColumn * (POINT_SIZE + POINT_GAP) - return heightNeeded < height - AXIS_LABEL_PADDING - COLUMN_PADDING - COLUMN_LABEL_SIZE + const individualItemSize = POINT_SIZES.find((pointSize) => { + const { pointsByRow } = getBarProps(data, width, pointSize) + const rowsInBiggestColumn = Math.ceil(max / pointsByRow) + const heightNeeded = rowsInBiggestColumn * (pointSize + POINT_GAP) + return heightNeeded < height - AXIS_LABEL_PADDING - COLUMN_PADDING - COLUMN_LABEL_SIZE + }) + return { isSupported: individualItemSize !== undefined, individualItemSize } } export function getIsIndividualTimeseriesSupported({ @@ -81,15 +90,15 @@ export function getIsIndividualTimeseriesSupported({ timeseriesInterval, aggregatedValueKey, individualValueKey, -}: IsIndividualSupportedParams): boolean { +}: IsIndividualSupportedParams): IsIndividualSupportedResult { const { total, max } = getColumnsStats(data, aggregatedValueKey, individualValueKey) if (total > MAX_INDIVIDUAL_ITEMS) { - return false + return { isSupported: false } } - const heightNeeded = max * (POINT_SIZE + POINT_GAP) + const heightNeeded = max * (DEFAULT_POINT_SIZE + POINT_GAP) const matchesHeight = heightNeeded < height - AXIS_LABEL_PADDING if (!matchesHeight) { - return false + return { isSupported: false } } if (start && end && timeseriesInterval) { const startMillis = DateTime.fromISO(start).toMillis() @@ -99,7 +108,7 @@ export function getIsIndividualTimeseriesSupported({ timeseriesInterval.toLowerCase() as DurationUnit ) ) - return intervalDiff * POINT_SIZE <= width - TIMESERIES_PADDING * 2 + return { isSupported: intervalDiff * DEFAULT_POINT_SIZE <= width - TIMESERIES_PADDING * 2 } } - return true + return { isSupported: true } } From 8d6fb9c9416b3b31e57054d40bbf96ec8b2f823e Mon Sep 17 00:00:00 2001 From: j8seangel Date: Wed, 15 Jan 2025 08:38:02 +0100 Subject: [PATCH 38/62] render 1 layer of activity graph data in responsive visualization --- .../reports/ports/ports-report.selectors.ts | 2 +- .../shared/activity/ReportActivityGraph.tsx | 1 + .../activity/vessels/ReportVesselsGraph.tsx | 53 +++++++-- .../report-activity-vessels.selectors.ts | 11 +- .../features/reports/shared/reports.utils.ts | 101 ++++++++++++++++++ .../insights/VGRInsightCoverageGraph.tsx | 2 +- .../vessel-group-report-vessels.selectors.ts | 94 +--------------- .../src/charts/barchart/BarChart.tsx | 4 +- .../src/charts/timeseries/Timeseries.tsx | 4 +- .../src/charts/types.ts | 6 +- 10 files changed, 167 insertions(+), 111 deletions(-) create mode 100644 apps/fishing-map/features/reports/shared/reports.utils.ts diff --git a/apps/fishing-map/features/reports/ports/ports-report.selectors.ts b/apps/fishing-map/features/reports/ports/ports-report.selectors.ts index cc7c5361a0..f8c1be69ad 100644 --- a/apps/fishing-map/features/reports/ports/ports-report.selectors.ts +++ b/apps/fishing-map/features/reports/ports/ports-report.selectors.ts @@ -10,11 +10,11 @@ import { TEMPLATE_VESSEL_DATAVIEW_SLUG, } from 'data/workspaces' import { selectAllDataviews } from 'features/dataviews/dataviews.slice' -import { getVesselIndividualGroupedData } from '../vessel-groups/vessels/vessel-group-report-vessels.selectors' import { OTHER_CATEGORY_LABEL, REPORT_FILTER_PROPERTIES, } from '../vessel-groups/vessel-group-report.config' +import { getVesselIndividualGroupedData } from '../shared/reports.utils' import { selectPortsReportVessels } from './ports-report.slice' import { selectPortReportVesselsFilter, diff --git a/apps/fishing-map/features/reports/shared/activity/ReportActivityGraph.tsx b/apps/fishing-map/features/reports/shared/activity/ReportActivityGraph.tsx index 1a779f424d..49edb83862 100644 --- a/apps/fishing-map/features/reports/shared/activity/ReportActivityGraph.tsx +++ b/apps/fishing-map/features/reports/shared/activity/ReportActivityGraph.tsx @@ -57,6 +57,7 @@ export default function ReportActivity() { if (loaded && bbox?.length) { fitAreaInViewport() } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [loaded, bboxHash]) const { t } = useTranslation() diff --git a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx index 3dbcb1f91c..4e87df8abb 100644 --- a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx @@ -1,10 +1,10 @@ -import React, { Fragment } from 'react' +import React, { Fragment, useCallback } from 'react' import cx from 'classnames' import { useSelector } from 'react-redux' -import { BarChart, Bar, XAxis, Tooltip, ResponsiveContainer, LabelList } from 'recharts' import { useTranslation } from 'react-i18next' import type { CategoricalChartFunc } from 'recharts/types/chart/generateCategoricalChart' import { Tooltip as GFWTooltip } from '@globalfishingwatch/ui-components' +import { ResponsiveBarChart } from '@globalfishingwatch/responsive-visualizations' import { selectReportVesselGraph } from 'features/app/selectors/app.reports.selector' import I18nNumber, { formatI18nNumber } from 'features/i18n/i18nNumber' import { useLocationConnect } from 'routes/routes.hook' @@ -19,6 +19,7 @@ import type { ReportVesselGraph } from 'features/reports/areas/area-reports.type import { selectReportVesselsGraphDataGrouped, selectReportVesselsGraphDataOthers, + selectReportVesselsGraphIndividualData, } from 'features/reports/shared/activity/vessels/report-activity-vessels.selectors' import { cleanFlagState } from 'features/reports/shared/activity/vessels/report-activity-vessels.utils' import { selectReportDataviewsWithPermissions } from 'features/reports/areas/area-reports.selectors' @@ -111,9 +112,11 @@ const CustomTick = (props: any) => { const tooltip = isOtherCategory ? (
              - {othersData?.slice(0, MAX_OTHER_TOOLTIP_ITEMS).map(({ name, value }) => ( -
            • {`${getTickLabel(name)}: ${value}`}
            • - ))} + {othersData + ?.slice(0, MAX_OTHER_TOOLTIP_ITEMS) + .map(({ name, value }) => ( +
            • {`${getTickLabel(name)}: ${value}`}
            • + ))} {othersData && othersData.length > MAX_OTHER_TOOLTIP_ITEMS && (
            • + {othersData.length - MAX_OTHER_TOOLTIP_ITEMS} {t('analysis.others', 'Others')} @@ -163,6 +166,7 @@ const CustomTick = (props: any) => { export default function ReportVesselsGraph() { const dataviews = useSelector(selectReportDataviewsWithPermissions) const data = useSelector(selectReportVesselsGraphDataGrouped) + const individualData = useSelector(selectReportVesselsGraphIndividualData) const selectedReportVesselGraph = useSelector(selectReportVesselGraph) const othersData = useSelector(selectReportVesselsGraphDataOthers) const { dispatchQueryParams } = useLocationConnect() @@ -182,6 +186,7 @@ export default function ReportVesselsGraph() { return label } } + // TODO: add this interaction const onLabelClick: CategoricalChartFunc = (e) => { const { payload } = e.activePayload?.[0] || {} if (!payload) return @@ -202,10 +207,42 @@ export default function ReportVesselsGraph() { }) } } + + const getIndividualData = useCallback(async () => { + return individualData + }, [individualData]) + + const getAggregatedData = useCallback(async () => { + return data as any[] + }, [data]) + + if (!data) { + return ( +
              + +
              + ) + } + return ( -
              - {data ? ( +
              + { + return formatI18nNumber(value).toString() + }} + barLabel={} + labelKey={'name'} + // individualTooltip={} + aggregatedTooltip={} + /> + {/* {data ? ( ) : ( - )} + )} */}
              ) diff --git a/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts b/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts index f155e55dcc..cfa606fe8a 100644 --- a/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts +++ b/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts @@ -30,6 +30,7 @@ import { } from 'features/reports/shared/activity/vessels/report-activity-vessels.utils' import { getVesselGearTypeLabel } from 'utils/info' import { selectIsVesselGroupReportLocation } from 'routes/routes.selectors' +import { getVesselIndividualGroupedData } from '../../reports.utils' const EMPTY_ARRAY: [] = [] @@ -145,7 +146,7 @@ const selectReportVesselsGraphData = createSelector( ? Object.values(reportData[dataview.id]).flatMap((v) => v || []) : [] const dataByKey = groupBy(dataviewData, (d) => d[reportGraph] || '') - return { id: dataview.id, data: dataByKey } + return { id: dataview.id, color: dataview.config?.color, data: dataByKey } }) const allDistributionKeys = uniq(dataByDataview.flatMap(({ data }) => Object.keys(data))) @@ -188,6 +189,14 @@ export const selectReportVesselsGraphDataGrouped = createSelector( } ) +export const selectReportVesselsGraphIndividualData = createSelector( + [selectReportVesselsFiltered, selectReportVesselGraph], + (vessels, groupBy) => { + if (!vessels || !groupBy) return [] + return getVesselIndividualGroupedData(vessels, groupBy) + } +) + const defaultOthersLabel: any[] = [] export const selectReportVesselsGraphDataOthers = createSelector( [selectReportVesselsGraphData], diff --git a/apps/fishing-map/features/reports/shared/reports.utils.ts b/apps/fishing-map/features/reports/shared/reports.utils.ts new file mode 100644 index 0000000000..3d739efb87 --- /dev/null +++ b/apps/fishing-map/features/reports/shared/reports.utils.ts @@ -0,0 +1,101 @@ +import { groupBy } from 'lodash' +import type { ResponsiveVisualizationData } from '@globalfishingwatch/responsive-visualizations' +import type { + VGRSubsection, + VGREventsVesselsProperty, +} from 'features/vessel-groups/vessel-groups.types' +import { EMPTY_FIELD_PLACEHOLDER } from 'utils/info' +import { MAX_CATEGORIES } from '../areas/area-reports.config' +import type { ReportVesselWithDatasets } from '../areas/area-reports.selectors' +import type { EventsStatsVessel } from '../ports/ports-report.slice' +import { OTHER_CATEGORY_LABEL } from '../vessel-groups/vessel-group-report.config' +import type { VesselGroupVesselTableParsed } from '../vessel-groups/vessels/vessel-group-report-vessels.selectors' +import type { ReportVesselGraph } from '../areas/area-reports.types' + +export function getVesselIndividualGroupedData( + vessels: (EventsStatsVessel | VesselGroupVesselTableParsed | ReportVesselWithDatasets)[], + groupByProperty: VGRSubsection | VGREventsVesselsProperty | ReportVesselGraph +) { + if (!vessels?.length) { + return [] + } + let vesselsGrouped = {} + switch (groupByProperty) { + case 'flag': { + vesselsGrouped = groupBy( + vessels, + (vessel) => + (vessel as VesselGroupVesselTableParsed).flagTranslatedClean || + (vessel as EventsStatsVessel).flagTranslated || + (vessel as EventsStatsVessel).flag + ) + break + } + case 'shiptype': + case 'shiptypes': { + vesselsGrouped = groupBy(vessels, (vessel) => + (vessel as VesselGroupVesselTableParsed).vesselType + ? (vessel as VesselGroupVesselTableParsed).vesselType.split(', ')[0] + : (vessel as EventsStatsVessel).shiptypes[0] + ) + break + } + case 'geartype': + case 'geartypes': { + vesselsGrouped = groupBy(vessels, (vessel) => vessel.geartype?.split(', ')[0]) + break + } + case 'source': { + vesselsGrouped = groupBy(vessels, (vessel) => (vessel as VesselGroupVesselTableParsed).source) + break + } + } + const orderedGroups: ResponsiveVisualizationData<'individual', { name: string; values: any[] }> = + Object.entries(vesselsGrouped) + .map(([key, value]) => ({ + name: key, + values: value as any[], + })) + .sort((a, b) => { + return b.values.length - a.values.length + }) + const groupsWithoutOther: ResponsiveVisualizationData< + 'individual', + { name: string; values: any[] } + > = [] + const otherGroups: ResponsiveVisualizationData<'individual', { name: string; values: any[] }> = [] + orderedGroups.forEach((group) => { + if ( + group.name === 'null' || + group.name.toLowerCase() === OTHER_CATEGORY_LABEL.toLowerCase() || + group.name === EMPTY_FIELD_PLACEHOLDER + ) { + otherGroups.push(group) + } else { + groupsWithoutOther.push(group) + } + }) + const allGroups = + otherGroups.length > 0 + ? [ + ...groupsWithoutOther, + { + name: OTHER_CATEGORY_LABEL, + values: otherGroups.flatMap((group) => group.values), + }, + ] + : groupsWithoutOther + if (allGroups.length <= MAX_CATEGORIES) { + return allGroups as ResponsiveVisualizationData<'individual'> + } + const firstGroups = allGroups.slice(0, MAX_CATEGORIES) + const restOfGroups = allGroups.slice(MAX_CATEGORIES) + + return [ + ...firstGroups, + { + name: OTHER_CATEGORY_LABEL, + values: restOfGroups.flatMap((group) => group.values), + }, + ] as ResponsiveVisualizationData<'individual'> +} diff --git a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx index 0bb9a4865b..1105efead6 100644 --- a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx @@ -114,7 +114,7 @@ export default function VesselGroupReportInsightCoverageGraph({ const reportDataview = useSelector(selectVGRDataview) return ( -
              +
              - (vessel as VesselGroupVesselTableParsed).flagTranslatedClean || - vessel.flagTranslated || - vessel.flag - ) - break - } - case 'shiptype': - case 'shiptypes': { - vesselsGrouped = groupBy(vessels, (vessel) => - (vessel as VesselGroupVesselTableParsed).vesselType - ? (vessel as VesselGroupVesselTableParsed).vesselType.split(', ')[0] - : (vessel as EventsStatsVessel).shiptypes[0] - ) - break - } - case 'geartype': - case 'geartypes': { - vesselsGrouped = groupBy(vessels, (vessel) => vessel.geartype.split(', ')[0]) - break - } - case 'source': { - vesselsGrouped = groupBy(vessels, (vessel) => (vessel as VesselGroupVesselTableParsed).source) - break - } - } - const orderedGroups: ResponsiveVisualizationData<'individual', { name: string; values: any[] }> = - Object.entries(vesselsGrouped) - .map(([key, value]) => ({ - name: key, - values: value as any[], - })) - .sort((a, b) => { - return b.values.length - a.values.length - }) - const groupsWithoutOther: ResponsiveVisualizationData< - 'individual', - { name: string; values: any[] } - > = [] - const otherGroups: ResponsiveVisualizationData<'individual', { name: string; values: any[] }> = [] - orderedGroups.forEach((group) => { - if ( - group.name === 'null' || - group.name.toLowerCase() === OTHER_CATEGORY_LABEL.toLowerCase() || - group.name === EMPTY_FIELD_PLACEHOLDER - ) { - otherGroups.push(group) - } else { - groupsWithoutOther.push(group) - } - }) - const allGroups = - otherGroups.length > 0 - ? [ - ...groupsWithoutOther, - { - name: OTHER_CATEGORY_LABEL, - values: otherGroups.flatMap((group) => group.values), - }, - ] - : groupsWithoutOther - if (allGroups.length <= MAX_CATEGORIES) { - return allGroups as ResponsiveVisualizationData<'individual'> - } - const firstGroups = allGroups.slice(0, MAX_CATEGORIES) - const restOfGroups = allGroups.slice(MAX_CATEGORIES) - - return [ - ...firstGroups, - { - name: OTHER_CATEGORY_LABEL, - values: restOfGroups.flatMap((group) => group.values), - }, - ] as ResponsiveVisualizationData<'individual'> -} - export const selectVGRVesselsGraphIndividualData = createSelector( [selectVGRVesselsFiltered, selectVGRVesselsSubsection], (vessels, groupBy) => { diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx index fe32b31f68..66ffb942a9 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx @@ -61,7 +61,7 @@ export function ResponsiveBarChart({ data={data as ResponsiveVisualizationData<'individual'>} color={color} pointSize={individualItemSize} - valueKey={individualValueKey} + valueKey={individualValueKey as keyof ResponsiveVisualizationData<'individual'>[0]} labelKey={labelKey as ResponsiveVisualizationAnyItemKey} onClick={onIndividualItemClick} barLabel={barLabel} @@ -72,7 +72,7 @@ export function ResponsiveBarChart({ } color={color} - valueKey={aggregatedValueKey} + valueKey={aggregatedValueKey as keyof ResponsiveVisualizationData<'aggregated'>[0]} labelKey={labelKey as ResponsiveVisualizationAnyItemKey} onClick={onAggregatedItemClick} barLabel={barLabel} diff --git a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx index e41cee96fa..db8e832c26 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx @@ -70,7 +70,7 @@ export function ResponsiveTimeseries({ color={color} dateKey={dateKey as ResponsiveVisualizationAnyItemKey} timeseriesInterval={timeseriesInterval} - valueKey={individualValueKey} + valueKey={individualValueKey as keyof ResponsiveVisualizationData<'individual'>[0]} onClick={onIndividualItemClick} tickLabelFormatter={tickLabelFormatter} customTooltip={individualTooltip} @@ -84,7 +84,7 @@ export function ResponsiveTimeseries({ color={color} dateKey={dateKey as ResponsiveVisualizationAnyItemKey} timeseriesInterval={timeseriesInterval} - valueKey={aggregatedValueKey} + valueKey={aggregatedValueKey as keyof ResponsiveVisualizationData<'aggregated'>[0]} onClick={onAggregatedItemClick} tickLabelFormatter={tickLabelFormatter} customTooltip={aggregatedTooltip} diff --git a/libs/responsive-visualizations/src/charts/types.ts b/libs/responsive-visualizations/src/charts/types.ts index eea1871879..3950a58eb2 100644 --- a/libs/responsive-visualizations/src/charts/types.ts +++ b/libs/responsive-visualizations/src/charts/types.ts @@ -16,12 +16,12 @@ export type BaseResponsiveChartProps = { aggregatedTooltip?: ReactElement onAggregatedItemClick?: ResponsiveVisualizationInteractionCallback getAggregatedData?: () => Promise | undefined> - aggregatedValueKey?: keyof ResponsiveVisualizationData<'aggregated'>[0] + aggregatedValueKey?: keyof ResponsiveVisualizationData<'aggregated'>[0] | string // Individual props individualTooltip?: ReactElement onIndividualItemClick?: ResponsiveVisualizationInteractionCallback getIndividualData?: () => Promise | undefined> - individualValueKey?: keyof ResponsiveVisualizationData<'individual'>[0] + individualValueKey?: keyof ResponsiveVisualizationData<'individual'>[0] | string individualIcon?: ReactElement } @@ -31,7 +31,7 @@ export type ResponsiveVisualizationAnyItemKey = // Shared types within the BarChart export type BaseResponsiveBarChartProps = { - color: string + color?: string barLabel?: ReactElement barValueFormatter?: (value: number) => string } From 420b759f0c6c66c262006477f949312562eb77f0 Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Wed, 15 Jan 2025 09:53:02 +0100 Subject: [PATCH 39/62] loitering and port visit tooltips --- .../shared/events/EventsReportGraph.tsx | 54 ++++++++++--------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx index e2c045c7ae..f1b8317824 100644 --- a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx +++ b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx @@ -4,8 +4,8 @@ import { DateTime } from 'luxon' import { groupBy } from 'es-toolkit' import { stringify } from 'qs' import { useTranslation } from 'react-i18next' -import { getEventsStatsQuery } from 'queries/report-events-stats-api' import type { BaseReportEventsVesselsParamsFilters } from 'queries/report-events-stats-api' +import { getEventsStatsQuery } from 'queries/report-events-stats-api' import { getFourwingsInterval, type FourwingsInterval } from '@globalfishingwatch/deck-loaders' import type { BaseResponsiveTimeseriesProps } from '@globalfishingwatch/responsive-visualizations' import { ResponsiveTimeseries } from '@globalfishingwatch/responsive-visualizations' @@ -17,7 +17,7 @@ import i18n from 'features/i18n/i18n' import { formatDateForInterval, getUTCDateTime } from 'utils/dates' import { formatI18nNumber } from 'features/i18n/i18nNumber' import { COLOR_PRIMARY_BLUE } from 'features/app/app.config' -import { formatInfoField } from 'utils/info' +import { formatInfoField, upperFirst } from 'utils/info' import { getTimeLabels } from 'utils/events' import EncounterIcon from '../../shared/events/icons/event-encounter.svg' import LoiteringIcon from '../../shared/events/icons/event-loitering.svg' @@ -63,35 +63,37 @@ const IndividualGraphTooltip = ({ data, eventType }: { data?: any; eventType?: E if (!data?.vessel?.name) { return null } - console.log('data:', data) - if (eventType === 'encounter') { - const { start, duration } = getTimeLabels({ start: data.start, end: data.end }) - return ( -
              -
              -
              - - {start} -
              -
              - - {duration} -
              + const { start, duration } = getTimeLabels({ start: data.start, end: data.end }) + + return ( +
              + {eventType && upperFirst(t(`common.eventLabels.${eventType}`, eventType))} +
              +
              + + {`${formatInfoField(data.vessel?.name, 'shipname')} (${formatInfoField(data.vessel?.flag, 'flag')})`}
              -
              -
              - - {formatInfoField(data.vessel?.name, 'shipname')} -
              + {eventType === 'encounter' && (
              - - {formatInfoField(data.encounter?.vessel?.name, 'shipname')} + + {`${formatInfoField(data.encounter?.vessel?.name, 'shipname')} (${formatInfoField(data.encounter?.vessel?.flag, 'flag')})`}
              + )} +
              +
              +
              + + {start} +
              +
              + + {duration}
              - ) - } - return formatInfoField(data.vessel.name, 'shipname') +
              + ) } const formatDateTicks: BaseResponsiveTimeseriesProps['tickLabelFormatter'] = ( From e0a029db97a17c93e8b602759d856839a1fb5bd8 Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Wed, 15 Jan 2025 11:34:04 +0100 Subject: [PATCH 40/62] vessel click interaction --- .../reports/shared/VesselGraphLink.module.css | 4 ++++ .../reports/shared/VesselGraphLink.tsx | 18 +++++++++++++++ .../activity/vessels/ReportVesselsGraph.tsx | 5 ++++- .../insights/VGRInsightCoverageGraph.tsx | 2 ++ .../vessels/VesselGroupReportVesselsGraph.tsx | 6 +++-- .../features/vessel/VesselLink.tsx | 22 ++++++++++++------- .../src/charts/barchart/BarChart.tsx | 2 ++ .../charts/barchart/BarChartIndividual.tsx | 5 ++++- .../charts/points/IndividualPoint.module.css | 4 ++-- .../src/charts/points/IndividualPoint.tsx | 17 ++++++++++++-- .../src/charts/types.ts | 2 ++ 11 files changed, 71 insertions(+), 16 deletions(-) create mode 100644 apps/fishing-map/features/reports/shared/VesselGraphLink.module.css create mode 100644 apps/fishing-map/features/reports/shared/VesselGraphLink.tsx diff --git a/apps/fishing-map/features/reports/shared/VesselGraphLink.module.css b/apps/fishing-map/features/reports/shared/VesselGraphLink.module.css new file mode 100644 index 0000000000..947a317018 --- /dev/null +++ b/apps/fishing-map/features/reports/shared/VesselGraphLink.module.css @@ -0,0 +1,4 @@ +.hiddenLink { + width: 100%; + height: 100%; +} diff --git a/apps/fishing-map/features/reports/shared/VesselGraphLink.tsx b/apps/fishing-map/features/reports/shared/VesselGraphLink.tsx new file mode 100644 index 0000000000..7652bfc463 --- /dev/null +++ b/apps/fishing-map/features/reports/shared/VesselGraphLink.tsx @@ -0,0 +1,18 @@ +import VesselLink from 'features/vessel/VesselLink' +import type { VesselGroupVesselTableParsed } from 'features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors' +import styles from './VesselGraphLink.module.css' + +export default function VesselGraphLink({ data }: { data?: VesselGroupVesselTableParsed }) { + if (!data) { + return null + } + const { vesselId, dataset } = data + return ( + + ) +} diff --git a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx index 4e87df8abb..a862e39c56 100644 --- a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx @@ -23,6 +23,8 @@ import { } from 'features/reports/shared/activity/vessels/report-activity-vessels.selectors' import { cleanFlagState } from 'features/reports/shared/activity/vessels/report-activity-vessels.utils' import { selectReportDataviewsWithPermissions } from 'features/reports/areas/area-reports.selectors' +import VesselGraphLink from 'features/reports/shared/VesselGraphLink' +import VesselGroupReportVesselsIndividualTooltip from 'features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip' import { ReportBarGraphPlaceholder } from '../../placeholders/ReportBarGraphPlaceholder' import styles from './ReportVesselsGraph.module.css' @@ -239,7 +241,8 @@ export default function ReportVesselsGraph() { }} barLabel={} labelKey={'name'} - // individualTooltip={} + individualTooltip={} + individualItem={} aggregatedTooltip={} /> {/* {data ? ( diff --git a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx index 1105efead6..931a9001ce 100644 --- a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx @@ -10,6 +10,7 @@ import type { VesselGroupVesselIdentity } from 'features/vessel-groups/vessel-gr import { weightedMean } from 'utils/statistics' import { formatI18nNumber } from 'features/i18n/i18nNumber' import VesselGroupReportVesselsIndividualTooltip from 'features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip' +import VesselGraphLink from 'features/reports/shared/VesselGraphLink' import { selectVGRDataview } from '../vessel-group-report.selectors' import styles from './VGRInsightCoverageGraph.module.css' @@ -125,6 +126,7 @@ export default function VesselGroupReportInsightCoverageGraph({ barLabel={} labelKey="label" individualTooltip={} + individualItem={} />
              diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx index 130c90007e..41f355d7b0 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx @@ -19,6 +19,7 @@ import { COLOR_PRIMARY_BLUE } from 'features/app/app.config' import { OTHER_CATEGORY_LABEL } from 'features/reports/vessel-groups/vessel-group-report.config' import type { PortsReportState } from 'features/reports/ports/ports-report.types' import VesselGroupReportVesselsIndividualTooltip from 'features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip' +import VesselGraphLink from 'features/reports/shared/VesselGraphLink' import styles from './VesselGroupReportVesselsGraph.module.css' type ReportGraphTooltipProps = { @@ -178,8 +179,8 @@ export default function VesselGroupReportVesselsGraph({ }) } } - const onPointClick: ResponsiveVisualizationInteractionCallback = (e) => { - console.log('TODO', e) + const onPointClick: ResponsiveVisualizationInteractionCallback = (item) => { + console.log('TODO', item) } const getAggregatedData = useCallback(async () => { @@ -211,6 +212,7 @@ export default function VesselGroupReportVesselsGraph({ } labelKey={'name'} individualTooltip={} + individualItem={} aggregatedTooltip={} />
              diff --git a/apps/fishing-map/features/vessel/VesselLink.tsx b/apps/fishing-map/features/vessel/VesselLink.tsx index 3b37e1586a..33c0a1a62b 100644 --- a/apps/fishing-map/features/vessel/VesselLink.tsx +++ b/apps/fishing-map/features/vessel/VesselLink.tsx @@ -29,26 +29,28 @@ type VesselLinkProps = { dataviewId?: string vesselId?: string identity?: VesselDataIdentity - children: any + children?: any onClick?: (e: MouseEvent, vesselId?: string) => void tooltip?: React.ReactNode fitBounds?: boolean className?: string query?: Partial> testId?: string + showTooltip?: boolean } const VesselLink = ({ vesselId: vesselIdProp, datasetId, dataviewId, identity, - children, + children = '', onClick, tooltip, fitBounds = false, className = '', query, testId = 'link-vessel-profile', + showTooltip = true, }: VesselLinkProps) => { const { t } = useTranslation() const workspaceId = useSelector(selectCurrentWorkspaceId) @@ -130,12 +132,16 @@ const VesselLink = ({ }} onClick={onLinkClick} > - - {children} - + {showTooltip ? ( + + {children} + + ) : ( + children + )} ) } diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx index 66ffb942a9..bb3b32aa67 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx @@ -30,6 +30,7 @@ export function ResponsiveBarChart({ barLabel, aggregatedTooltip, individualTooltip, + individualItem, barValueFormatter, onIndividualItemClick, onAggregatedItemClick, @@ -66,6 +67,7 @@ export function ResponsiveBarChart({ onClick={onIndividualItemClick} barLabel={barLabel} customTooltip={individualTooltip} + customItem={individualItem} barValueFormatter={barValueFormatter} /> ) : ( diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx index 41f7a91a9c..b3734cf4a3 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx @@ -16,7 +16,9 @@ export function IndividualBarChart({ labelKey, barValueFormatter, customTooltip, + customItem, pointSize, + onClick, }: IndividualBarChartProps) { return ( @@ -30,7 +32,6 @@ export function IndividualBarChart({ left: 0, bottom: 0, }} - // onClick={onBarClick} >
              @@ -49,6 +50,8 @@ export function IndividualBarChart({ point={point} color={color} tooltip={customTooltip} + item={customItem} + onClick={onClick} /> ))}
            diff --git a/libs/responsive-visualizations/src/charts/points/IndividualPoint.module.css b/libs/responsive-visualizations/src/charts/points/IndividualPoint.module.css index 7e77342bf9..102de9c9d1 100644 --- a/libs/responsive-visualizations/src/charts/points/IndividualPoint.module.css +++ b/libs/responsive-visualizations/src/charts/points/IndividualPoint.module.css @@ -1,6 +1,6 @@ .point { position: relative; - border: 2px solid transparent; + outline: 1px solid transparent; transition: border-color 0.3s linear; border-radius: 50%; display: flex; @@ -9,7 +9,7 @@ } .point:not(.withIcon):hover { - border: var(--border-thick); + outline: 1px solid var(--color-secondary-blue); } .tooltip { diff --git a/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx b/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx index bc8a038c1d..054b07bf17 100644 --- a/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx +++ b/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx @@ -7,8 +7,9 @@ import { useHover, FloatingPortal, } from '@floating-ui/react' -import { cloneElement, useState, type ReactElement } from 'react' +import { cloneElement, useCallback, useState, type ReactElement } from 'react' import cx from 'classnames' +import type { ResponsiveVisualizationInteractionCallback } from 'libs/responsive-visualizations/src/charts/types' import type { ResponsiveVisualizationItem } from '../../types' import { DEFAULT_POINT_SIZE } from '../config' import styles from './IndividualPoint.module.css' @@ -17,17 +18,21 @@ type IndividualPointProps = { color?: string point: ResponsiveVisualizationItem tooltip?: ReactElement + item?: ReactElement className?: string icon?: ReactElement pointSize?: number + onClick?: ResponsiveVisualizationInteractionCallback } export function IndividualPoint({ point, color, tooltip, + item, className, icon, + onClick, pointSize = DEFAULT_POINT_SIZE, }: IndividualPointProps) { const [isOpen, setIsOpen] = useState(false) @@ -42,6 +47,10 @@ export function IndividualPoint({ const hover = useHover(context) const { getReferenceProps, getFloatingProps } = useInteractions([hover]) + const handleOnCLick = useCallback(() => { + onClick?.(point) + }, [onClick, point]) + return (
          • {isOpen && ( @@ -70,7 +82,8 @@ export function IndividualPoint({
          )} - {icon && {icon}} + {item && cloneElement(item, { ...(item.props || {}), data: point } as any)} + {icon && {icon}}
        • ) } diff --git a/libs/responsive-visualizations/src/charts/types.ts b/libs/responsive-visualizations/src/charts/types.ts index 3950a58eb2..f41964fa71 100644 --- a/libs/responsive-visualizations/src/charts/types.ts +++ b/libs/responsive-visualizations/src/charts/types.ts @@ -19,6 +19,7 @@ export type BaseResponsiveChartProps = { aggregatedValueKey?: keyof ResponsiveVisualizationData<'aggregated'>[0] | string // Individual props individualTooltip?: ReactElement + individualItem?: ReactElement onIndividualItemClick?: ResponsiveVisualizationInteractionCallback getIndividualData?: () => Promise | undefined> individualValueKey?: keyof ResponsiveVisualizationData<'individual'>[0] | string @@ -43,6 +44,7 @@ export type BarChartByTypeProps = data: ResponsiveVisualizationData onClick?: ResponsiveVisualizationInteractionCallback customTooltip?: ReactElement + customItem?: ReactElement } // Shared types within the Timeseries From 6308f12f6ffb8c3ae579aa3566f57ca7fb698d2c Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Wed, 15 Jan 2025 12:15:02 +0100 Subject: [PATCH 41/62] fix build --- .../insights/VGRInsightCoverageGraph.tsx | 32 ++++++------- .../vessels/VesselGroupReportVesselsGraph.tsx | 48 +++++++++---------- .../src/charts/barchart/BarChart.tsx | 4 +- .../src/charts/hooks.ts | 5 +- .../src/charts/points/IndividualPoint.tsx | 2 +- .../src/charts/timeseries/Timeseries.tsx | 4 +- 6 files changed, 46 insertions(+), 49 deletions(-) diff --git a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx index 931a9001ce..fad7d4ea1f 100644 --- a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useCallback } from 'react' +import React, { useCallback } from 'react' import { useSelector } from 'react-redux' import { groupBy } from 'es-toolkit' import { type VesselGroupInsightResponse } from '@globalfishingwatch/api-types' @@ -114,21 +114,19 @@ export default function VesselGroupReportInsightCoverageGraph({ const reportDataview = useSelector(selectVGRDataview) return ( - -
          - { - return formatI18nNumber(value).toString() - }} - barLabel={} - labelKey="label" - individualTooltip={} - individualItem={} - /> -
          -
          +
          + { + return formatI18nNumber(value).toString() + }} + barLabel={} + labelKey="label" + individualTooltip={} + individualItem={} + /> +
          ) } diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx index 41f355d7b0..c0467f4f9e 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx @@ -192,30 +192,28 @@ export default function VesselGroupReportVesselsGraph({ }, [individualData]) return ( - -
          - { - return formatI18nNumber(value).toString() - }} - barLabel={ - - } - labelKey={'name'} - individualTooltip={} - individualItem={} - aggregatedTooltip={} - /> -
          -
          +
          + { + return formatI18nNumber(value).toString() + }} + barLabel={ + + } + labelKey={'name'} + individualTooltip={} + individualItem={} + aggregatedTooltip={} + /> +
          ) } diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx index bb3b32aa67..f95bf05863 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx @@ -40,8 +40,8 @@ export function ResponsiveBarChart({ containerRef, { labelKey: labelKey as ResponsiveVisualizationAnyItemKey, - aggregatedValueKey, - individualValueKey, + aggregatedValueKey: aggregatedValueKey as keyof ResponsiveVisualizationData<'aggregated'>[0], + individualValueKey: individualValueKey as keyof ResponsiveVisualizationData<'individual'>[0], getAggregatedData, getIndividualData, getIsIndividualSupported: getIsIndividualBarChartSupported, diff --git a/libs/responsive-visualizations/src/charts/hooks.ts b/libs/responsive-visualizations/src/charts/hooks.ts index 34f390196d..1c5b06dba7 100644 --- a/libs/responsive-visualizations/src/charts/hooks.ts +++ b/libs/responsive-visualizations/src/charts/hooks.ts @@ -45,14 +45,15 @@ type UseResponsiveVisualizationDataProps = { end?: string timeseriesInterval?: FourwingsInterval labelKey: ResponsiveVisualizationAnyItemKey - individualValueKey: BaseResponsiveChartProps['individualValueKey'] - aggregatedValueKey: BaseResponsiveChartProps['aggregatedValueKey'] + individualValueKey: keyof ResponsiveVisualizationData<'individual'>[0] + aggregatedValueKey: keyof ResponsiveVisualizationData<'aggregated'>[0] getAggregatedData?: BaseResponsiveChartProps['getAggregatedData'] getIndividualData?: BaseResponsiveChartProps['getIndividualData'] getIsIndividualSupported: | typeof getIsIndividualBarChartSupported | typeof getIsIndividualTimeseriesSupported } + export function useResponsiveVisualizationData({ labelKey = DEFAULT_LABEL_KEY, individualValueKey = DEFAULT_INDIVIDUAL_VALUE_KEY, diff --git a/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx b/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx index 054b07bf17..8f3aadc8fe 100644 --- a/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx +++ b/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx @@ -9,7 +9,7 @@ import { } from '@floating-ui/react' import { cloneElement, useCallback, useState, type ReactElement } from 'react' import cx from 'classnames' -import type { ResponsiveVisualizationInteractionCallback } from 'libs/responsive-visualizations/src/charts/types' +import type { ResponsiveVisualizationInteractionCallback } from '@globalfishingwatch/responsive-visualizations' import type { ResponsiveVisualizationItem } from '../../types' import { DEFAULT_POINT_SIZE } from '../config' import styles from './IndividualPoint.module.css' diff --git a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx index db8e832c26..3a4ca9ce0d 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx @@ -45,8 +45,8 @@ export function ResponsiveTimeseries({ end, timeseriesInterval, labelKey: dateKey as ResponsiveVisualizationAnyItemKey, - individualValueKey, - aggregatedValueKey, + aggregatedValueKey: aggregatedValueKey as keyof ResponsiveVisualizationData<'aggregated'>[0], + individualValueKey: individualValueKey as keyof ResponsiveVisualizationData<'individual'>[0], getAggregatedData, getIndividualData, getIsIndividualSupported: getIsIndividualTimeseriesSupported, From a0de0d6eb4f5283d629fa51a3bf08cf48c74e0b8 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Wed, 15 Jan 2025 21:33:34 +0100 Subject: [PATCH 42/62] support barchar values as number or object --- .../report-activity-vessels.selectors.ts | 6 +- .../src/charts/barchart/BarChart.tsx | 26 ++++---- .../charts/barchart/BarChartAggregated.tsx | 22 +++++-- .../charts/barchart/BarChartIndividual.tsx | 4 +- .../src/charts/config.ts | 1 + .../src/charts/hooks.ts | 10 +-- .../src/charts/points/IndividualPoint.tsx | 8 +-- .../src/charts/timeseries/Timeseries.tsx | 22 +++---- .../timeseries/TimeseriesAggregated.tsx | 2 +- .../timeseries/TimeseriesIndividual.tsx | 4 +- .../src/charts/types.ts | 16 ++--- .../src/lib/density.ts | 21 ++++--- libs/responsive-visualizations/src/types.ts | 62 ++++++++++--------- 13 files changed, 108 insertions(+), 96 deletions(-) diff --git a/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts b/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts index cfa606fe8a..7213eb8f3c 100644 --- a/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts +++ b/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts @@ -146,7 +146,11 @@ const selectReportVesselsGraphData = createSelector( ? Object.values(reportData[dataview.id]).flatMap((v) => v || []) : [] const dataByKey = groupBy(dataviewData, (d) => d[reportGraph] || '') - return { id: dataview.id, color: dataview.config?.color, data: dataByKey } + return { + id: dataview.id, + color: dataview.config?.color, + data: dataByKey, + } }) const allDistributionKeys = uniq(dataByDataview.flatMap(({ data }) => Object.keys(data))) diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx index f95bf05863..f1b7f89431 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx @@ -1,11 +1,7 @@ import { useRef } from 'react' import type { ResponsiveVisualizationData } from '../../types' import { getIsIndividualBarChartSupported } from '../../lib/density' -import type { - BaseResponsiveBarChartProps, - BaseResponsiveChartProps, - ResponsiveVisualizationAnyItemKey, -} from '../types' +import type { BaseResponsiveBarChartProps, BaseResponsiveChartProps } from '../types' import { DEFAULT_AGGREGATED_VALUE_KEY, DEFAULT_INDIVIDUAL_VALUE_KEY, @@ -18,14 +14,14 @@ import { AggregatedBarChart } from './BarChartAggregated' import styles from './BarChart.module.css' type ResponsiveBarChartProps = BaseResponsiveChartProps & - BaseResponsiveBarChartProps & { labelKey?: ResponsiveVisualizationAnyItemKey | string } + BaseResponsiveBarChartProps & { labelKey?: keyof ResponsiveVisualizationData[0] } export function ResponsiveBarChart({ getIndividualData, getAggregatedData, color, - aggregatedValueKey = DEFAULT_AGGREGATED_VALUE_KEY as keyof ResponsiveVisualizationData<'aggregated'>[0], - individualValueKey = DEFAULT_INDIVIDUAL_VALUE_KEY as keyof ResponsiveVisualizationData<'individual'>[0], + aggregatedValueKey = DEFAULT_AGGREGATED_VALUE_KEY, + individualValueKey = DEFAULT_INDIVIDUAL_VALUE_KEY, labelKey = DEFAULT_LABEL_KEY, barLabel, aggregatedTooltip, @@ -39,9 +35,9 @@ export function ResponsiveBarChart({ const { data, isIndividualSupported, individualItemSize } = useResponsiveVisualization( containerRef, { - labelKey: labelKey as ResponsiveVisualizationAnyItemKey, - aggregatedValueKey: aggregatedValueKey as keyof ResponsiveVisualizationData<'aggregated'>[0], - individualValueKey: individualValueKey as keyof ResponsiveVisualizationData<'individual'>[0], + labelKey, + aggregatedValueKey, + individualValueKey, getAggregatedData, getIndividualData, getIsIndividualSupported: getIsIndividualBarChartSupported, @@ -62,8 +58,8 @@ export function ResponsiveBarChart({ data={data as ResponsiveVisualizationData<'individual'>} color={color} pointSize={individualItemSize} - valueKey={individualValueKey as keyof ResponsiveVisualizationData<'individual'>[0]} - labelKey={labelKey as ResponsiveVisualizationAnyItemKey} + valueKey={individualValueKey} + labelKey={labelKey} onClick={onIndividualItemClick} barLabel={barLabel} customTooltip={individualTooltip} @@ -74,8 +70,8 @@ export function ResponsiveBarChart({ } color={color} - valueKey={aggregatedValueKey as keyof ResponsiveVisualizationData<'aggregated'>[0]} - labelKey={labelKey as ResponsiveVisualizationAnyItemKey} + valueKey={aggregatedValueKey} + labelKey={labelKey} onClick={onAggregatedItemClick} barLabel={barLabel} customTooltip={aggregatedTooltip} diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx index a25f27e5f7..8881d73d23 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx @@ -1,5 +1,5 @@ import { BarChart, Bar, XAxis, Tooltip, ResponsiveContainer, LabelList } from 'recharts' -import type { ResponsiveVisualizationData } from '../../types' +import type { ResponsiveVisualizationValue } from '../../types' import type { BarChartByTypeProps } from '../types' type AggregatedBarChartProps = BarChartByTypeProps<'aggregated'> @@ -14,6 +14,7 @@ export function AggregatedBarChart({ customTooltip, barValueFormatter, }: AggregatedBarChartProps) { + const dataKey = typeof data[0][valueKey] === 'number' ? valueKey : `${valueKey}.value` return ( onClick?.(d.activePayload)} > {data && } - + {/* {valueKeys.map((valueKey) => ( */} + onClick?.(e.activePayload as ResponsiveVisualizationValue)} + > [0]) => - barValueFormatter?.(entry[valueKey] as number) || entry[valueKey] - } + valueAccessor={({ value }: { value: [number, number] }) => { + return barValueFormatter?.(value[1]) || value[1] + }} /> + {/* ))} */}
          {data.map((item, index) => { - const points = item?.[valueKey] as ResponsiveVisualizationItem[] + const points = item?.[valueKey] as ResponsiveVisualizationValue<'individual'>[] return (
          )} diff --git a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx index 3a4ca9ce0d..40d3346e8c 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx @@ -1,11 +1,7 @@ import { useRef } from 'react' import type { ResponsiveVisualizationData } from '../../types' import { getIsIndividualTimeseriesSupported } from '../../lib/density' -import type { - BaseResponsiveChartProps, - BaseResponsiveTimeseriesProps, - ResponsiveVisualizationAnyItemKey, -} from '../types' +import type { BaseResponsiveChartProps, BaseResponsiveTimeseriesProps } from '../types' import { useResponsiveVisualization } from '../hooks' import { DEFAULT_AGGREGATED_VALUE_KEY, @@ -19,7 +15,7 @@ import styles from './Timeseries.module.css' type ResponsiveTimeseriesProps = BaseResponsiveChartProps & BaseResponsiveTimeseriesProps & { - dateKey?: ResponsiveVisualizationAnyItemKey | string + dateKey?: keyof ResponsiveVisualizationData[0] } export function ResponsiveTimeseries({ @@ -44,9 +40,9 @@ export function ResponsiveTimeseries({ start, end, timeseriesInterval, - labelKey: dateKey as ResponsiveVisualizationAnyItemKey, - aggregatedValueKey: aggregatedValueKey as keyof ResponsiveVisualizationData<'aggregated'>[0], - individualValueKey: individualValueKey as keyof ResponsiveVisualizationData<'individual'>[0], + labelKey: dateKey, + aggregatedValueKey, + individualValueKey, getAggregatedData, getIndividualData, getIsIndividualSupported: getIsIndividualTimeseriesSupported, @@ -68,9 +64,9 @@ export function ResponsiveTimeseries({ start={start} end={end} color={color} - dateKey={dateKey as ResponsiveVisualizationAnyItemKey} + dateKey={dateKey} timeseriesInterval={timeseriesInterval} - valueKey={individualValueKey as keyof ResponsiveVisualizationData<'individual'>[0]} + valueKey={individualValueKey} onClick={onIndividualItemClick} tickLabelFormatter={tickLabelFormatter} customTooltip={individualTooltip} @@ -82,9 +78,9 @@ export function ResponsiveTimeseries({ start={start} end={end} color={color} - dateKey={dateKey as ResponsiveVisualizationAnyItemKey} + dateKey={dateKey} timeseriesInterval={timeseriesInterval} - valueKey={aggregatedValueKey as keyof ResponsiveVisualizationData<'aggregated'>[0]} + valueKey={aggregatedValueKey} onClick={onAggregatedItemClick} tickLabelFormatter={tickLabelFormatter} customTooltip={aggregatedTooltip} diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx index d387055a04..c96e47c212 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx @@ -9,7 +9,7 @@ import { } from 'recharts' import min from 'lodash/min' import max from 'lodash/max' -import type { ResponsiveVisualizationAnyItemKey, TimeseriesByTypeProps } from '../types' +import type { TimeseriesByTypeProps } from '../types' import type { ResponsiveVisualizationData } from '../../types' import { useFullTimeseries, useTimeseriesDomain } from './timeseries.hooks' diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx index 9fc3f1b48e..392dc9c2e2 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx @@ -2,7 +2,7 @@ import { XAxis, ResponsiveContainer, ComposedChart, Tooltip } from 'recharts' import cx from 'classnames' import type { ReactElement } from 'react' import type { TimeseriesByTypeProps } from '../types' -import type { ResponsiveVisualizationData, ResponsiveVisualizationItem } from '../../types' +import type { ResponsiveVisualizationData, ResponsiveVisualizationValue } from '../../types' import { IndividualPoint } from '../points/IndividualPoint' import { AXIS_LABEL_PADDING, POINT_GAP, DEFAULT_POINT_SIZE, TIMESERIES_PADDING } from '../config' import styles from './TimeseriesIndividual.module.css' @@ -55,7 +55,7 @@ export function IndividualTimeseries({ style={{ paddingBottom: AXIS_LABEL_PADDING, paddingInline: TIMESERIES_PADDING }} > {fullTimeseries.map((item, index) => { - const points = item?.[valueKey] as ResponsiveVisualizationItem[] + const points = item?.[valueKey] as ResponsiveVisualizationValue<'individual'>[] return (
          = ( +export type ResponsiveVisualizationInteractionCallback = ( item: Item ) => void @@ -16,20 +16,16 @@ export type BaseResponsiveChartProps = { aggregatedTooltip?: ReactElement onAggregatedItemClick?: ResponsiveVisualizationInteractionCallback getAggregatedData?: () => Promise | undefined> - aggregatedValueKey?: keyof ResponsiveVisualizationData<'aggregated'>[0] | string + aggregatedValueKey?: keyof ResponsiveVisualizationData<'aggregated'>[0] // Individual props individualTooltip?: ReactElement individualItem?: ReactElement onIndividualItemClick?: ResponsiveVisualizationInteractionCallback getIndividualData?: () => Promise | undefined> - individualValueKey?: keyof ResponsiveVisualizationData<'individual'>[0] | string + individualValueKey?: keyof ResponsiveVisualizationData<'individual'>[0] individualIcon?: ReactElement } -export type ResponsiveVisualizationAnyItemKey = - | keyof ResponsiveVisualizationData<'aggregated'>[0] - | keyof ResponsiveVisualizationData<'individual'>[0] - // Shared types within the BarChart export type BaseResponsiveBarChartProps = { color?: string @@ -39,7 +35,7 @@ export type BaseResponsiveBarChartProps = { export type BarChartByTypeProps = BaseResponsiveBarChartProps & { - labelKey: ResponsiveVisualizationAnyItemKey + labelKey: keyof ResponsiveVisualizationData[0] valueKey: keyof ResponsiveVisualizationData[0] data: ResponsiveVisualizationData onClick?: ResponsiveVisualizationInteractionCallback @@ -58,7 +54,7 @@ export type BaseResponsiveTimeseriesProps = { export type TimeseriesByTypeProps = BaseResponsiveTimeseriesProps & { - dateKey: ResponsiveVisualizationAnyItemKey + dateKey: keyof ResponsiveVisualizationData[0] valueKey: keyof ResponsiveVisualizationData[0] data: ResponsiveVisualizationData onClick?: ResponsiveVisualizationInteractionCallback diff --git a/libs/responsive-visualizations/src/lib/density.ts b/libs/responsive-visualizations/src/lib/density.ts index 7855d3d873..17e0a921a7 100644 --- a/libs/responsive-visualizations/src/lib/density.ts +++ b/libs/responsive-visualizations/src/lib/density.ts @@ -11,7 +11,14 @@ import { POINT_GAP, POINT_SIZES, } from '../charts/config' -import type { ResponsiveVisualizationData } from '../types' +import type { ResponsiveVisualizationData, ResponsiveVisualizationValue } from '../types' + +export const getItemValue = (value: ResponsiveVisualizationValue) => { + if (typeof value === 'number') { + return value + } + return value.value +} export const getBarProps = ( data: ResponsiveVisualizationData, @@ -34,16 +41,14 @@ export const getColumnsStats = ( aggregatedValueKey: keyof ResponsiveVisualizationData<'aggregated'>[0], individualValueKey: keyof ResponsiveVisualizationData<'individual'>[0] ): ColumnsStats => { - return data.reduce( + return data.reduce( (acc, column) => { - const value = ( - column[aggregatedValueKey as keyof typeof column] - ? column[aggregatedValueKey as keyof typeof column] - : column[individualValueKey as keyof typeof column]?.length || 0 - ) as number + const value: number = column[aggregatedValueKey] + ? getItemValue(column[aggregatedValueKey] as ResponsiveVisualizationValue) + : (column[individualValueKey] as ResponsiveVisualizationValue[])?.length || 0 return { total: acc.total + value, max: Math.max(acc.max, value) } }, - { total: 0, max: 0 } as ColumnsStats + { total: 0, max: 0 } ) } diff --git a/libs/responsive-visualizations/src/types.ts b/libs/responsive-visualizations/src/types.ts index c30e2abdf4..b9e67c0910 100644 --- a/libs/responsive-visualizations/src/types.ts +++ b/libs/responsive-visualizations/src/types.ts @@ -1,40 +1,44 @@ -import type { - DEFAULT_LABEL_KEY, - DEFAULT_DATE_KEY, - DEFAULT_AGGREGATED_VALUE_KEY, - DEFAULT_INDIVIDUAL_VALUE_KEY, -} from './charts/config' - export type ResponsiveVisualizationMode = 'individual' | 'aggregated' export type ResponsiveVisualizationChart = 'barchart' | 'timeseries' -export type ResponsiveVisualizationItem = Record -export type ResponsiveVisualizationAggregatedItem< - Item = { - [DEFAULT_LABEL_KEY]?: string - [DEFAULT_DATE_KEY]?: string - [DEFAULT_AGGREGATED_VALUE_KEY]: number - }, -> = Item +export type ResponsiveVisualizationKey = string + +export type ResponsiveVisualizationLabel = string +export type ResponsiveVisualizationIndividualValue = Record +export type ResponsiveVisualizationAggregatedValue = + | number + | { + label?: string + color?: string + value: number + } + +export type ResponsiveVisualizationValue< + Mode extends ResponsiveVisualizationMode | undefined = undefined, +> = Mode extends 'aggregated' + ? ResponsiveVisualizationAggregatedValue + : Mode extends 'individual' + ? ResponsiveVisualizationIndividualValue + : ResponsiveVisualizationAggregatedValue | ResponsiveVisualizationIndividualValue + +export type ResponsiveVisualizationAggregatedItem = Record< + ResponsiveVisualizationKey, + ResponsiveVisualizationLabel | ResponsiveVisualizationValue<'aggregated'> +> +export type ResponsiveVisualizationIndividualItem = Record< + ResponsiveVisualizationKey, + ResponsiveVisualizationLabel | ResponsiveVisualizationValue<'individual'>[] +> -export type ResponsiveVisualizationIndividualItem< - Item = { - [DEFAULT_LABEL_KEY]?: string - [DEFAULT_DATE_KEY]?: string - [DEFAULT_INDIVIDUAL_VALUE_KEY]: ResponsiveVisualizationItem[] - }, -> = Item +export type ResponsiveVisualizationItem = + | ResponsiveVisualizationAggregatedItem + | ResponsiveVisualizationIndividualItem export type ResponsiveVisualizationData< Mode extends ResponsiveVisualizationMode | undefined = undefined, - Data extends - | ResponsiveVisualizationAggregatedItem - | ResponsiveVisualizationIndividualItem = Mode extends 'aggregated' - ? ResponsiveVisualizationAggregatedItem - : ResponsiveVisualizationIndividualItem, > = Mode extends 'aggregated' - ? ResponsiveVisualizationAggregatedItem[] + ? ResponsiveVisualizationAggregatedItem[] : Mode extends 'individual' - ? ResponsiveVisualizationIndividualItem[] + ? ResponsiveVisualizationIndividualItem[] : (ResponsiveVisualizationAggregatedItem | ResponsiveVisualizationIndividualItem)[] From 1c74734051e29e82682d67c65567330e80b73d10 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Thu, 16 Jan 2025 07:56:02 +0100 Subject: [PATCH 43/62] support stacked responsive bar chart --- .../activity/vessels/ReportVesselsGraph.tsx | 6 +- .../report-activity-vessels.selectors.ts | 33 +++++++---- .../features/reports/shared/reports.utils.ts | 33 +++++------ .../vessels/VesselGroupReportVesselsGraph.tsx | 3 +- .../src/charts/barchart/BarChart.tsx | 14 +++-- .../charts/barchart/BarChartAggregated.tsx | 52 ++++++++++------- .../charts/barchart/BarChartIndividual.tsx | 40 +++++++------ .../src/charts/hooks.ts | 57 +++++++++++++++---- .../src/charts/timeseries/Timeseries.tsx | 15 +++-- .../timeseries/TimeseriesAggregated.tsx | 11 ++-- .../timeseries/TimeseriesIndividual.tsx | 7 ++- .../src/charts/timeseries/timeseries.hooks.ts | 3 +- .../src/charts/types.ts | 18 ++++-- .../src/lib/density.ts | 40 ++++++++----- libs/responsive-visualizations/src/types.ts | 29 ++++++---- 15 files changed, 232 insertions(+), 129 deletions(-) diff --git a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx index a862e39c56..cc5d35723b 100644 --- a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx @@ -100,7 +100,7 @@ const CustomTick = (props: any) => { ? cleanFlagState( ( othersData?.flatMap((d) => - EMPTY_API_VALUES.includes(d.name) ? [] : getTickLabel(d.name) + EMPTY_API_VALUES.includes(d.name as string) ? [] : getTickLabel(d.name as string) ) || [] ).join('|') ) @@ -198,7 +198,7 @@ export default function ReportVesselsGraph() { ? cleanFlagState( ( othersData?.flatMap((d) => - EMPTY_API_VALUES.includes(d.name) ? [] : getTickLabel(d.name) + EMPTY_API_VALUES.includes(d.name as string) ? [] : getTickLabel(d.name as string) ) || [] ).join('|') ) @@ -235,7 +235,7 @@ export default function ReportVesselsGraph() { getAggregatedData={getAggregatedData} // onAggregatedItemClick={onBarClick} // onIndividualItemClick={onPointClick} - aggregatedValueKey={dataviews[0]?.id} + aggregatedValueKey={dataviews.map((dataview) => dataview.id)} barValueFormatter={(value: any) => { return formatI18nNumber(value).toString() }} diff --git a/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts b/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts index 7213eb8f3c..65d83090aa 100644 --- a/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts +++ b/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts @@ -2,6 +2,7 @@ import { createSelector } from '@reduxjs/toolkit' import { groupBy, sum, sumBy, uniq, uniqBy } from 'es-toolkit' import { t } from 'i18next' import { DatasetTypes } from '@globalfishingwatch/api-types' +import type { ResponsiveVisualizationData } from '@globalfishingwatch/responsive-visualizations' import { selectReportVesselGraph, selectReportCategory, @@ -138,7 +139,6 @@ const selectReportVesselsGraphData = createSelector( [selectReportVesselGraph, selectReportVesselsFiltered, selectReportDataviewsWithPermissions], (reportGraph, vesselsFiltered, dataviews) => { if (!vesselsFiltered?.length) return null - const reportData = groupBy(vesselsFiltered, (v) => v.dataviewId || '') const dataByDataview = dataviews.map((dataview) => { @@ -156,11 +156,11 @@ const selectReportVesselsGraphData = createSelector( const allDistributionKeys = uniq(dataByDataview.flatMap(({ data }) => Object.keys(data))) const dataviewIds = dataviews.map((d) => d.id) - const data = allDistributionKeys + const data: ResponsiveVisualizationData<'aggregated'> = allDistributionKeys .flatMap((key) => { const distributionData: Record = { name: key } - dataByDataview.forEach(({ id, data }) => { - distributionData[id] = (data?.[key] || []).length + dataByDataview.forEach(({ id, color, data }) => { + distributionData[id] = { color, value: (data?.[key] || []).length } }) if (sum(dataviewIds.map((d) => distributionData[d])) === 0) return EMPTY_ARRAY return distributionData @@ -177,7 +177,7 @@ const selectReportVesselsGraphData = createSelector( export const selectReportVesselsGraphDataGrouped = createSelector( [selectReportVesselsGraphData, selectReportDataviewsWithPermissions], - (reportGraph, dataviews) => { + (reportGraph, dataviews): ResponsiveVisualizationData<'aggregated'> | null => { if (!reportGraph?.data?.length) return null if (reportGraph?.distributionKeys.length <= MAX_CATEGORIES) return reportGraph.data const dataviewIds = dataviews.map((d) => d.id) @@ -186,7 +186,10 @@ export const selectReportVesselsGraphDataGrouped = createSelector( const others = { name: OTHERS_CATEGORY_LABEL, ...Object.fromEntries( - dataviewIds.map((dataview) => [dataview, sum(rest.map((key: any) => key[dataview]))]) + dataviewIds.map((dataview) => [ + dataview, + { value: sum(rest.map((key: any) => key[dataview]?.value)) }, + ]) ), } return [...top, others] @@ -201,24 +204,30 @@ export const selectReportVesselsGraphIndividualData = createSelector( } ) -const defaultOthersLabel: any[] = [] +const defaultOthersLabel: ResponsiveVisualizationData<'aggregated'> = [] export const selectReportVesselsGraphDataOthers = createSelector( [selectReportVesselsGraphData], - (reportGraph) => { + (reportGraph): ResponsiveVisualizationData<'aggregated'> | null => { if (!reportGraph?.data?.length) return null if (reportGraph?.distributionKeys.length <= MAX_CATEGORIES) return defaultOthersLabel const others = reportGraph.data.slice(MAX_CATEGORIES) + return reportGraph.distributionKeys .flatMap((key) => { const other = others.find((o) => o.name === key) if (!other) return EMPTY_ARRAY const { name, ...rest } = other - return { name, value: sum(Object.values(rest)) } + return { + name, + value: { + value: sum(Object.values(rest).map((v) => (v as any).value)), + }, + } }) .sort((a, b) => { - if (EMPTY_API_VALUES.includes(a.name)) return 1 - if (EMPTY_API_VALUES.includes(b.name)) return -1 - return b.value - a.value + if (EMPTY_API_VALUES.includes(a.name as string)) return 1 + if (EMPTY_API_VALUES.includes(b.name as string)) return -1 + return b.value?.value - a.value?.value }) } ) diff --git a/apps/fishing-map/features/reports/shared/reports.utils.ts b/apps/fishing-map/features/reports/shared/reports.utils.ts index 3d739efb87..bea8795f46 100644 --- a/apps/fishing-map/features/reports/shared/reports.utils.ts +++ b/apps/fishing-map/features/reports/shared/reports.utils.ts @@ -12,6 +12,11 @@ import { OTHER_CATEGORY_LABEL } from '../vessel-groups/vessel-group-report.confi import type { VesselGroupVesselTableParsed } from '../vessel-groups/vessels/vessel-group-report-vessels.selectors' import type { ReportVesselGraph } from '../areas/area-reports.types' +type VesselVisualizationData = ResponsiveVisualizationData< + 'individual', + { name: string; values: any[] } +> + export function getVesselIndividualGroupedData( vessels: (EventsStatsVessel | VesselGroupVesselTableParsed | ReportVesselWithDatasets)[], groupByProperty: VGRSubsection | VGREventsVesselsProperty | ReportVesselGraph @@ -50,20 +55,16 @@ export function getVesselIndividualGroupedData( break } } - const orderedGroups: ResponsiveVisualizationData<'individual', { name: string; values: any[] }> = - Object.entries(vesselsGrouped) - .map(([key, value]) => ({ - name: key, - values: value as any[], - })) - .sort((a, b) => { - return b.values.length - a.values.length - }) - const groupsWithoutOther: ResponsiveVisualizationData< - 'individual', - { name: string; values: any[] } - > = [] - const otherGroups: ResponsiveVisualizationData<'individual', { name: string; values: any[] }> = [] + const orderedGroups: VesselVisualizationData = Object.entries(vesselsGrouped) + .map(([key, value]) => ({ + name: key, + values: value as any[], + })) + .sort((a, b) => { + return b.values.length - a.values.length + }) + const groupsWithoutOther: VesselVisualizationData = [] + const otherGroups: VesselVisualizationData = [] orderedGroups.forEach((group) => { if ( group.name === 'null' || @@ -86,7 +87,7 @@ export function getVesselIndividualGroupedData( ] : groupsWithoutOther if (allGroups.length <= MAX_CATEGORIES) { - return allGroups as ResponsiveVisualizationData<'individual'> + return allGroups as VesselVisualizationData } const firstGroups = allGroups.slice(0, MAX_CATEGORIES) const restOfGroups = allGroups.slice(MAX_CATEGORIES) @@ -97,5 +98,5 @@ export function getVesselIndividualGroupedData( name: OTHER_CATEGORY_LABEL, values: restOfGroups.flatMap((group) => group.values), }, - ] as ResponsiveVisualizationData<'individual'> + ] as VesselVisualizationData } diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx index c0467f4f9e..1871177ea8 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx @@ -168,8 +168,7 @@ export default function VesselGroupReportVesselsGraph({ }: VesselGroupReportVesselsGraphProps) { const { dispatchQueryParams } = useLocationConnect() - const onBarClick: ResponsiveVisualizationInteractionCallback = (e) => { - const { payload } = e.activePayload?.[0] || {} + const onBarClick: ResponsiveVisualizationInteractionCallback = (payload: any) => { if (payload && payload?.name !== OTHER_CATEGORY_LABEL) { dispatchQueryParams({ [filterQueryParam]: `${FILTER_PROPERTIES[property as VGRVesselsSubsection]}:${ diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx index f1b7f89431..16799c91dc 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx @@ -7,7 +7,7 @@ import { DEFAULT_INDIVIDUAL_VALUE_KEY, DEFAULT_LABEL_KEY, } from '../config' -import { useResponsiveVisualization } from '../hooks' +import { useResponsiveVisualization, useValueKeys } from '../hooks' import { BarChartPlaceholder } from '../placeholders/BarChartPlaceholder' import { IndividualBarChart } from './BarChartIndividual' import { AggregatedBarChart } from './BarChartAggregated' @@ -31,13 +31,17 @@ export function ResponsiveBarChart({ onIndividualItemClick, onAggregatedItemClick, }: ResponsiveBarChartProps) { + const { individualValueKeys, aggregatedValueKeys } = useValueKeys( + individualValueKey, + aggregatedValueKey + ) const containerRef = useRef(null) const { data, isIndividualSupported, individualItemSize } = useResponsiveVisualization( containerRef, { labelKey, - aggregatedValueKey, - individualValueKey, + aggregatedValueKeys, + individualValueKeys, getAggregatedData, getIndividualData, getIsIndividualSupported: getIsIndividualBarChartSupported, @@ -58,7 +62,7 @@ export function ResponsiveBarChart({ data={data as ResponsiveVisualizationData<'individual'>} color={color} pointSize={individualItemSize} - valueKey={individualValueKey} + valueKeys={individualValueKeys} labelKey={labelKey} onClick={onIndividualItemClick} barLabel={barLabel} @@ -70,7 +74,7 @@ export function ResponsiveBarChart({ } color={color} - valueKey={aggregatedValueKey} + valueKeys={aggregatedValueKeys} labelKey={labelKey} onClick={onAggregatedItemClick} barLabel={barLabel} diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx index 8881d73d23..70acbc69bb 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx @@ -1,5 +1,8 @@ import { BarChart, Bar, XAxis, Tooltip, ResponsiveContainer, LabelList } from 'recharts' -import type { ResponsiveVisualizationValue } from '../../types' +import type { + ResponsiveVisualizationAggregatedObjectValue, + ResponsiveVisualizationValue, +} from '../../types' import type { BarChartByTypeProps } from '../types' type AggregatedBarChartProps = BarChartByTypeProps<'aggregated'> @@ -8,13 +11,12 @@ export function AggregatedBarChart({ data, color, barLabel, - valueKey, + valueKeys, labelKey, onClick, customTooltip, barValueFormatter, }: AggregatedBarChartProps) { - const dataKey = typeof data[0][valueKey] === 'number' ? valueKey : `${valueKey}.value` return ( onClick?.(d.activePayload)} + onClick={(d: any) => { + onClick?.(d.activePayload[0].payload) + }} > {data && } - {/* {valueKeys.map((valueKey) => ( */} - onClick?.(e.activePayload as ResponsiveVisualizationValue)} - > - { - return barValueFormatter?.(value[1]) || value[1] - }} - /> - - {/* ))} */} + {valueKeys.map((valueKey) => { + const isValueObject = typeof data[0][valueKey] === 'object' + const dataKey = isValueObject ? `${valueKey}.value` : valueKey + const barColor = isValueObject + ? (data[0][valueKey] as ResponsiveVisualizationAggregatedObjectValue).color || color + : color + return ( + onClick?.(e.activePayload as ResponsiveVisualizationValue)} + > + { + return barValueFormatter?.(value[1]) || value[1] + }} + /> + + ) + })}
          {data.map((item, index) => { - const points = item?.[valueKey] as ResponsiveVisualizationValue<'individual'>[] + const totalPoints = valueKeys.reduce((acc, valueKey) => { + const points = item?.[valueKey] as ResponsiveVisualizationValue<'individual'>[] + return acc + points.length + }, 0) return (
          -
            - {points?.map((point, pointIndex) => ( - - ))} -
          + {valueKeys.map((valueKey) => { + const points = item?.[valueKey] as ResponsiveVisualizationValue<'individual'>[] + return ( +
            + {points?.map((point, pointIndex) => ( + + ))} +
          + ) + })}
          ) })} diff --git a/libs/responsive-visualizations/src/charts/hooks.ts b/libs/responsive-visualizations/src/charts/hooks.ts index 202da6ff7a..98cedf3e4c 100644 --- a/libs/responsive-visualizations/src/charts/hooks.ts +++ b/libs/responsive-visualizations/src/charts/hooks.ts @@ -6,7 +6,11 @@ import type { getIsIndividualTimeseriesSupported, IsIndividualSupportedParams, } from '../lib/density' -import type { BaseResponsiveChartProps } from './types' +import type { + BaseResponsiveChartProps, + ResponsiveVisualizationAggregatedValueKey, + ResponsiveVisualizationIndividualValueKey, +} from './types' import { DEFAULT_AGGREGATED_VALUE_KEY, DEFAULT_INDIVIDUAL_VALUE_KEY, @@ -14,6 +18,36 @@ import { DEFAULT_POINT_SIZE, } from './config' +export function useValueKeys( + individualValueKey: + | ResponsiveVisualizationIndividualValueKey + | ResponsiveVisualizationIndividualValueKey[], + aggregatedValueKey: + | ResponsiveVisualizationAggregatedValueKey + | ResponsiveVisualizationIndividualValueKey[] +) { + const individualValueKeysHash = Array.isArray(individualValueKey) + ? individualValueKey.join(',') + : individualValueKey + const individualValueKeys = useMemo( + () => (Array.isArray(individualValueKey) ? individualValueKey : [individualValueKey]), + // eslint-disable-next-line react-hooks/exhaustive-deps + [individualValueKeysHash] + ) + const aggregatedValueKeysHash = Array.isArray(aggregatedValueKey) + ? aggregatedValueKey.join(',') + : aggregatedValueKey + const aggregatedValueKeys = useMemo( + () => (Array.isArray(aggregatedValueKey) ? aggregatedValueKey : [aggregatedValueKey]), + // eslint-disable-next-line react-hooks/exhaustive-deps + [aggregatedValueKeysHash] + ) + return useMemo( + () => ({ individualValueKeys, aggregatedValueKeys }), + [individualValueKeys, aggregatedValueKeys] + ) +} + type ResponsiveVisualizationContainerRef = React.RefObject export function useResponsiveDimensions(containerRef: ResponsiveVisualizationContainerRef) { const [dimensions, setDimensions] = useState({ width: 0, height: 0 }) @@ -45,8 +79,8 @@ type UseResponsiveVisualizationDataProps = { end?: string timeseriesInterval?: FourwingsInterval labelKey: keyof ResponsiveVisualizationData[0] - individualValueKey: keyof ResponsiveVisualizationData<'individual'>[0] - aggregatedValueKey: keyof ResponsiveVisualizationData<'aggregated'>[0] + individualValueKeys: ResponsiveVisualizationIndividualValueKey[] + aggregatedValueKeys: ResponsiveVisualizationAggregatedValueKey[] getAggregatedData?: BaseResponsiveChartProps['getAggregatedData'] getIndividualData?: BaseResponsiveChartProps['getIndividualData'] getIsIndividualSupported: @@ -56,8 +90,8 @@ type UseResponsiveVisualizationDataProps = { export function useResponsiveVisualizationData({ labelKey = DEFAULT_LABEL_KEY, - individualValueKey = DEFAULT_INDIVIDUAL_VALUE_KEY, - aggregatedValueKey = DEFAULT_AGGREGATED_VALUE_KEY, + individualValueKeys = [DEFAULT_INDIVIDUAL_VALUE_KEY], + aggregatedValueKeys = [DEFAULT_AGGREGATED_VALUE_KEY], start, end, timeseriesInterval, @@ -77,8 +111,8 @@ export function useResponsiveVisualizationData({ start, end, timeseriesInterval, - individualValueKey, - aggregatedValueKey, + individualValueKeys, + aggregatedValueKeys, } if (getAggregatedData) { const aggregatedData = await getAggregatedData() @@ -130,10 +164,11 @@ export function useResponsiveVisualizationData({ setData(individualData) } else { const aggregatedData = individualData.map((item) => { - const value = item[individualValueKey] as ResponsiveVisualizationValue[] + // TODO: handle multiple individual value keys + const value = item[individualValueKeys[0]] as ResponsiveVisualizationValue[] return { [labelKey]: item[labelKey as keyof typeof item], - [individualValueKey]: value.length, + [individualValueKeys[0]]: value.length, } }) as ResponsiveVisualizationData<'aggregated'> setIsIndividualSupported(false) @@ -149,8 +184,8 @@ export function useResponsiveVisualizationData({ start, end, timeseriesInterval, - individualValueKey, - aggregatedValueKey, + individualValueKeys, + aggregatedValueKeys, labelKey, ] ) diff --git a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx index 40d3346e8c..498533e619 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx @@ -2,7 +2,7 @@ import { useRef } from 'react' import type { ResponsiveVisualizationData } from '../../types' import { getIsIndividualTimeseriesSupported } from '../../lib/density' import type { BaseResponsiveChartProps, BaseResponsiveTimeseriesProps } from '../types' -import { useResponsiveVisualization } from '../hooks' +import { useResponsiveVisualization, useValueKeys } from '../hooks' import { DEFAULT_AGGREGATED_VALUE_KEY, DEFAULT_INDIVIDUAL_VALUE_KEY, @@ -35,14 +35,19 @@ export function ResponsiveTimeseries({ onAggregatedItemClick, individualIcon, }: ResponsiveTimeseriesProps) { + // TODO: add support for multiple value keys + const { individualValueKeys, aggregatedValueKeys } = useValueKeys( + individualValueKey, + aggregatedValueKey + ) const containerRef = useRef(null) const { width, data, isIndividualSupported } = useResponsiveVisualization(containerRef, { start, end, timeseriesInterval, labelKey: dateKey, - aggregatedValueKey, - individualValueKey, + aggregatedValueKeys, + individualValueKeys, getAggregatedData, getIndividualData, getIsIndividualSupported: getIsIndividualTimeseriesSupported, @@ -66,7 +71,7 @@ export function ResponsiveTimeseries({ color={color} dateKey={dateKey} timeseriesInterval={timeseriesInterval} - valueKey={individualValueKey} + valueKeys={individualValueKeys} onClick={onIndividualItemClick} tickLabelFormatter={tickLabelFormatter} customTooltip={individualTooltip} @@ -80,7 +85,7 @@ export function ResponsiveTimeseries({ color={color} dateKey={dateKey} timeseriesInterval={timeseriesInterval} - valueKey={aggregatedValueKey} + valueKeys={aggregatedValueKeys} onClick={onAggregatedItemClick} tickLabelFormatter={tickLabelFormatter} customTooltip={aggregatedTooltip} diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx index c96e47c212..70df4c7479 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx @@ -22,13 +22,14 @@ export function AggregatedTimeseries({ start, end, dateKey, - valueKey, + valueKeys, customTooltip, timeseriesInterval, tickLabelFormatter, }: AggregatedTimeseriesProps) { - const dataMin: number = data.length ? (min(data.map((item) => item[valueKey])) as number) : 0 - const dataMax: number = data.length ? (max(data.map((item) => item[valueKey])) as number) : 0 + // TODO: add support for multiple value keys + const dataMin: number = data.length ? (min(data.map((item) => item[valueKeys[0]])) as number) : 0 + const dataMax: number = data.length ? (max(data.map((item) => item[valueKeys[0]])) as number) : 0 const domainPadding = (dataMax - dataMin) / 8 const paddedDomain: [number, number] = [ @@ -43,7 +44,7 @@ export function AggregatedTimeseries({ data, timeseriesInterval, dateKey, - valueKey: valueKey as keyof ResponsiveVisualizationData[0], + valueKey: valueKeys[0] as keyof ResponsiveVisualizationData[0], }) if (!fullTimeseries.length) { @@ -74,7 +75,7 @@ export function AggregatedTimeseries({ {fullTimeseries.map((item, index) => { - const points = item?.[valueKey] as ResponsiveVisualizationValue<'individual'>[] + const points = item?.[valueKeys[0]] as ResponsiveVisualizationValue<'individual'>[] return (
          void +export type ResponsiveVisualizationAggregatedValueKey = + keyof ResponsiveVisualizationData<'aggregated'>[0] + +export type ResponsiveVisualizationIndividualValueKey = + keyof ResponsiveVisualizationData<'individual'>[0] + export type BaseResponsiveChartProps = { // Aggregated props aggregatedTooltip?: ReactElement onAggregatedItemClick?: ResponsiveVisualizationInteractionCallback getAggregatedData?: () => Promise | undefined> - aggregatedValueKey?: keyof ResponsiveVisualizationData<'aggregated'>[0] + aggregatedValueKey?: + | ResponsiveVisualizationAggregatedValueKey + | ResponsiveVisualizationAggregatedValueKey[] // Individual props individualTooltip?: ReactElement individualItem?: ReactElement onIndividualItemClick?: ResponsiveVisualizationInteractionCallback getIndividualData?: () => Promise | undefined> - individualValueKey?: keyof ResponsiveVisualizationData<'individual'>[0] + individualValueKey?: + | ResponsiveVisualizationIndividualValueKey + | ResponsiveVisualizationIndividualValueKey[] individualIcon?: ReactElement } @@ -36,7 +46,7 @@ export type BaseResponsiveBarChartProps = { export type BarChartByTypeProps = BaseResponsiveBarChartProps & { labelKey: keyof ResponsiveVisualizationData[0] - valueKey: keyof ResponsiveVisualizationData[0] + valueKeys: (keyof ResponsiveVisualizationData[0])[] data: ResponsiveVisualizationData onClick?: ResponsiveVisualizationInteractionCallback customTooltip?: ReactElement @@ -55,7 +65,7 @@ export type BaseResponsiveTimeseriesProps = { export type TimeseriesByTypeProps = BaseResponsiveTimeseriesProps & { dateKey: keyof ResponsiveVisualizationData[0] - valueKey: keyof ResponsiveVisualizationData[0] + valueKeys: (keyof ResponsiveVisualizationData[0])[] data: ResponsiveVisualizationData onClick?: ResponsiveVisualizationInteractionCallback customTooltip?: ReactElement diff --git a/libs/responsive-visualizations/src/lib/density.ts b/libs/responsive-visualizations/src/lib/density.ts index 17e0a921a7..470035203b 100644 --- a/libs/responsive-visualizations/src/lib/density.ts +++ b/libs/responsive-visualizations/src/lib/density.ts @@ -12,6 +12,10 @@ import { POINT_SIZES, } from '../charts/config' import type { ResponsiveVisualizationData, ResponsiveVisualizationValue } from '../types' +import type { + ResponsiveVisualizationAggregatedValueKey, + ResponsiveVisualizationIndividualValueKey, +} from '../charts/types' export const getItemValue = (value: ResponsiveVisualizationValue) => { if (typeof value === 'number') { @@ -38,14 +42,24 @@ type ColumnsStats = { } export const getColumnsStats = ( data: ResponsiveVisualizationData, - aggregatedValueKey: keyof ResponsiveVisualizationData<'aggregated'>[0], - individualValueKey: keyof ResponsiveVisualizationData<'individual'>[0] + aggregatedValueKeys: ResponsiveVisualizationAggregatedValueKey[], + individualValueKeys: ResponsiveVisualizationIndividualValueKey[] ): ColumnsStats => { return data.reduce( (acc, column) => { - const value: number = column[aggregatedValueKey] - ? getItemValue(column[aggregatedValueKey] as ResponsiveVisualizationValue) - : (column[individualValueKey] as ResponsiveVisualizationValue[])?.length || 0 + const useAggregatedValue = aggregatedValueKeys.every((key) => column[key] !== undefined) + let value = 0 + if (useAggregatedValue) { + value = aggregatedValueKeys.reduce((acc, key) => { + const v = getItemValue(column[key] as ResponsiveVisualizationValue) + return acc + v + }, 0) + } else { + value = individualValueKeys.reduce((acc, key) => { + const v = (column[key] as ResponsiveVisualizationValue[])?.length || 0 + return acc + v + }, 0) + } return { total: acc.total + value, max: Math.max(acc.max, value) } }, { total: 0, max: 0 } @@ -59,8 +73,8 @@ export type IsIndividualSupportedParams = { timeseriesInterval?: FourwingsInterval width: number height: number - aggregatedValueKey: keyof ResponsiveVisualizationData<'aggregated'>[0] - individualValueKey: keyof ResponsiveVisualizationData<'individual'>[0] + aggregatedValueKeys: ResponsiveVisualizationAggregatedValueKey[] + individualValueKeys: ResponsiveVisualizationIndividualValueKey[] } type IsIndividualSupportedResult = { isSupported: boolean @@ -70,10 +84,10 @@ export function getIsIndividualBarChartSupported({ data, width, height, - aggregatedValueKey, - individualValueKey, + aggregatedValueKeys, + individualValueKeys, }: IsIndividualSupportedParams): IsIndividualSupportedResult { - const { total, max } = getColumnsStats(data, aggregatedValueKey, individualValueKey) + const { total, max } = getColumnsStats(data, aggregatedValueKeys, individualValueKeys) if (total > MAX_INDIVIDUAL_ITEMS) { return { isSupported: false } } @@ -93,10 +107,10 @@ export function getIsIndividualTimeseriesSupported({ start, end, timeseriesInterval, - aggregatedValueKey, - individualValueKey, + aggregatedValueKeys, + individualValueKeys, }: IsIndividualSupportedParams): IsIndividualSupportedResult { - const { total, max } = getColumnsStats(data, aggregatedValueKey, individualValueKey) + const { total, max } = getColumnsStats(data, aggregatedValueKeys, individualValueKeys) if (total > MAX_INDIVIDUAL_ITEMS) { return { isSupported: false } } diff --git a/libs/responsive-visualizations/src/types.ts b/libs/responsive-visualizations/src/types.ts index b9e67c0910..583b701812 100644 --- a/libs/responsive-visualizations/src/types.ts +++ b/libs/responsive-visualizations/src/types.ts @@ -6,13 +6,15 @@ export type ResponsiveVisualizationKey = string export type ResponsiveVisualizationLabel = string export type ResponsiveVisualizationIndividualValue = Record +export type ResponsiveVisualizationAggregatedObjectValue = { + label?: string + color?: string + value: number +} + export type ResponsiveVisualizationAggregatedValue = | number - | { - label?: string - color?: string - value: number - } + | ResponsiveVisualizationAggregatedObjectValue export type ResponsiveVisualizationValue< Mode extends ResponsiveVisualizationMode | undefined = undefined, @@ -31,14 +33,19 @@ export type ResponsiveVisualizationIndividualItem = Record< ResponsiveVisualizationLabel | ResponsiveVisualizationValue<'individual'>[] > -export type ResponsiveVisualizationItem = - | ResponsiveVisualizationAggregatedItem - | ResponsiveVisualizationIndividualItem +export type ResponsiveVisualizationItem< + D = ResponsiveVisualizationAggregatedItem | ResponsiveVisualizationIndividualItem, +> = D export type ResponsiveVisualizationData< Mode extends ResponsiveVisualizationMode | undefined = undefined, + Data extends + | ResponsiveVisualizationAggregatedItem + | ResponsiveVisualizationIndividualItem = Mode extends 'aggregated' + ? ResponsiveVisualizationAggregatedItem + : ResponsiveVisualizationIndividualItem, > = Mode extends 'aggregated' - ? ResponsiveVisualizationAggregatedItem[] + ? ResponsiveVisualizationItem[] : Mode extends 'individual' - ? ResponsiveVisualizationIndividualItem[] - : (ResponsiveVisualizationAggregatedItem | ResponsiveVisualizationIndividualItem)[] + ? ResponsiveVisualizationItem[] + : ResponsiveVisualizationItem[] From 7a23cee391bd3c87cd504fa7bd3a48accb627e48 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Thu, 16 Jan 2025 11:16:49 +0100 Subject: [PATCH 44/62] run lint in responsive-visualization --- .../features/reports/shared/VesselGraphLink.tsx | 3 ++- .../features/reports/shared/reports.utils.ts | 7 +++++-- .../vessel-groups/vessel-group-report.config.ts | 3 ++- ...esselGroupReportVesselsIndividualTooltip.tsx | 6 ++++-- libs/data-transforms/src/dates/dates.ts | 1 + .../src/charts/barchart/BarChart.tsx | 9 ++++++--- .../src/charts/barchart/BarChartAggregated.tsx | 3 ++- .../src/charts/barchart/BarChartIndividual.tsx | 8 +++++--- .../src/charts/hooks.ts | 17 ++++++++++------- .../charts/placeholders/BarChartPlaceholder.tsx | 1 + .../placeholders/TimeseriesPlaceholder.tsx | 1 + .../src/charts/points/IndividualPoint.tsx | 13 ++++++++----- .../src/charts/timeseries/Timeseries.tsx | 13 ++++++++----- .../charts/timeseries/TimeseriesAggregated.tsx | 14 ++++++++------ .../charts/timeseries/TimeseriesIndividual.tsx | 13 ++++++++----- .../src/charts/timeseries/timeseries.hooks.ts | 6 ++++-- .../src/charts/types.ts | 2 ++ .../src/lib/density.ts | 10 ++++++---- libs/responsive-visualizations/src/types.d.ts | 1 + 19 files changed, 84 insertions(+), 47 deletions(-) diff --git a/apps/fishing-map/features/reports/shared/VesselGraphLink.tsx b/apps/fishing-map/features/reports/shared/VesselGraphLink.tsx index 7652bfc463..884362b7a7 100644 --- a/apps/fishing-map/features/reports/shared/VesselGraphLink.tsx +++ b/apps/fishing-map/features/reports/shared/VesselGraphLink.tsx @@ -1,5 +1,6 @@ -import VesselLink from 'features/vessel/VesselLink' import type { VesselGroupVesselTableParsed } from 'features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors' +import VesselLink from 'features/vessel/VesselLink' + import styles from './VesselGraphLink.module.css' export default function VesselGraphLink({ data }: { data?: VesselGroupVesselTableParsed }) { diff --git a/apps/fishing-map/features/reports/shared/reports.utils.ts b/apps/fishing-map/features/reports/shared/reports.utils.ts index bea8795f46..ff67848dc3 100644 --- a/apps/fishing-map/features/reports/shared/reports.utils.ts +++ b/apps/fishing-map/features/reports/shared/reports.utils.ts @@ -1,16 +1,19 @@ import { groupBy } from 'lodash' + import type { ResponsiveVisualizationData } from '@globalfishingwatch/responsive-visualizations' + import type { - VGRSubsection, VGREventsVesselsProperty, + VGRSubsection, } from 'features/vessel-groups/vessel-groups.types' import { EMPTY_FIELD_PLACEHOLDER } from 'utils/info' + import { MAX_CATEGORIES } from '../areas/area-reports.config' import type { ReportVesselWithDatasets } from '../areas/area-reports.selectors' +import type { ReportVesselGraph } from '../areas/area-reports.types' import type { EventsStatsVessel } from '../ports/ports-report.slice' import { OTHER_CATEGORY_LABEL } from '../vessel-groups/vessel-group-report.config' import type { VesselGroupVesselTableParsed } from '../vessel-groups/vessels/vessel-group-report-vessels.selectors' -import type { ReportVesselGraph } from '../areas/area-reports.types' type VesselVisualizationData = ResponsiveVisualizationData< 'individual', diff --git a/apps/fishing-map/features/reports/vessel-groups/vessel-group-report.config.ts b/apps/fishing-map/features/reports/vessel-groups/vessel-group-report.config.ts index b221d5d256..e4a6056b05 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessel-group-report.config.ts +++ b/apps/fishing-map/features/reports/vessel-groups/vessel-group-report.config.ts @@ -1,7 +1,8 @@ import { REPORT_VESSELS_PER_PAGE } from 'data/config' import type { VesselGroupReportState } from 'features/vessel-groups/vessel-groups.types' -import { FILTER_PROPERTIES } from '../areas/area-reports.utils' + import type { FilterProperty } from '../areas/area-reports.utils' +import { FILTER_PROPERTIES } from '../areas/area-reports.utils' export const OTHER_CATEGORY_LABEL = 'OTHER' diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.tsx index 1e5009368f..551787fae9 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.tsx @@ -1,9 +1,11 @@ import { useTranslation } from 'react-i18next' + import { VesselIdentitySourceEnum } from '@globalfishingwatch/api-types' -import { EMPTY_FIELD_PLACEHOLDER, getVesselShipTypeLabel } from 'utils/info' -import { formatInfoField } from 'utils/info' + import type { VesselGroupVesselTableParsed } from 'features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors' import { getVesselProperty } from 'features/vessel/vessel.utils' +import { EMPTY_FIELD_PLACEHOLDER, formatInfoField,getVesselShipTypeLabel } from 'utils/info' + import styles from './VesselGroupReportVesselsIndividualTooltip.module.css' const VesselGroupReportVesselsIndividualTooltip = ({ diff --git a/libs/data-transforms/src/dates/dates.ts b/libs/data-transforms/src/dates/dates.ts index b45e7ce3ce..9a9d5dd754 100644 --- a/libs/data-transforms/src/dates/dates.ts +++ b/libs/data-transforms/src/dates/dates.ts @@ -1,6 +1,7 @@ import toNumber from 'lodash/toNumber' import type { DateTimeOptions } from 'luxon' import { DateTime } from 'luxon' + import type { FourwingsInterval } from '@globalfishingwatch/deck-loaders' type DateTimeParseFunction = { (timestamp: string, opts: DateTimeOptions | undefined): DateTime } diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx index 16799c91dc..0e5c8c444f 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx @@ -1,7 +1,7 @@ import { useRef } from 'react' -import type { ResponsiveVisualizationData } from '../../types' + import { getIsIndividualBarChartSupported } from '../../lib/density' -import type { BaseResponsiveBarChartProps, BaseResponsiveChartProps } from '../types' +import type { ResponsiveVisualizationData } from '../../types' import { DEFAULT_AGGREGATED_VALUE_KEY, DEFAULT_INDIVIDUAL_VALUE_KEY, @@ -9,8 +9,11 @@ import { } from '../config' import { useResponsiveVisualization, useValueKeys } from '../hooks' import { BarChartPlaceholder } from '../placeholders/BarChartPlaceholder' -import { IndividualBarChart } from './BarChartIndividual' +import type { BaseResponsiveBarChartProps, BaseResponsiveChartProps } from '../types' + import { AggregatedBarChart } from './BarChartAggregated' +import { IndividualBarChart } from './BarChartIndividual' + import styles from './BarChart.module.css' type ResponsiveBarChartProps = BaseResponsiveChartProps & diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx index 70acbc69bb..e81896864b 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx @@ -1,4 +1,5 @@ -import { BarChart, Bar, XAxis, Tooltip, ResponsiveContainer, LabelList } from 'recharts' +import { Bar, BarChart, LabelList,ResponsiveContainer, Tooltip, XAxis } from 'recharts' + import type { ResponsiveVisualizationAggregatedObjectValue, ResponsiveVisualizationValue, diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx index fea31c40c8..1e90ab7c17 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx @@ -1,9 +1,11 @@ -import { BarChart, XAxis, ResponsiveContainer } from 'recharts' import React from 'react' +import { BarChart, ResponsiveContainer, XAxis } from 'recharts' + import type { ResponsiveVisualizationValue } from '../../types' -import type { BarChartByTypeProps } from '../types' -import { IndividualPoint } from '../points/IndividualPoint' import { AXIS_LABEL_PADDING, POINT_GAP } from '../config' +import { IndividualPoint } from '../points/IndividualPoint' +import type { BarChartByTypeProps } from '../types' + import styles from './BarChartIndividual.module.css' type IndividualBarChartProps = BarChartByTypeProps<'individual'> & { pointSize?: number } diff --git a/libs/responsive-visualizations/src/charts/hooks.ts b/libs/responsive-visualizations/src/charts/hooks.ts index 98cedf3e4c..ba71f49985 100644 --- a/libs/responsive-visualizations/src/charts/hooks.ts +++ b/libs/responsive-visualizations/src/charts/hooks.ts @@ -1,22 +1,25 @@ -import { useState, useEffect, useCallback, useMemo } from 'react' +import { useCallback, useEffect, useMemo,useState } from 'react' + import type { FourwingsInterval } from '@globalfishingwatch/deck-loaders' -import type { ResponsiveVisualizationData, ResponsiveVisualizationValue } from '../types' + import type { getIsIndividualBarChartSupported, getIsIndividualTimeseriesSupported, IsIndividualSupportedParams, } from '../lib/density' -import type { - BaseResponsiveChartProps, - ResponsiveVisualizationAggregatedValueKey, - ResponsiveVisualizationIndividualValueKey, -} from './types' +import type { ResponsiveVisualizationData, ResponsiveVisualizationValue } from '../types' + import { DEFAULT_AGGREGATED_VALUE_KEY, DEFAULT_INDIVIDUAL_VALUE_KEY, DEFAULT_LABEL_KEY, DEFAULT_POINT_SIZE, } from './config' +import type { + BaseResponsiveChartProps, + ResponsiveVisualizationAggregatedValueKey, + ResponsiveVisualizationIndividualValueKey, +} from './types' export function useValueKeys( individualValueKey: diff --git a/libs/responsive-visualizations/src/charts/placeholders/BarChartPlaceholder.tsx b/libs/responsive-visualizations/src/charts/placeholders/BarChartPlaceholder.tsx index 1d99330f4b..802c756447 100644 --- a/libs/responsive-visualizations/src/charts/placeholders/BarChartPlaceholder.tsx +++ b/libs/responsive-visualizations/src/charts/placeholders/BarChartPlaceholder.tsx @@ -1,4 +1,5 @@ import cx from 'classnames' + import styles from './placeholders.module.css' export function BarChartPlaceholder({ diff --git a/libs/responsive-visualizations/src/charts/placeholders/TimeseriesPlaceholder.tsx b/libs/responsive-visualizations/src/charts/placeholders/TimeseriesPlaceholder.tsx index f5bcdc1b9c..4d64e91cca 100644 --- a/libs/responsive-visualizations/src/charts/placeholders/TimeseriesPlaceholder.tsx +++ b/libs/responsive-visualizations/src/charts/placeholders/TimeseriesPlaceholder.tsx @@ -1,4 +1,5 @@ import cx from 'classnames' + import styles from './placeholders.module.css' export default function TimeseriesPlaceholder({ diff --git a/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx b/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx index 1a93536039..e5bfdae8cf 100644 --- a/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx +++ b/libs/responsive-visualizations/src/charts/points/IndividualPoint.tsx @@ -1,17 +1,20 @@ +import { cloneElement, type ReactElement,useCallback, useState } from 'react' import { - useFloating, - offset, flip, + FloatingPortal, + offset, shift, - useInteractions, + useFloating, useHover, - FloatingPortal, + useInteractions, } from '@floating-ui/react' -import { cloneElement, useCallback, useState, type ReactElement } from 'react' import cx from 'classnames' + import type { ResponsiveVisualizationInteractionCallback } from '@globalfishingwatch/responsive-visualizations' + import type { ResponsiveVisualizationValue } from '../../types' import { DEFAULT_POINT_SIZE } from '../config' + import styles from './IndividualPoint.module.css' type IndividualPointProps = { diff --git a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx index 498533e619..ec2784a5c3 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx @@ -1,16 +1,19 @@ import { useRef } from 'react' -import type { ResponsiveVisualizationData } from '../../types' + import { getIsIndividualTimeseriesSupported } from '../../lib/density' -import type { BaseResponsiveChartProps, BaseResponsiveTimeseriesProps } from '../types' -import { useResponsiveVisualization, useValueKeys } from '../hooks' +import type { ResponsiveVisualizationData } from '../../types' import { DEFAULT_AGGREGATED_VALUE_KEY, - DEFAULT_INDIVIDUAL_VALUE_KEY, DEFAULT_DATE_KEY, + DEFAULT_INDIVIDUAL_VALUE_KEY, } from '../config' +import { useResponsiveVisualization, useValueKeys } from '../hooks' import TimeseriesPlaceholder from '../placeholders/TimeseriesPlaceholder' -import { IndividualTimeseries } from './TimeseriesIndividual' +import type { BaseResponsiveChartProps, BaseResponsiveTimeseriesProps } from '../types' + import { AggregatedTimeseries } from './TimeseriesAggregated' +import { IndividualTimeseries } from './TimeseriesIndividual' + import styles from './Timeseries.module.css' type ResponsiveTimeseriesProps = BaseResponsiveChartProps & diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx index 70df4c7479..7415e84cdf 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx @@ -1,16 +1,18 @@ +import max from 'lodash/max' +import min from 'lodash/min' import { - XAxis, - ResponsiveContainer, - YAxis, CartesianGrid, ComposedChart, Line, + ResponsiveContainer, Tooltip, + XAxis, + YAxis, } from 'recharts' -import min from 'lodash/min' -import max from 'lodash/max' -import type { TimeseriesByTypeProps } from '../types' + import type { ResponsiveVisualizationData } from '../../types' +import type { TimeseriesByTypeProps } from '../types' + import { useFullTimeseries, useTimeseriesDomain } from './timeseries.hooks' const graphMargin = { top: 0, right: 0, left: -20, bottom: -10 } diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx index 03efd7563f..dcee79f9cc 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx @@ -1,13 +1,16 @@ -import { XAxis, ResponsiveContainer, ComposedChart, Tooltip } from 'recharts' -import cx from 'classnames' import type { ReactElement } from 'react' -import type { TimeseriesByTypeProps } from '../types' +import cx from 'classnames' +import { ComposedChart, ResponsiveContainer, Tooltip,XAxis } from 'recharts' + import type { ResponsiveVisualizationData, ResponsiveVisualizationValue } from '../../types' +import { AXIS_LABEL_PADDING, DEFAULT_POINT_SIZE, POINT_GAP, TIMESERIES_PADDING } from '../config' import { IndividualPoint } from '../points/IndividualPoint' -import { AXIS_LABEL_PADDING, POINT_GAP, DEFAULT_POINT_SIZE, TIMESERIES_PADDING } from '../config' -import styles from './TimeseriesIndividual.module.css' +import type { TimeseriesByTypeProps } from '../types' + import { useFullTimeseries, useTimeseriesDomain } from './timeseries.hooks' +import styles from './TimeseriesIndividual.module.css' + const graphMargin = { top: 0, right: DEFAULT_POINT_SIZE, left: DEFAULT_POINT_SIZE, bottom: 0 } type IndividualTimeseriesProps = TimeseriesByTypeProps<'individual'> & { diff --git a/libs/responsive-visualizations/src/charts/timeseries/timeseries.hooks.ts b/libs/responsive-visualizations/src/charts/timeseries/timeseries.hooks.ts index 5563dcf373..af9d3b5059 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/timeseries.hooks.ts +++ b/libs/responsive-visualizations/src/charts/timeseries/timeseries.hooks.ts @@ -1,8 +1,10 @@ +import { useMemo } from 'react' import type { DurationUnit } from 'luxon' import { Duration } from 'luxon' -import { useMemo } from 'react' -import type { FourwingsInterval } from '@globalfishingwatch/deck-loaders' + import { getUTCDateTime } from '@globalfishingwatch/data-transforms/dates' +import type { FourwingsInterval } from '@globalfishingwatch/deck-loaders' + import type { ResponsiveVisualizationData } from '../../types' export function useTimeseriesDomain({ diff --git a/libs/responsive-visualizations/src/charts/types.ts b/libs/responsive-visualizations/src/charts/types.ts index cf5b60b6dc..dea3716171 100644 --- a/libs/responsive-visualizations/src/charts/types.ts +++ b/libs/responsive-visualizations/src/charts/types.ts @@ -1,5 +1,7 @@ import type { ReactElement } from 'react' + import type { FourwingsInterval } from '@globalfishingwatch/deck-loaders' + import type { ResponsiveVisualizationData, ResponsiveVisualizationMode, diff --git a/libs/responsive-visualizations/src/lib/density.ts b/libs/responsive-visualizations/src/lib/density.ts index 470035203b..dad0bd3422 100644 --- a/libs/responsive-visualizations/src/lib/density.ts +++ b/libs/responsive-visualizations/src/lib/density.ts @@ -1,21 +1,23 @@ import type { DurationUnit } from 'luxon' import { DateTime, Duration } from 'luxon' + import type { FourwingsInterval } from '@globalfishingwatch/deck-loaders' + import { - COLUMN_PADDING, - DEFAULT_POINT_SIZE, AXIS_LABEL_PADDING, COLUMN_LABEL_SIZE, - TIMESERIES_PADDING, + COLUMN_PADDING, + DEFAULT_POINT_SIZE, MAX_INDIVIDUAL_ITEMS, POINT_GAP, POINT_SIZES, + TIMESERIES_PADDING, } from '../charts/config' -import type { ResponsiveVisualizationData, ResponsiveVisualizationValue } from '../types' import type { ResponsiveVisualizationAggregatedValueKey, ResponsiveVisualizationIndividualValueKey, } from '../charts/types' +import type { ResponsiveVisualizationData, ResponsiveVisualizationValue } from '../types' export const getItemValue = (value: ResponsiveVisualizationValue) => { if (typeof value === 'number') { diff --git a/libs/responsive-visualizations/src/types.d.ts b/libs/responsive-visualizations/src/types.d.ts index 9e78d237dd..7ec92c52d4 100644 --- a/libs/responsive-visualizations/src/types.d.ts +++ b/libs/responsive-visualizations/src/types.d.ts @@ -1,6 +1,7 @@ declare module '*.svg' { // eslint-disable-next-line @typescript-eslint/no-require-imports import React = require('react') + export const ReactComponent: React.FC> const src: string export default src From e4a328927b394b69a6853258cfab57449c3a14bd Mon Sep 17 00:00:00 2001 From: j8seangel Date: Thu, 16 Jan 2025 12:02:17 +0100 Subject: [PATCH 45/62] render individual point with its color for vessels in activity tab --- .../reports/areas/area-reports.selectors.ts | 6 +++--- .../activity/vessels/ReportVesselsGraph.tsx | 3 +-- .../activity/vessels/ReportVesselsTable.tsx | 7 ++----- .../vessels/ReportVesselsTableFooter.tsx | 3 +-- .../report-activity-vessels.selectors.ts | 2 +- .../src/charts/barchart/BarChartAggregated.tsx | 18 ++++++++++-------- .../src/charts/barchart/BarChartIndividual.tsx | 2 +- 7 files changed, 19 insertions(+), 22 deletions(-) diff --git a/apps/fishing-map/features/reports/areas/area-reports.selectors.ts b/apps/fishing-map/features/reports/areas/area-reports.selectors.ts index da331e3d9a..00140c6855 100644 --- a/apps/fishing-map/features/reports/areas/area-reports.selectors.ts +++ b/apps/fishing-map/features/reports/areas/area-reports.selectors.ts @@ -47,7 +47,7 @@ const EMPTY_ARRAY: [] = [] type ReportVesselWithMeta = ReportVessel & { // Merging detections or hours depending on the activity unit into the same property value: number - sourceColor: string + color: string activityDatasetId: string category: ReportCategory dataviewId: string @@ -57,7 +57,7 @@ type ReportVesselWithMeta = ReportVessel & { export type ReportVesselWithDatasets = Pick & Partial & - Pick & { + Pick & { infoDataset?: Dataset trackDataset?: Dataset dataviewId?: string @@ -132,8 +132,8 @@ export const selectReportActivityFlatten = createSelector( : vessel.shipName, activityDatasetId: datasetId, dataviewId: dataview?.id, + color: dataview?.config?.color, category: getReportCategoryFromDataview(dataview), - sourceColor: dataview?.config?.color, } as ReportVesselWithMeta }) }) diff --git a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx index 83efd2bae0..5bbc69a715 100644 --- a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx @@ -23,13 +23,12 @@ import { selectReportVesselsGraphIndividualData, } from 'features/reports/shared/activity/vessels/report-activity-vessels.selectors' import { cleanFlagState } from 'features/reports/shared/activity/vessels/report-activity-vessels.utils' +import { ReportBarGraphPlaceholder } from 'features/reports/shared/placeholders/ReportBarGraphPlaceholder' import VesselGraphLink from 'features/reports/shared/VesselGraphLink' import VesselGroupReportVesselsIndividualTooltip from 'features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip' import { useLocationConnect } from 'routes/routes.hook' import { getVesselGearTypeLabel } from 'utils/info' -import { ReportBarGraphPlaceholder } from '../../placeholders/ReportBarGraphPlaceholder' - import styles from './ReportVesselsGraph.module.css' const MAX_OTHER_TOOLTIP_ITEMS = 10 diff --git a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsTable.tsx b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsTable.tsx index e61e79265f..1c6d891f72 100644 --- a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsTable.tsx +++ b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsTable.tsx @@ -109,11 +109,8 @@ export default function ReportVesselsTable({ activityUnit, reportName }: ReportV />
          - {vessel.sourceColor && ( - + {vessel.color && ( + )} { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { dataviewId, category, sourceColor, flagTranslatedClean, hours, value, ...rest } = - vessel + const { dataviewId, category, color, flagTranslatedClean, hours, value, ...rest } = vessel return { ...rest, value: formatI18nNumber(hours || value) } }) trackEvent({ diff --git a/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts b/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts index 3423ca370c..11873101d8 100644 --- a/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts +++ b/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts @@ -88,7 +88,7 @@ export const selectReportVesselsList = createSelector( value: vesselActivity[0]?.value, infoDataset, trackDataset, - sourceColor: vesselActivity[0]?.sourceColor, + color: vesselActivity[0]?.color, } as ReportVesselWithDatasets }) .sort((a, b) => (b.value as number) - (a.value as number)) diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx index e81896864b..662a8118cf 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx @@ -1,4 +1,4 @@ -import { Bar, BarChart, LabelList,ResponsiveContainer, Tooltip, XAxis } from 'recharts' +import { Bar, BarChart, LabelList, ResponsiveContainer, Tooltip, XAxis } from 'recharts' import type { ResponsiveVisualizationAggregatedObjectValue, @@ -35,7 +35,7 @@ export function AggregatedBarChart({ }} > {data && } - {valueKeys.map((valueKey) => { + {valueKeys.map((valueKey, index) => { const isValueObject = typeof data[0][valueKey] === 'object' const dataKey = isValueObject ? `${valueKey}.value` : valueKey const barColor = isValueObject @@ -49,12 +49,14 @@ export function AggregatedBarChart({ stackId="a" onClick={(e) => onClick?.(e.activePayload as ResponsiveVisualizationValue)} > - { - return barValueFormatter?.(value[1]) || value[1] - }} - /> + {index && valueKeys.length - 1 && ( + { + return barValueFormatter?.(value[1]) || value[1] + }} + /> + )} ) })} diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx index 1e90ab7c17..2c3aa0227d 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx @@ -50,7 +50,7 @@ export function IndividualBarChart({ {valueKeys.map((valueKey) => { const points = item?.[valueKey] as ResponsiveVisualizationValue<'individual'>[] return ( -
            +
              {points?.map((point, pointIndex) => ( Date: Thu, 16 Jan 2025 12:43:39 +0100 Subject: [PATCH 46/62] fix labels for stacks of more than 2 bars --- .../src/charts/barchart/BarChartAggregated.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx index 662a8118cf..70f087bf56 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx @@ -49,7 +49,7 @@ export function AggregatedBarChart({ stackId="a" onClick={(e) => onClick?.(e.activePayload as ResponsiveVisualizationValue)} > - {index && valueKeys.length - 1 && ( + {index === valueKeys.length - 1 && ( { From d782335d3656cfd9bafcb8a5cf5436f7f3c8f4f2 Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Thu, 16 Jan 2025 12:46:18 +0100 Subject: [PATCH 47/62] sort bars --- .../activity/vessels/ReportVesselsGraph.tsx | 8 ++- .../report-activity-vessels.selectors.ts | 56 ++++++++++++------- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx index 5bbc69a715..1bc0d5433c 100644 --- a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx @@ -170,7 +170,9 @@ const CustomTick = (props: any) => { export default function ReportVesselsGraph() { const dataviews = useSelector(selectReportDataviewsWithPermissions) - const data = useSelector(selectReportVesselsGraphDataGrouped) + const valueKeys = dataviews.map((dataview) => dataview.id) + const labelKey = 'name' + const data = useSelector(selectReportVesselsGraphDataGrouped({ labelKey, valueKeys })) const individualData = useSelector(selectReportVesselsGraphIndividualData) const selectedReportVesselGraph = useSelector(selectReportVesselGraph) const othersData = useSelector(selectReportVesselsGraphDataOthers) @@ -238,12 +240,12 @@ export default function ReportVesselsGraph() { getAggregatedData={getAggregatedData} // onAggregatedItemClick={onBarClick} // onIndividualItemClick={onPointClick} - aggregatedValueKey={dataviews.map((dataview) => dataview.id)} + aggregatedValueKey={valueKeys} barValueFormatter={(value: any) => { return formatI18nNumber(value).toString() }} barLabel={} - labelKey={'name'} + labelKey={labelKey} individualTooltip={} individualItem={} aggregatedTooltip={} diff --git a/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts b/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts index 11873101d8..6b75000d5e 100644 --- a/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts +++ b/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts @@ -1,5 +1,5 @@ import { createSelector } from '@reduxjs/toolkit' -import { groupBy, sum, sumBy, uniq, uniqBy } from 'es-toolkit' +import { groupBy, memoize, sum, sumBy, uniq, uniqBy } from 'es-toolkit' import { t } from 'i18next' import { DatasetTypes } from '@globalfishingwatch/api-types' @@ -178,25 +178,41 @@ const selectReportVesselsGraphData = createSelector( } ) -export const selectReportVesselsGraphDataGrouped = createSelector( - [selectReportVesselsGraphData, selectReportDataviewsWithPermissions], - (reportGraph, dataviews): ResponsiveVisualizationData<'aggregated'> | null => { - if (!reportGraph?.data?.length) return null - if (reportGraph?.distributionKeys.length <= MAX_CATEGORIES) return reportGraph.data - const dataviewIds = dataviews.map((d) => d.id) - const top = reportGraph.data.slice(0, MAX_CATEGORIES) - const rest = reportGraph.data.slice(MAX_CATEGORIES) - const others = { - name: OTHERS_CATEGORY_LABEL, - ...Object.fromEntries( - dataviewIds.map((dataview) => [ - dataview, - { value: sum(rest.map((key: any) => key[dataview]?.value)) }, - ]) - ), - } - return [...top, others] - } +export const selectReportVesselsGraphDataGrouped = memoize( + ({ labelKey, valueKeys }: { labelKey: string; valueKeys: string[] }) => + createSelector( + [selectReportVesselsGraphData], + (reportGraph): ResponsiveVisualizationData<'aggregated'> | null => { + if (!reportGraph?.data?.length || !valueKeys?.length) return null + if (reportGraph?.distributionKeys.length <= MAX_CATEGORIES) return reportGraph.data + const sortedData = reportGraph.data.toSorted((a, b) => { + if (a[labelKey] === 'OTHERS' || b[labelKey] === 'OTHERS') { + return -1 + } + const sumA = valueKeys.reduce( + (sum, key) => sum + (typeof a[key] === 'object' ? a[key].value : (a[key] as number)), + 0 + ) + const sumB = valueKeys.reduce( + (sum, key) => sum + (typeof b[key] === 'object' ? b[key].value : (b[key] as number)), + 0 + ) + return sumB - sumA + }) + const top = sortedData.slice(0, MAX_CATEGORIES) + const rest = sortedData.slice(MAX_CATEGORIES) + const others = { + name: OTHERS_CATEGORY_LABEL, + ...Object.fromEntries( + valueKeys.map((valueKey) => [ + valueKey, + { value: sum(rest.map((key: any) => key[valueKey]?.value)) }, + ]) + ), + } + return [...top, others] + } + ) ) export const selectReportVesselsGraphIndividualData = createSelector( From b1c0017c50a727c71d3915f14d8298803fdaea50 Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Thu, 16 Jan 2025 13:02:11 +0100 Subject: [PATCH 48/62] sort individual data in stacked bars --- .../activity/vessels/ReportVesselsGraph.tsx | 2 +- .../report-activity-vessels.selectors.ts | 9 ++++---- .../features/reports/shared/reports.utils.ts | 23 +++++++++++++++---- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx index 1bc0d5433c..6ef41fcf88 100644 --- a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx @@ -173,7 +173,7 @@ export default function ReportVesselsGraph() { const valueKeys = dataviews.map((dataview) => dataview.id) const labelKey = 'name' const data = useSelector(selectReportVesselsGraphDataGrouped({ labelKey, valueKeys })) - const individualData = useSelector(selectReportVesselsGraphIndividualData) + const individualData = useSelector(selectReportVesselsGraphIndividualData(valueKeys)) const selectedReportVesselGraph = useSelector(selectReportVesselGraph) const othersData = useSelector(selectReportVesselsGraphDataOthers) const { dispatchQueryParams } = useLocationConnect() diff --git a/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts b/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts index 6b75000d5e..fee4ae26d3 100644 --- a/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts +++ b/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts @@ -215,12 +215,11 @@ export const selectReportVesselsGraphDataGrouped = memoize( ) ) -export const selectReportVesselsGraphIndividualData = createSelector( - [selectReportVesselsFiltered, selectReportVesselGraph], - (vessels, groupBy) => { +export const selectReportVesselsGraphIndividualData = memoize((valueKeys: string[]) => + createSelector([selectReportVesselsFiltered, selectReportVesselGraph], (vessels, groupBy) => { if (!vessels || !groupBy) return [] - return getVesselIndividualGroupedData(vessels, groupBy) - } + return getVesselIndividualGroupedData(vessels, groupBy, valueKeys) + }) ) const defaultOthersLabel: ResponsiveVisualizationData<'aggregated'> = [] diff --git a/apps/fishing-map/features/reports/shared/reports.utils.ts b/apps/fishing-map/features/reports/shared/reports.utils.ts index ff67848dc3..b97c2aa4db 100644 --- a/apps/fishing-map/features/reports/shared/reports.utils.ts +++ b/apps/fishing-map/features/reports/shared/reports.utils.ts @@ -22,16 +22,26 @@ type VesselVisualizationData = ResponsiveVisualizationData< export function getVesselIndividualGroupedData( vessels: (EventsStatsVessel | VesselGroupVesselTableParsed | ReportVesselWithDatasets)[], - groupByProperty: VGRSubsection | VGREventsVesselsProperty | ReportVesselGraph + groupByProperty: VGRSubsection | VGREventsVesselsProperty | ReportVesselGraph, + valueKeys: string[] ) { if (!vessels?.length) { return [] } + const vesselsSorted = vessels.toSorted((a, b) => { + const aValue = (a as ReportVesselWithDatasets).dataviewId + ? valueKeys.indexOf((a as ReportVesselWithDatasets).dataviewId as string) + : 0 + const bValue = (b as ReportVesselWithDatasets).dataviewId + ? valueKeys.indexOf((b as ReportVesselWithDatasets).dataviewId as string) + : 0 + return aValue - bValue + }) let vesselsGrouped = {} switch (groupByProperty) { case 'flag': { vesselsGrouped = groupBy( - vessels, + vesselsSorted, (vessel) => (vessel as VesselGroupVesselTableParsed).flagTranslatedClean || (vessel as EventsStatsVessel).flagTranslated || @@ -41,7 +51,7 @@ export function getVesselIndividualGroupedData( } case 'shiptype': case 'shiptypes': { - vesselsGrouped = groupBy(vessels, (vessel) => + vesselsGrouped = groupBy(vesselsSorted, (vessel) => (vessel as VesselGroupVesselTableParsed).vesselType ? (vessel as VesselGroupVesselTableParsed).vesselType.split(', ')[0] : (vessel as EventsStatsVessel).shiptypes[0] @@ -50,11 +60,14 @@ export function getVesselIndividualGroupedData( } case 'geartype': case 'geartypes': { - vesselsGrouped = groupBy(vessels, (vessel) => vessel.geartype?.split(', ')[0]) + vesselsGrouped = groupBy(vesselsSorted, (vessel) => vessel.geartype?.split(', ')[0]) break } case 'source': { - vesselsGrouped = groupBy(vessels, (vessel) => (vessel as VesselGroupVesselTableParsed).source) + vesselsGrouped = groupBy( + vesselsSorted, + (vessel) => (vessel as VesselGroupVesselTableParsed).source + ) break } } From e4c72b783b90bafe41a3c281d3814cd552af9655 Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Thu, 16 Jan 2025 13:03:52 +0100 Subject: [PATCH 49/62] fix MMSI in area report vessels tooltip --- .../vessels/VesselGroupReportVesselsIndividualTooltip.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.tsx index 551787fae9..3fb47cf908 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.tsx @@ -4,7 +4,7 @@ import { VesselIdentitySourceEnum } from '@globalfishingwatch/api-types' import type { VesselGroupVesselTableParsed } from 'features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors' import { getVesselProperty } from 'features/vessel/vessel.utils' -import { EMPTY_FIELD_PLACEHOLDER, formatInfoField,getVesselShipTypeLabel } from 'utils/info' +import { EMPTY_FIELD_PLACEHOLDER, formatInfoField, getVesselShipTypeLabel } from 'utils/info' import styles from './VesselGroupReportVesselsIndividualTooltip.module.css' @@ -31,7 +31,7 @@ const VesselGroupReportVesselsIndividualTooltip = ({ const mmsi = data.identity ? getVesselProperty(data.identity, 'ssvid', getVesselPropertyParams) - : data.ssvid + : data.mmsi const vesselFlag = data.identity ? getVesselProperty(data.identity, 'flag', getVesselPropertyParams) From 6f7abf8ac2798b649ad0ccbd7a7a52b7e467cb54 Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Thu, 16 Jan 2025 13:39:12 +0100 Subject: [PATCH 50/62] make mmsi a fallback of ssvid --- .../vessels/VesselGroupReportVesselsIndividualTooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.tsx index 3fb47cf908..724d0ebdf0 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsIndividualTooltip.tsx @@ -31,7 +31,7 @@ const VesselGroupReportVesselsIndividualTooltip = ({ const mmsi = data.identity ? getVesselProperty(data.identity, 'ssvid', getVesselPropertyParams) - : data.mmsi + : data.ssvid || data.mmsi const vesselFlag = data.identity ? getVesselProperty(data.identity, 'flag', getVesselPropertyParams) From ee1588fe60b555b871ead9722755e621ca753b49 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Thu, 16 Jan 2025 15:25:26 +0100 Subject: [PATCH 51/62] avoid unneded memoize --- .../activity/vessels/ReportVesselsGraph.tsx | 9 +-- .../report-activity-vessels.selectors.ts | 71 ++++++++++--------- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx index 6ef41fcf88..9a9309c353 100644 --- a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx @@ -18,7 +18,9 @@ import { EMPTY_API_VALUES, OTHERS_CATEGORY_LABEL } from 'features/reports/areas/ import { selectReportDataviewsWithPermissions } from 'features/reports/areas/area-reports.selectors' import type { ReportVesselGraph } from 'features/reports/areas/area-reports.types' import { + REPORT_GRAPH_LABEL_KEY, selectReportVesselsGraphDataGrouped, + selectReportVesselsGraphDataKeys, selectReportVesselsGraphDataOthers, selectReportVesselsGraphIndividualData, } from 'features/reports/shared/activity/vessels/report-activity-vessels.selectors' @@ -170,9 +172,8 @@ const CustomTick = (props: any) => { export default function ReportVesselsGraph() { const dataviews = useSelector(selectReportDataviewsWithPermissions) - const valueKeys = dataviews.map((dataview) => dataview.id) - const labelKey = 'name' - const data = useSelector(selectReportVesselsGraphDataGrouped({ labelKey, valueKeys })) + const valueKeys = useSelector(selectReportVesselsGraphDataKeys) + const data = useSelector(selectReportVesselsGraphDataGrouped) const individualData = useSelector(selectReportVesselsGraphIndividualData(valueKeys)) const selectedReportVesselGraph = useSelector(selectReportVesselGraph) const othersData = useSelector(selectReportVesselsGraphDataOthers) @@ -245,7 +246,7 @@ export default function ReportVesselsGraph() { return formatI18nNumber(value).toString() }} barLabel={} - labelKey={labelKey} + labelKey={REPORT_GRAPH_LABEL_KEY} individualTooltip={} individualItem={} aggregatedTooltip={} diff --git a/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts b/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts index fee4ae26d3..c444e71d3f 100644 --- a/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts +++ b/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts @@ -178,41 +178,44 @@ const selectReportVesselsGraphData = createSelector( } ) -export const selectReportVesselsGraphDataGrouped = memoize( - ({ labelKey, valueKeys }: { labelKey: string; valueKeys: string[] }) => - createSelector( - [selectReportVesselsGraphData], - (reportGraph): ResponsiveVisualizationData<'aggregated'> | null => { - if (!reportGraph?.data?.length || !valueKeys?.length) return null - if (reportGraph?.distributionKeys.length <= MAX_CATEGORIES) return reportGraph.data - const sortedData = reportGraph.data.toSorted((a, b) => { - if (a[labelKey] === 'OTHERS' || b[labelKey] === 'OTHERS') { - return -1 - } - const sumA = valueKeys.reduce( - (sum, key) => sum + (typeof a[key] === 'object' ? a[key].value : (a[key] as number)), - 0 - ) - const sumB = valueKeys.reduce( - (sum, key) => sum + (typeof b[key] === 'object' ? b[key].value : (b[key] as number)), - 0 - ) - return sumB - sumA - }) - const top = sortedData.slice(0, MAX_CATEGORIES) - const rest = sortedData.slice(MAX_CATEGORIES) - const others = { - name: OTHERS_CATEGORY_LABEL, - ...Object.fromEntries( - valueKeys.map((valueKey) => [ - valueKey, - { value: sum(rest.map((key: any) => key[valueKey]?.value)) }, - ]) - ), - } - return [...top, others] +export const REPORT_GRAPH_LABEL_KEY = 'name' +export const selectReportVesselsGraphDataKeys = createSelector( + [selectReportDataviewsWithPermissions], + (dataviews) => dataviews.map((dataview) => dataview.id) +) + +export const selectReportVesselsGraphDataGrouped = createSelector( + [selectReportVesselsGraphData, selectReportVesselsGraphDataKeys], + (reportGraph, valueKeys): ResponsiveVisualizationData<'aggregated'> | null => { + if (!reportGraph?.data?.length || !valueKeys?.length) return null + if (reportGraph?.distributionKeys.length <= MAX_CATEGORIES) return reportGraph.data + const sortedData = reportGraph.data.toSorted((a, b) => { + if (a[REPORT_GRAPH_LABEL_KEY] === 'OTHERS' || b[REPORT_GRAPH_LABEL_KEY] === 'OTHERS') { + return -1 } - ) + const sumA = valueKeys.reduce( + (sum, key) => sum + (typeof a[key] === 'object' ? a[key].value : (a[key] as number)), + 0 + ) + const sumB = valueKeys.reduce( + (sum, key) => sum + (typeof b[key] === 'object' ? b[key].value : (b[key] as number)), + 0 + ) + return sumB - sumA + }) + const top = sortedData.slice(0, MAX_CATEGORIES) + const rest = sortedData.slice(MAX_CATEGORIES) + const others = { + name: OTHERS_CATEGORY_LABEL, + ...Object.fromEntries( + valueKeys.map((valueKey) => [ + valueKey, + { value: sum(rest.map((key: any) => key[valueKey]?.value)) }, + ]) + ), + } + return [...top, others] + } ) export const selectReportVesselsGraphIndividualData = memoize((valueKeys: string[]) => From a73cd12afcaad3d06e5f9a715f70234c7ece1377 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Thu, 16 Jan 2025 15:45:08 +0100 Subject: [PATCH 52/62] fix build --- apps/fishing-map/features/reports/shared/reports.utils.ts | 3 ++- libs/responsive-visualizations/src/index.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/fishing-map/features/reports/shared/reports.utils.ts b/apps/fishing-map/features/reports/shared/reports.utils.ts index b97c2aa4db..22d5102029 100644 --- a/apps/fishing-map/features/reports/shared/reports.utils.ts +++ b/apps/fishing-map/features/reports/shared/reports.utils.ts @@ -1,6 +1,7 @@ import { groupBy } from 'lodash' import type { ResponsiveVisualizationData } from '@globalfishingwatch/responsive-visualizations' +import { DEFAULT_INDIVIDUAL_VALUE_KEY } from '@globalfishingwatch/responsive-visualizations' import type { VGREventsVesselsProperty, @@ -23,7 +24,7 @@ type VesselVisualizationData = ResponsiveVisualizationData< export function getVesselIndividualGroupedData( vessels: (EventsStatsVessel | VesselGroupVesselTableParsed | ReportVesselWithDatasets)[], groupByProperty: VGRSubsection | VGREventsVesselsProperty | ReportVesselGraph, - valueKeys: string[] + valueKeys: string[] = [DEFAULT_INDIVIDUAL_VALUE_KEY] ) { if (!vessels?.length) { return [] diff --git a/libs/responsive-visualizations/src/index.ts b/libs/responsive-visualizations/src/index.ts index baa86941b2..3d5c762187 100644 --- a/libs/responsive-visualizations/src/index.ts +++ b/libs/responsive-visualizations/src/index.ts @@ -1,2 +1,3 @@ export * from './charts' +export * from './charts/config' export * from './types' From 366d9984711b12a6a302cd4df947e5538168ae3c Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Thu, 16 Jan 2025 16:43:06 +0100 Subject: [PATCH 53/62] fix column sorting --- .../activity/vessels/ReportVesselsGraph.tsx | 52 +------------------ .../report-activity-vessels.selectors.ts | 45 +++++++--------- .../features/reports/shared/reports.utils.ts | 26 +++++----- libs/responsive-visualizations/src/index.ts | 1 + .../src/lib/density.ts | 15 ++++-- 5 files changed, 47 insertions(+), 92 deletions(-) diff --git a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx index 9a9309c353..199a7e3a56 100644 --- a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx @@ -15,7 +15,6 @@ import { import { selectReportVesselGraph } from 'features/app/selectors/app.reports.selector' import I18nNumber, { formatI18nNumber } from 'features/i18n/i18nNumber' import { EMPTY_API_VALUES, OTHERS_CATEGORY_LABEL } from 'features/reports/areas/area-reports.config' -import { selectReportDataviewsWithPermissions } from 'features/reports/areas/area-reports.selectors' import type { ReportVesselGraph } from 'features/reports/areas/area-reports.types' import { REPORT_GRAPH_LABEL_KEY, @@ -171,10 +170,9 @@ const CustomTick = (props: any) => { } export default function ReportVesselsGraph() { - const dataviews = useSelector(selectReportDataviewsWithPermissions) const valueKeys = useSelector(selectReportVesselsGraphDataKeys) const data = useSelector(selectReportVesselsGraphDataGrouped) - const individualData = useSelector(selectReportVesselsGraphIndividualData(valueKeys)) + const individualData = useSelector(selectReportVesselsGraphIndividualData) const selectedReportVesselGraph = useSelector(selectReportVesselGraph) const othersData = useSelector(selectReportVesselsGraphDataOthers) const { dispatchQueryParams } = useLocationConnect() @@ -236,7 +234,6 @@ export default function ReportVesselsGraph() {
              } aggregatedTooltip={} /> - {/* {data ? ( - - - {data && ( - } /> - )} - {dataviews.map((dataview, index) => { - return ( - - {index === dataviews.length - 1 && ( - formatI18nNumber(entry.value[1])} - /> - )} - - ) - })} - } - tickMargin={0} - /> - - - ) : ( - - )} */}
              ) diff --git a/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts b/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts index c444e71d3f..ed9522a73f 100644 --- a/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts +++ b/apps/fishing-map/features/reports/shared/activity/vessels/report-activity-vessels.selectors.ts @@ -1,9 +1,12 @@ import { createSelector } from '@reduxjs/toolkit' -import { groupBy, memoize, sum, sumBy, uniq, uniqBy } from 'es-toolkit' +import { groupBy, sum, sumBy, uniq, uniqBy } from 'es-toolkit' import { t } from 'i18next' import { DatasetTypes } from '@globalfishingwatch/api-types' -import type { ResponsiveVisualizationData } from '@globalfishingwatch/responsive-visualizations' +import { + getResponsiveVisualizationItemValue, + type ResponsiveVisualizationData, +} from '@globalfishingwatch/responsive-visualizations' import { selectReportCategory, @@ -166,12 +169,15 @@ const selectReportVesselsGraphData = createSelector( distributionData[id] = { color, value: (data?.[key] || []).length } }) if (sum(dataviewIds.map((d) => distributionData[d])) === 0) return EMPTY_ARRAY - return distributionData + return distributionData as ResponsiveVisualizationData<'aggregated'> }) .sort((a, b) => { - if (EMPTY_API_VALUES.includes(a.name)) return 1 - if (EMPTY_API_VALUES.includes(b.name)) return -1 - return sum(dataviewIds.map((d: any) => b[d])) - sum(dataviewIds.map((d: any) => a[d])) + if (EMPTY_API_VALUES.includes(a.name as string)) return 1 + if (EMPTY_API_VALUES.includes(b.name as string)) return -1 + return ( + sum(dataviewIds.map((d) => getResponsiveVisualizationItemValue(b[d]))) - + sum(dataviewIds.map((d) => getResponsiveVisualizationItemValue(a[d]))) + ) }) return { distributionKeys: data.map((d) => d.name), data } @@ -189,22 +195,8 @@ export const selectReportVesselsGraphDataGrouped = createSelector( (reportGraph, valueKeys): ResponsiveVisualizationData<'aggregated'> | null => { if (!reportGraph?.data?.length || !valueKeys?.length) return null if (reportGraph?.distributionKeys.length <= MAX_CATEGORIES) return reportGraph.data - const sortedData = reportGraph.data.toSorted((a, b) => { - if (a[REPORT_GRAPH_LABEL_KEY] === 'OTHERS' || b[REPORT_GRAPH_LABEL_KEY] === 'OTHERS') { - return -1 - } - const sumA = valueKeys.reduce( - (sum, key) => sum + (typeof a[key] === 'object' ? a[key].value : (a[key] as number)), - 0 - ) - const sumB = valueKeys.reduce( - (sum, key) => sum + (typeof b[key] === 'object' ? b[key].value : (b[key] as number)), - 0 - ) - return sumB - sumA - }) - const top = sortedData.slice(0, MAX_CATEGORIES) - const rest = sortedData.slice(MAX_CATEGORIES) + const top = reportGraph.data.slice(0, MAX_CATEGORIES) + const rest = reportGraph.data.slice(MAX_CATEGORIES) const others = { name: OTHERS_CATEGORY_LABEL, ...Object.fromEntries( @@ -218,11 +210,12 @@ export const selectReportVesselsGraphDataGrouped = createSelector( } ) -export const selectReportVesselsGraphIndividualData = memoize((valueKeys: string[]) => - createSelector([selectReportVesselsFiltered, selectReportVesselGraph], (vessels, groupBy) => { - if (!vessels || !groupBy) return [] +export const selectReportVesselsGraphIndividualData = createSelector( + [selectReportVesselsFiltered, selectReportVesselGraph, selectReportVesselsGraphDataKeys], + (vessels, groupBy, valueKeys) => { + if (!vessels || !groupBy || !valueKeys) return [] return getVesselIndividualGroupedData(vessels, groupBy, valueKeys) - }) + } ) const defaultOthersLabel: ResponsiveVisualizationData<'aggregated'> = [] diff --git a/apps/fishing-map/features/reports/shared/reports.utils.ts b/apps/fishing-map/features/reports/shared/reports.utils.ts index 22d5102029..5ea8fbf9d4 100644 --- a/apps/fishing-map/features/reports/shared/reports.utils.ts +++ b/apps/fishing-map/features/reports/shared/reports.utils.ts @@ -1,7 +1,6 @@ import { groupBy } from 'lodash' -import type { ResponsiveVisualizationData } from '@globalfishingwatch/responsive-visualizations' -import { DEFAULT_INDIVIDUAL_VALUE_KEY } from '@globalfishingwatch/responsive-visualizations' +import { type ResponsiveVisualizationData } from '@globalfishingwatch/responsive-visualizations' import type { VGREventsVesselsProperty, @@ -24,20 +23,23 @@ type VesselVisualizationData = ResponsiveVisualizationData< export function getVesselIndividualGroupedData( vessels: (EventsStatsVessel | VesselGroupVesselTableParsed | ReportVesselWithDatasets)[], groupByProperty: VGRSubsection | VGREventsVesselsProperty | ReportVesselGraph, - valueKeys: string[] = [DEFAULT_INDIVIDUAL_VALUE_KEY] + dataviewsIdsOrder?: string[] ) { if (!vessels?.length) { return [] } - const vesselsSorted = vessels.toSorted((a, b) => { - const aValue = (a as ReportVesselWithDatasets).dataviewId - ? valueKeys.indexOf((a as ReportVesselWithDatasets).dataviewId as string) - : 0 - const bValue = (b as ReportVesselWithDatasets).dataviewId - ? valueKeys.indexOf((b as ReportVesselWithDatasets).dataviewId as string) - : 0 - return aValue - bValue - }) + const vesselsSorted = dataviewsIdsOrder + ? vessels.toSorted((a, b) => { + const aValue = (a as ReportVesselWithDatasets).dataviewId + ? dataviewsIdsOrder.indexOf((a as ReportVesselWithDatasets).dataviewId as string) + : 0 + const bValue = (b as ReportVesselWithDatasets).dataviewId + ? dataviewsIdsOrder.indexOf((b as ReportVesselWithDatasets).dataviewId as string) + : 0 + return aValue - bValue + }) + : vessels + let vesselsGrouped = {} switch (groupByProperty) { case 'flag': { diff --git a/libs/responsive-visualizations/src/index.ts b/libs/responsive-visualizations/src/index.ts index 3d5c762187..5614f2fbdf 100644 --- a/libs/responsive-visualizations/src/index.ts +++ b/libs/responsive-visualizations/src/index.ts @@ -1,3 +1,4 @@ export * from './charts' export * from './charts/config' +export * from './lib/density' export * from './types' diff --git a/libs/responsive-visualizations/src/lib/density.ts b/libs/responsive-visualizations/src/lib/density.ts index dad0bd3422..aaf629b30b 100644 --- a/libs/responsive-visualizations/src/lib/density.ts +++ b/libs/responsive-visualizations/src/lib/density.ts @@ -17,9 +17,18 @@ import type { ResponsiveVisualizationAggregatedValueKey, ResponsiveVisualizationIndividualValueKey, } from '../charts/types' -import type { ResponsiveVisualizationData, ResponsiveVisualizationValue } from '../types' +import type { + ResponsiveVisualizationData, + ResponsiveVisualizationLabel, + ResponsiveVisualizationValue, +} from '../types' -export const getItemValue = (value: ResponsiveVisualizationValue) => { +export const getResponsiveVisualizationItemValue = ( + value: ResponsiveVisualizationValue | ResponsiveVisualizationLabel +): number => { + if (typeof value === 'string') { + return parseFloat(value) + } if (typeof value === 'number') { return value } @@ -53,7 +62,7 @@ export const getColumnsStats = ( let value = 0 if (useAggregatedValue) { value = aggregatedValueKeys.reduce((acc, key) => { - const v = getItemValue(column[key] as ResponsiveVisualizationValue) + const v = getResponsiveVisualizationItemValue(column[key]) return acc + v }, 0) } else { From 47e9d5546c9c3e977a2f9f4eaee4e42851970f75 Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Thu, 16 Jan 2025 17:04:17 +0100 Subject: [PATCH 54/62] reove animation --- .../src/charts/barchart/BarChartAggregated.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx index 70f087bf56..3b026bcaf7 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx @@ -48,6 +48,7 @@ export function AggregatedBarChart({ fill={barColor} stackId="a" onClick={(e) => onClick?.(e.activePayload as ResponsiveVisualizationValue)} + isAnimationActive={false} > {index === valueKeys.length - 1 && ( Date: Thu, 16 Jan 2025 17:04:31 +0100 Subject: [PATCH 55/62] fix info tooltip in Other bar --- .../shared/activity/vessels/ReportVesselsGraph.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx index 199a7e3a56..3d63d75531 100644 --- a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx @@ -4,7 +4,10 @@ import { useSelector } from 'react-redux' import cx from 'classnames' import type { CategoricalChartFunc } from 'recharts/types/chart/generateCategoricalChart' -import { ResponsiveBarChart } from '@globalfishingwatch/responsive-visualizations' +import { + getResponsiveVisualizationItemValue, + ResponsiveBarChart, +} from '@globalfishingwatch/responsive-visualizations' import { Tooltip as GFWTooltip } from '@globalfishingwatch/ui-components' import { @@ -121,7 +124,9 @@ const CustomTick = (props: any) => { {othersData ?.slice(0, MAX_OTHER_TOOLTIP_ITEMS) .map(({ name, value }) => ( -
            • {`${getTickLabel(name)}: ${value}`}
            • +
            • {`${getTickLabel(name)}: ${getResponsiveVisualizationItemValue(value)}`}
            • ))} {othersData && othersData.length > MAX_OTHER_TOOLTIP_ITEMS && (
            • From 9501ead36e217017b1149ed8e25765e48e60b9bd Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Thu, 16 Jan 2025 17:37:06 +0100 Subject: [PATCH 56/62] fixes and TODOs --- .../shared/events/EventsReportGraph.tsx | 3 +- .../vessels/VesselGroupReportVesselsGraph.tsx | 4 -- .../src/charts/barchart/BarChart.tsx | 17 +++--- .../charts/barchart/BarChartAggregated.tsx | 2 +- .../charts/barchart/BarChartIndividual.tsx | 40 +++++++------- .../src/charts/config.ts | 5 +- .../src/charts/hooks.ts | 52 +++++++------------ .../src/charts/timeseries/Timeseries.tsx | 18 +++---- .../timeseries/TimeseriesAggregated.tsx | 33 ++++++------ .../timeseries/TimeseriesIndividual.tsx | 11 ++-- .../src/charts/types.ts | 8 ++- .../src/lib/density.ts | 17 +++--- 12 files changed, 89 insertions(+), 121 deletions(-) diff --git a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx index d807e6398c..c0fb14e837 100644 --- a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx +++ b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx @@ -45,6 +45,7 @@ type EventsReportGraphTooltipProps = { } const AggregatedGraphTooltip = (props: any) => { + const { t } = useTranslation() const { active, payload, label, timeChunkInterval } = props as EventsReportGraphTooltipProps if (active && payload && payload.length) { @@ -54,7 +55,7 @@ const AggregatedGraphTooltip = (props: any) => {

              {formattedLabel}

              - {formatI18nNumber(payload[0].payload.value)} {payload[0].unit} + {formatI18nNumber(payload[0].payload.value)} {t('common.events', 'Events').toLowerCase()}

              ) diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx index 1a2477b3c9..5c3849db70 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx @@ -181,9 +181,6 @@ export default function VesselGroupReportVesselsGraph({ }) } } - const onPointClick: ResponsiveVisualizationInteractionCallback = (item) => { - console.log('TODO', item) - } const getAggregatedData = useCallback(async () => { return data @@ -200,7 +197,6 @@ export default function VesselGroupReportVesselsGraph({ getIndividualData={getIndividualData} getAggregatedData={getAggregatedData} onAggregatedItemClick={onBarClick} - onIndividualItemClick={onPointClick} barValueFormatter={(value: any) => { return formatI18nNumber(value).toString() }} diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx index 0e5c8c444f..4a5104da37 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChart.tsx @@ -3,8 +3,8 @@ import { useRef } from 'react' import { getIsIndividualBarChartSupported } from '../../lib/density' import type { ResponsiveVisualizationData } from '../../types' import { - DEFAULT_AGGREGATED_VALUE_KEY, - DEFAULT_INDIVIDUAL_VALUE_KEY, + DEFAULT_AGGREGATED_ITEM_KEY, + DEFAULT_INDIVIDUAL_ITEM_KEY, DEFAULT_LABEL_KEY, } from '../config' import { useResponsiveVisualization, useValueKeys } from '../hooks' @@ -23,8 +23,8 @@ export function ResponsiveBarChart({ getIndividualData, getAggregatedData, color, - aggregatedValueKey = DEFAULT_AGGREGATED_VALUE_KEY, - individualValueKey = DEFAULT_INDIVIDUAL_VALUE_KEY, + aggregatedValueKey = DEFAULT_AGGREGATED_ITEM_KEY, + individualValueKey = DEFAULT_INDIVIDUAL_ITEM_KEY, labelKey = DEFAULT_LABEL_KEY, barLabel, aggregatedTooltip, @@ -34,17 +34,14 @@ export function ResponsiveBarChart({ onIndividualItemClick, onAggregatedItemClick, }: ResponsiveBarChartProps) { - const { individualValueKeys, aggregatedValueKeys } = useValueKeys( - individualValueKey, - aggregatedValueKey - ) + const aggregatedValueKeys = useValueKeys(aggregatedValueKey) const containerRef = useRef(null) const { data, isIndividualSupported, individualItemSize } = useResponsiveVisualization( containerRef, { labelKey, aggregatedValueKeys, - individualValueKeys, + individualValueKey, getAggregatedData, getIndividualData, getIsIndividualSupported: getIsIndividualBarChartSupported, @@ -65,7 +62,7 @@ export function ResponsiveBarChart({ data={data as ResponsiveVisualizationData<'individual'>} color={color} pointSize={individualItemSize} - valueKeys={individualValueKeys} + valueKeys={individualValueKey} labelKey={labelKey} onClick={onIndividualItemClick} barLabel={barLabel} diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx index 3b026bcaf7..43a81229dd 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx @@ -35,7 +35,7 @@ export function AggregatedBarChart({ }} > {data && } - {valueKeys.map((valueKey, index) => { + {(valueKeys as string[]).map((valueKey, index) => { const isValueObject = typeof data[0][valueKey] === 'object' const dataKey = isValueObject ? `${valueKey}.value` : valueKey const barColor = isValueObject diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx index 2c3aa0227d..13b1936797 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartIndividual.tsx @@ -38,33 +38,29 @@ export function IndividualBarChart({
              {data.map((item, index) => { - const totalPoints = valueKeys.reduce((acc, valueKey) => { - const points = item?.[valueKey] as ResponsiveVisualizationValue<'individual'>[] - return acc + points.length - }, 0) + const totalPoints = ( + item?.[valueKeys as string] as ResponsiveVisualizationValue<'individual'>[] + ).length return (
              - {valueKeys.map((valueKey) => { - const points = item?.[valueKey] as ResponsiveVisualizationValue<'individual'>[] - return ( -
                - {points?.map((point, pointIndex) => ( - - ))} -
              - ) - })} +
                + {( + item?.[valueKeys as string] as ResponsiveVisualizationValue<'individual'>[] + )?.map((point, pointIndex) => ( + + ))} +
              ) })} diff --git a/libs/responsive-visualizations/src/charts/config.ts b/libs/responsive-visualizations/src/charts/config.ts index 1a8966fbca..f62920db5e 100644 --- a/libs/responsive-visualizations/src/charts/config.ts +++ b/libs/responsive-visualizations/src/charts/config.ts @@ -1,8 +1,7 @@ export const DEFAULT_LABEL_KEY = 'label' export const DEFAULT_DATE_KEY = 'date' -// TODO rename this to item -export const DEFAULT_AGGREGATED_VALUE_KEY = 'value' -export const DEFAULT_INDIVIDUAL_VALUE_KEY = 'values' +export const DEFAULT_AGGREGATED_ITEM_KEY = 'value' +export const DEFAULT_INDIVIDUAL_ITEM_KEY = 'values' export const COLUMN_LABEL_SIZE = 10 export const COLUMN_PADDING = 10 diff --git a/libs/responsive-visualizations/src/charts/hooks.ts b/libs/responsive-visualizations/src/charts/hooks.ts index ba71f49985..a79896ec61 100644 --- a/libs/responsive-visualizations/src/charts/hooks.ts +++ b/libs/responsive-visualizations/src/charts/hooks.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo,useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import type { FourwingsInterval } from '@globalfishingwatch/deck-loaders' @@ -10,8 +10,8 @@ import type { import type { ResponsiveVisualizationData, ResponsiveVisualizationValue } from '../types' import { - DEFAULT_AGGREGATED_VALUE_KEY, - DEFAULT_INDIVIDUAL_VALUE_KEY, + DEFAULT_AGGREGATED_ITEM_KEY, + DEFAULT_INDIVIDUAL_ITEM_KEY, DEFAULT_LABEL_KEY, DEFAULT_POINT_SIZE, } from './config' @@ -22,33 +22,18 @@ import type { } from './types' export function useValueKeys( - individualValueKey: - | ResponsiveVisualizationIndividualValueKey - | ResponsiveVisualizationIndividualValueKey[], - aggregatedValueKey: + valueKey: + | ResponsiveVisualizationAggregatedValueKey[] | ResponsiveVisualizationAggregatedValueKey - | ResponsiveVisualizationIndividualValueKey[] + | ResponsiveVisualizationIndividualValueKey ) { - const individualValueKeysHash = Array.isArray(individualValueKey) - ? individualValueKey.join(',') - : individualValueKey - const individualValueKeys = useMemo( - () => (Array.isArray(individualValueKey) ? individualValueKey : [individualValueKey]), - // eslint-disable-next-line react-hooks/exhaustive-deps - [individualValueKeysHash] - ) - const aggregatedValueKeysHash = Array.isArray(aggregatedValueKey) - ? aggregatedValueKey.join(',') - : aggregatedValueKey - const aggregatedValueKeys = useMemo( - () => (Array.isArray(aggregatedValueKey) ? aggregatedValueKey : [aggregatedValueKey]), + const valueKeysHash = Array.isArray(valueKey) ? valueKey.join(',') : valueKey + const valueKeys = useMemo( + () => (Array.isArray(valueKey) ? valueKey : [valueKey]), // eslint-disable-next-line react-hooks/exhaustive-deps - [aggregatedValueKeysHash] - ) - return useMemo( - () => ({ individualValueKeys, aggregatedValueKeys }), - [individualValueKeys, aggregatedValueKeys] + [valueKeysHash] ) + return valueKeys } type ResponsiveVisualizationContainerRef = React.RefObject @@ -82,7 +67,7 @@ type UseResponsiveVisualizationDataProps = { end?: string timeseriesInterval?: FourwingsInterval labelKey: keyof ResponsiveVisualizationData[0] - individualValueKeys: ResponsiveVisualizationIndividualValueKey[] + individualValueKey: ResponsiveVisualizationIndividualValueKey aggregatedValueKeys: ResponsiveVisualizationAggregatedValueKey[] getAggregatedData?: BaseResponsiveChartProps['getAggregatedData'] getIndividualData?: BaseResponsiveChartProps['getIndividualData'] @@ -93,8 +78,8 @@ type UseResponsiveVisualizationDataProps = { export function useResponsiveVisualizationData({ labelKey = DEFAULT_LABEL_KEY, - individualValueKeys = [DEFAULT_INDIVIDUAL_VALUE_KEY], - aggregatedValueKeys = [DEFAULT_AGGREGATED_VALUE_KEY], + individualValueKey = DEFAULT_INDIVIDUAL_ITEM_KEY, + aggregatedValueKeys = [DEFAULT_AGGREGATED_ITEM_KEY], start, end, timeseriesInterval, @@ -114,7 +99,7 @@ export function useResponsiveVisualizationData({ start, end, timeseriesInterval, - individualValueKeys, + individualValueKey, aggregatedValueKeys, } if (getAggregatedData) { @@ -167,11 +152,10 @@ export function useResponsiveVisualizationData({ setData(individualData) } else { const aggregatedData = individualData.map((item) => { - // TODO: handle multiple individual value keys - const value = item[individualValueKeys[0]] as ResponsiveVisualizationValue[] + const value = item[individualValueKey] as ResponsiveVisualizationValue[] return { [labelKey]: item[labelKey as keyof typeof item], - [individualValueKeys[0]]: value.length, + [individualValueKey]: value.length, } }) as ResponsiveVisualizationData<'aggregated'> setIsIndividualSupported(false) @@ -187,7 +171,7 @@ export function useResponsiveVisualizationData({ start, end, timeseriesInterval, - individualValueKeys, + individualValueKey, aggregatedValueKeys, labelKey, ] diff --git a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx index ec2784a5c3..76c1ce770c 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/Timeseries.tsx @@ -3,9 +3,9 @@ import { useRef } from 'react' import { getIsIndividualTimeseriesSupported } from '../../lib/density' import type { ResponsiveVisualizationData } from '../../types' import { - DEFAULT_AGGREGATED_VALUE_KEY, + DEFAULT_AGGREGATED_ITEM_KEY, DEFAULT_DATE_KEY, - DEFAULT_INDIVIDUAL_VALUE_KEY, + DEFAULT_INDIVIDUAL_ITEM_KEY, } from '../config' import { useResponsiveVisualization, useValueKeys } from '../hooks' import TimeseriesPlaceholder from '../placeholders/TimeseriesPlaceholder' @@ -25,8 +25,8 @@ export function ResponsiveTimeseries({ start, end, dateKey = DEFAULT_DATE_KEY, - aggregatedValueKey = DEFAULT_AGGREGATED_VALUE_KEY, - individualValueKey = DEFAULT_INDIVIDUAL_VALUE_KEY, + aggregatedValueKey = DEFAULT_AGGREGATED_ITEM_KEY, + individualValueKey = DEFAULT_INDIVIDUAL_ITEM_KEY, timeseriesInterval, getIndividualData, getAggregatedData, @@ -38,11 +38,7 @@ export function ResponsiveTimeseries({ onAggregatedItemClick, individualIcon, }: ResponsiveTimeseriesProps) { - // TODO: add support for multiple value keys - const { individualValueKeys, aggregatedValueKeys } = useValueKeys( - individualValueKey, - aggregatedValueKey - ) + const aggregatedValueKeys = useValueKeys(aggregatedValueKey) const containerRef = useRef(null) const { width, data, isIndividualSupported } = useResponsiveVisualization(containerRef, { start, @@ -50,7 +46,7 @@ export function ResponsiveTimeseries({ timeseriesInterval, labelKey: dateKey, aggregatedValueKeys, - individualValueKeys, + individualValueKey, getAggregatedData, getIndividualData, getIsIndividualSupported: getIsIndividualTimeseriesSupported, @@ -74,7 +70,7 @@ export function ResponsiveTimeseries({ color={color} dateKey={dateKey} timeseriesInterval={timeseriesInterval} - valueKeys={individualValueKeys} + valueKeys={individualValueKey} onClick={onIndividualItemClick} tickLabelFormatter={tickLabelFormatter} customTooltip={individualTooltip} diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx index 7415e84cdf..6dd72de538 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx @@ -29,9 +29,12 @@ export function AggregatedTimeseries({ timeseriesInterval, tickLabelFormatter, }: AggregatedTimeseriesProps) { - // TODO: add support for multiple value keys - const dataMin: number = data.length ? (min(data.map((item) => item[valueKeys[0]])) as number) : 0 - const dataMax: number = data.length ? (max(data.map((item) => item[valueKeys[0]])) as number) : 0 + const dataMin: number = data.length + ? (min(data.map((item) => item[valueKeys as string])) as number) + : 0 + const dataMax: number = data.length + ? (max(data.map((item) => item[valueKeys as string])) as number) + : 0 const domainPadding = (dataMax - dataMin) / 8 const paddedDomain: [number, number] = [ @@ -46,7 +49,7 @@ export function AggregatedTimeseries({ data, timeseriesInterval, dateKey, - valueKey: valueKeys[0] as keyof ResponsiveVisualizationData[0], + valueKey: valueKeys as keyof ResponsiveVisualizationData[0], }) if (!fullTimeseries.length) { @@ -74,17 +77,17 @@ export function AggregatedTimeseries({ tickCount={4} /> {data?.length && customTooltip ? : null} - + {(valueKeys as string[]).map((valueKey) => ( + + ))} ) diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx index dcee79f9cc..9c40ac3f9e 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesIndividual.tsx @@ -1,8 +1,8 @@ import type { ReactElement } from 'react' import cx from 'classnames' -import { ComposedChart, ResponsiveContainer, Tooltip,XAxis } from 'recharts' +import { ComposedChart, ResponsiveContainer, Tooltip, XAxis } from 'recharts' -import type { ResponsiveVisualizationData, ResponsiveVisualizationValue } from '../../types' +import type { ResponsiveVisualizationValue } from '../../types' import { AXIS_LABEL_PADDING, DEFAULT_POINT_SIZE, POINT_GAP, TIMESERIES_PADDING } from '../config' import { IndividualPoint } from '../points/IndividualPoint' import type { TimeseriesByTypeProps } from '../types' @@ -37,8 +37,7 @@ export function IndividualTimeseries({ data, timeseriesInterval, dateKey, - // TODO: add support for multiple value keys - valueKey: valueKeys[0] as keyof ResponsiveVisualizationData[0], + valueKey: valueKeys as string, aggregated: false, }) @@ -59,7 +58,9 @@ export function IndividualTimeseries({ style={{ paddingBottom: AXIS_LABEL_PADDING, paddingInline: TIMESERIES_PADDING }} > {fullTimeseries.map((item, index) => { - const points = item?.[valueKeys[0]] as ResponsiveVisualizationValue<'individual'>[] + const points = item?.[ + valueKeys as string + ] as ResponsiveVisualizationValue<'individual'>[] return (
              Promise | undefined> - individualValueKey?: - | ResponsiveVisualizationIndividualValueKey - | ResponsiveVisualizationIndividualValueKey[] + individualValueKey?: ResponsiveVisualizationIndividualValueKey individualIcon?: ReactElement } @@ -48,7 +46,7 @@ export type BaseResponsiveBarChartProps = { export type BarChartByTypeProps = BaseResponsiveBarChartProps & { labelKey: keyof ResponsiveVisualizationData[0] - valueKeys: (keyof ResponsiveVisualizationData[0])[] + valueKeys: (keyof ResponsiveVisualizationData[0])[] | keyof ResponsiveVisualizationData[0] data: ResponsiveVisualizationData onClick?: ResponsiveVisualizationInteractionCallback customTooltip?: ReactElement @@ -67,7 +65,7 @@ export type BaseResponsiveTimeseriesProps = { export type TimeseriesByTypeProps = BaseResponsiveTimeseriesProps & { dateKey: keyof ResponsiveVisualizationData[0] - valueKeys: (keyof ResponsiveVisualizationData[0])[] + valueKeys: (keyof ResponsiveVisualizationData[0])[] | keyof ResponsiveVisualizationData[0] data: ResponsiveVisualizationData onClick?: ResponsiveVisualizationInteractionCallback customTooltip?: ReactElement diff --git a/libs/responsive-visualizations/src/lib/density.ts b/libs/responsive-visualizations/src/lib/density.ts index aaf629b30b..cf00ee1a2c 100644 --- a/libs/responsive-visualizations/src/lib/density.ts +++ b/libs/responsive-visualizations/src/lib/density.ts @@ -54,7 +54,7 @@ type ColumnsStats = { export const getColumnsStats = ( data: ResponsiveVisualizationData, aggregatedValueKeys: ResponsiveVisualizationAggregatedValueKey[], - individualValueKeys: ResponsiveVisualizationIndividualValueKey[] + individualValueKey: ResponsiveVisualizationIndividualValueKey ): ColumnsStats => { return data.reduce( (acc, column) => { @@ -66,10 +66,7 @@ export const getColumnsStats = ( return acc + v }, 0) } else { - value = individualValueKeys.reduce((acc, key) => { - const v = (column[key] as ResponsiveVisualizationValue[])?.length || 0 - return acc + v - }, 0) + value = (column[individualValueKey] as ResponsiveVisualizationValue[])?.length || 0 } return { total: acc.total + value, max: Math.max(acc.max, value) } }, @@ -85,7 +82,7 @@ export type IsIndividualSupportedParams = { width: number height: number aggregatedValueKeys: ResponsiveVisualizationAggregatedValueKey[] - individualValueKeys: ResponsiveVisualizationIndividualValueKey[] + individualValueKey: ResponsiveVisualizationIndividualValueKey } type IsIndividualSupportedResult = { isSupported: boolean @@ -96,9 +93,9 @@ export function getIsIndividualBarChartSupported({ width, height, aggregatedValueKeys, - individualValueKeys, + individualValueKey, }: IsIndividualSupportedParams): IsIndividualSupportedResult { - const { total, max } = getColumnsStats(data, aggregatedValueKeys, individualValueKeys) + const { total, max } = getColumnsStats(data, aggregatedValueKeys, individualValueKey) if (total > MAX_INDIVIDUAL_ITEMS) { return { isSupported: false } } @@ -119,9 +116,9 @@ export function getIsIndividualTimeseriesSupported({ end, timeseriesInterval, aggregatedValueKeys, - individualValueKeys, + individualValueKey, }: IsIndividualSupportedParams): IsIndividualSupportedResult { - const { total, max } = getColumnsStats(data, aggregatedValueKeys, individualValueKeys) + const { total, max } = getColumnsStats(data, aggregatedValueKeys, individualValueKey) if (total > MAX_INDIVIDUAL_ITEMS) { return { isSupported: false } } From 14588ed2c61451d28655313f304f28df02f5e8d8 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Thu, 16 Jan 2025 17:52:42 +0100 Subject: [PATCH 57/62] handle if valueKeys not array --- .../charts/barchart/BarChartAggregated.tsx | 67 ++++++++++++------- .../timeseries/TimeseriesAggregated.tsx | 18 ++++- 2 files changed, 56 insertions(+), 29 deletions(-) diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx index 43a81229dd..cc7f12cd16 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx @@ -35,32 +35,47 @@ export function AggregatedBarChart({ }} > {data && } - {(valueKeys as string[]).map((valueKey, index) => { - const isValueObject = typeof data[0][valueKey] === 'object' - const dataKey = isValueObject ? `${valueKey}.value` : valueKey - const barColor = isValueObject - ? (data[0][valueKey] as ResponsiveVisualizationAggregatedObjectValue).color || color - : color - return ( - onClick?.(e.activePayload as ResponsiveVisualizationValue)} - isAnimationActive={false} - > - {index === valueKeys.length - 1 && ( - { - return barValueFormatter?.(value[1]) || value[1] - }} - /> - )} - - ) - })} + {Array.isArray(valueKeys) ? ( + valueKeys.map((valueKey, index) => { + const isValueObject = typeof data[0][valueKey] === 'object' + const dataKey = isValueObject ? `${valueKey}.value` : valueKey + const barColor = isValueObject + ? (data[0][valueKey] as ResponsiveVisualizationAggregatedObjectValue).color || color + : color + return ( + onClick?.(e.activePayload as ResponsiveVisualizationValue)} + isAnimationActive={false} + > + {index === valueKeys.length - 1 && ( + { + return barValueFormatter?.(value[1]) || value[1] + }} + /> + )} + + ) + }) + ) : ( + onClick?.(e.activePayload as ResponsiveVisualizationValue)} + > + { + return barValueFormatter?.(value[1]) || value[1] + }} + /> + + )} {data?.length && customTooltip ? : null} - {(valueKeys as string[]).map((valueKey) => ( + {Array.isArray(valueKeys) ? ( + valueKeys.map((valueKey) => ( + + )) + ) : ( - ))} + )} ) From c6c5e40edb86ab30817de56697793cf443451271 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Thu, 16 Jan 2025 19:18:07 +0100 Subject: [PATCH 58/62] fix details --- .../reports/shared/events/EventsReportGraph.tsx | 14 ++++++-------- .../src/charts/timeseries/TimeseriesAggregated.tsx | 1 + 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx index c0fb14e837..ad61d660af 100644 --- a/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx +++ b/apps/fishing-map/features/reports/shared/events/EventsReportGraph.tsx @@ -66,7 +66,7 @@ const AggregatedGraphTooltip = (props: any) => { const IndividualGraphTooltip = ({ data, eventType }: { data?: any; eventType?: EventType }) => { const { t } = useTranslation() - if (!data?.vessel?.name) { + if (!data?.vessel) { return null } const { start, duration } = getTimeLabels({ start: data.start, end: data.end }) @@ -79,9 +79,12 @@ const IndividualGraphTooltip = ({ data, eventType }: { data?: any; eventType?: E - {`${formatInfoField(data.vessel?.name, 'shipname')} (${formatInfoField(data.vessel?.flag, 'flag')})`} + + {formatInfoField(data.vessel?.name, 'shipname')}{' '} + {data.vessel?.flag && ({formatInfoField(data.vessel?.flag, 'flag')})} +
              - {eventType === 'encounter' && ( + {eventType === 'encounter' && data.encounter?.vessel?.flag && (
              {`${formatInfoField(data.encounter?.vessel?.name, 'shipname')} (${formatInfoField(data.encounter?.vessel?.flag, 'flag')})`} @@ -161,11 +164,6 @@ export default function EventsReportGraph({ } const data = await GFWAPI.fetch>(`/v3/events?${stringify(params)}`) const groupedData = groupBy(data.entries, (item) => getISODateByInterval(item.start, interval)) - console.log( - Object.entries(groupedData) - .map(([date, events]) => ({ date, values: events })) - .sort((a, b) => a.date.localeCompare(b.date)) - ) return Object.entries(groupedData) .map(([date, events]) => ({ date, values: events })) diff --git a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx index 7b6224a45d..e8c01004ba 100644 --- a/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/timeseries/TimeseriesAggregated.tsx @@ -80,6 +80,7 @@ export function AggregatedTimeseries({ {Array.isArray(valueKeys) ? ( valueKeys.map((valueKey) => ( Date: Fri, 17 Jan 2025 10:01:49 +0100 Subject: [PATCH 59/62] fix vms vessel links --- .../features/reports/shared/VesselGraphLink.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/fishing-map/features/reports/shared/VesselGraphLink.tsx b/apps/fishing-map/features/reports/shared/VesselGraphLink.tsx index 884362b7a7..1c75c83986 100644 --- a/apps/fishing-map/features/reports/shared/VesselGraphLink.tsx +++ b/apps/fishing-map/features/reports/shared/VesselGraphLink.tsx @@ -1,18 +1,24 @@ +import type { ReportVesselWithDatasets } from 'features/reports/areas/area-reports.selectors' import type { VesselGroupVesselTableParsed } from 'features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors' import VesselLink from 'features/vessel/VesselLink' import styles from './VesselGraphLink.module.css' -export default function VesselGraphLink({ data }: { data?: VesselGroupVesselTableParsed }) { +export default function VesselGraphLink({ + data, +}: { + data?: VesselGroupVesselTableParsed | ReportVesselWithDatasets +}) { if (!data) { return null } const { vesselId, dataset } = data + const datasetId = dataset || (data as ReportVesselWithDatasets).infoDataset?.id return ( ) From 98d267395eeb2a96cebfb3bc4cc12063c136a58b Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Fri, 17 Jan 2025 10:05:43 +0100 Subject: [PATCH 60/62] fix onClick crash --- .../src/charts/barchart/BarChartAggregated.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx index cc7f12cd16..9de4e1d3d5 100644 --- a/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx +++ b/libs/responsive-visualizations/src/charts/barchart/BarChartAggregated.tsx @@ -31,7 +31,7 @@ export function AggregatedBarChart({ bottom: 0, }} onClick={(d: any) => { - onClick?.(d.activePayload[0].payload) + onClick?.(d.activePayload?.[0]?.payload) }} > {data && } From 3d62425ef879791d71e7cd8d60e37d101fa924a1 Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Fri, 17 Jan 2025 11:31:59 +0100 Subject: [PATCH 61/62] fix individual shipType grouping --- apps/fishing-map/features/reports/shared/reports.utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/fishing-map/features/reports/shared/reports.utils.ts b/apps/fishing-map/features/reports/shared/reports.utils.ts index 5ea8fbf9d4..11e32e5253 100644 --- a/apps/fishing-map/features/reports/shared/reports.utils.ts +++ b/apps/fishing-map/features/reports/shared/reports.utils.ts @@ -53,6 +53,7 @@ export function getVesselIndividualGroupedData( break } case 'shiptype': + case 'vesselType': case 'shiptypes': { vesselsGrouped = groupBy(vesselsSorted, (vessel) => (vessel as VesselGroupVesselTableParsed).vesselType From 924a4eea83922ca7f77f4e34ed25028b4fe922fc Mon Sep 17 00:00:00 2001 From: j8seangel Date: Fri, 24 Jan 2025 09:30:43 +0100 Subject: [PATCH 62/62] disable individual data --- .../activity/vessels/ReportVesselsGraph.tsx | 8 ++-- .../shared/events/EventsReportGraph.tsx | 40 +++++++++---------- .../insights/VGRInsightCoverageGraph.tsx | 12 +++--- .../vessels/VesselGroupReportVesselsGraph.tsx | 8 ++-- 4 files changed, 34 insertions(+), 34 deletions(-) diff --git a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx index 3d63d75531..ed73e3c189 100644 --- a/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/shared/activity/vessels/ReportVesselsGraph.tsx @@ -219,9 +219,9 @@ export default function ReportVesselsGraph() { } } - const getIndividualData = useCallback(async () => { - return individualData - }, [individualData]) + // const getIndividualData = useCallback(async () => { + // return individualData + // }, [individualData]) const getAggregatedData = useCallback(async () => { return data as any[] @@ -239,7 +239,7 @@ export default function ReportVesselsGraph() {
              timeseries, [timeseries]) - const getIndividualData = useCallback(async () => { - const params = { - ...getEventsStatsQuery({ - start, - end, - filters: filtersMemo, - dataset: datasetId, - }), - ...(includesMemo && { includes: includesMemo }), - limit: 1000, - offset: 0, - } - const data = await GFWAPI.fetch>(`/v3/events?${stringify(params)}`) - const groupedData = groupBy(data.entries, (item) => getISODateByInterval(item.start, interval)) - - return Object.entries(groupedData) - .map(([date, events]) => ({ date, values: events })) - .sort((a, b) => a.date.localeCompare(b.date)) - }, [start, end, filtersMemo, includesMemo, datasetId, interval]) + // const getIndividualData = useCallback(async () => { + // const params = { + // ...getEventsStatsQuery({ + // start, + // end, + // filters: filtersMemo, + // dataset: datasetId, + // }), + // ...(includesMemo && { includes: includesMemo }), + // limit: 1000, + // offset: 0, + // } + // const data = await GFWAPI.fetch>(`/v3/events?${stringify(params)}`) + // const groupedData = groupBy(data.entries, (item) => getISODateByInterval(item.start, interval)) + + // return Object.entries(groupedData) + // .map(([date, events]) => ({ date, values: events })) + // .sort((a, b) => a.date.localeCompare(b.date)) + // }, [start, end, filtersMemo, includesMemo, datasetId, interval]) if (!timeseries.length) { return null @@ -181,7 +181,7 @@ export default function EventsReportGraph({ end={end} timeseriesInterval={interval} getAggregatedData={getAggregatedData} - getIndividualData={getIndividualData} + // getIndividualData={getIndividualData} tickLabelFormatter={formatDateTicks} aggregatedTooltip={} individualTooltip={} diff --git a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx index 1daa92c11c..ffec47c913 100644 --- a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightCoverageGraph.tsx @@ -105,11 +105,11 @@ export default function VesselGroupReportInsightCoverageGraph({ data: VesselGroupInsightResponse['coverage'] }) { const vesselGroup = useSelector(selectVGRData) - const getIndividualData = useCallback(async () => { - if (vesselGroup?.vessels.length) { - return parseCoverageGraphIndividualData(data, vesselGroup.vessels) - } else return [] - }, [data, vesselGroup?.vessels]) + // const getIndividualData = useCallback(async () => { + // if (vesselGroup?.vessels.length) { + // return parseCoverageGraphIndividualData(data, vesselGroup.vessels) + // } else return [] + // }, [data, vesselGroup?.vessels]) const getAggregatedData = useCallback(async () => { if (vesselGroup?.vessels.length) { return parseCoverageGraphAggregatedData(data, vesselGroup.vessels) @@ -121,7 +121,7 @@ export default function VesselGroupReportInsightCoverageGraph({
              { return formatI18nNumber(value).toString() diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx index 5c3849db70..7a3348505c 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsGraph.tsx @@ -186,15 +186,15 @@ export default function VesselGroupReportVesselsGraph({ return data }, [data]) - const getIndividualData = useCallback(async () => { - return individualData - }, [individualData]) + // const getIndividualData = useCallback(async () => { + // return individualData + // }, [individualData]) return (
              {