@carto/ps-react-ui 4.4.2 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/download-config-DemuQ3Jm.js +56 -0
- package/dist/download-config-DemuQ3Jm.js.map +1 -0
- package/dist/error-Cj8eUMrl.js +40 -0
- package/dist/error-Cj8eUMrl.js.map +1 -0
- package/dist/formatter-B9Bxn1k7.js +6 -0
- package/dist/formatter-B9Bxn1k7.js.map +1 -0
- package/dist/no-data-DkIt7Qt1.js +61 -0
- package/dist/no-data-DkIt7Qt1.js.map +1 -0
- package/dist/row-D4VOhcNI.js +34 -0
- package/dist/row-D4VOhcNI.js.map +1 -0
- package/dist/series-Bola3CmD.js +90 -0
- package/dist/series-Bola3CmD.js.map +1 -0
- package/dist/styles-Y8q7Jff3.js +118 -0
- package/dist/styles-Y8q7Jff3.js.map +1 -0
- package/dist/types/widgets/actions/brush-toggle/types.d.ts +8 -2
- package/dist/types/widgets/category/components/category-row-multi.d.ts +2 -1
- package/dist/types/widgets/category/components/category-row-single.d.ts +2 -1
- package/dist/types/widgets/category/types.d.ts +1 -0
- package/dist/types/widgets/echart/shared-resize-observer.d.ts +12 -0
- package/dist/types/widgets/echart/types.d.ts +2 -0
- package/dist/types/widgets/histogram/config.d.ts +15 -3
- package/dist/types/widgets/histogram/index.d.ts +2 -1
- package/dist/types/widgets/histogram/types.d.ts +6 -3
- package/dist/types/widgets/stores/index.d.ts +2 -1
- package/dist/types/widgets/stores/types.d.ts +2 -0
- package/dist/types/widgets/stores/use-widget-selector.d.ts +35 -0
- package/dist/types/widgets/stores/widget-store-performance.test.d.ts +1 -0
- package/dist/types/widgets/stores/widget-store.d.ts +49 -27
- package/dist/types/widgets/table/types.d.ts +1 -1
- package/dist/types/widgets/utils/chart-config/index.d.ts +1 -1
- package/dist/types/widgets/utils/chart-config/option-builders.d.ts +13 -8
- package/dist/types/widgets/utils/formatter.d.ts +1 -0
- package/dist/types/widgets/utils/index.d.ts +1 -1
- package/dist/use-widget-ref-BFazQvJK.js +22 -0
- package/dist/use-widget-ref-BFazQvJK.js.map +1 -0
- package/dist/use-widget-selector-DqRmWQ1K.js +12 -0
- package/dist/use-widget-selector-DqRmWQ1K.js.map +1 -0
- package/dist/widget-store-CIrb9RKP.js +263 -0
- package/dist/widget-store-CIrb9RKP.js.map +1 -0
- package/dist/widgets/actions.js +799 -817
- package/dist/widgets/actions.js.map +1 -1
- package/dist/widgets/bar.js +53 -47
- package/dist/widgets/bar.js.map +1 -1
- package/dist/widgets/category.js +261 -255
- package/dist/widgets/category.js.map +1 -1
- package/dist/widgets/echart.js +109 -99
- package/dist/widgets/echart.js.map +1 -1
- package/dist/widgets/error.js +1 -1
- package/dist/widgets/formula.js +71 -63
- package/dist/widgets/formula.js.map +1 -1
- package/dist/widgets/histogram.js +119 -80
- package/dist/widgets/histogram.js.map +1 -1
- package/dist/widgets/loader.js +53 -60
- package/dist/widgets/loader.js.map +1 -1
- package/dist/widgets/markdown.js +51 -50
- package/dist/widgets/markdown.js.map +1 -1
- package/dist/widgets/no-data.js +1 -1
- package/dist/widgets/pie.js +111 -99
- package/dist/widgets/pie.js.map +1 -1
- package/dist/widgets/range.js +146 -144
- package/dist/widgets/range.js.map +1 -1
- package/dist/widgets/scatterplot.js +50 -44
- package/dist/widgets/scatterplot.js.map +1 -1
- package/dist/widgets/skeleton-loader.js +18 -17
- package/dist/widgets/skeleton-loader.js.map +1 -1
- package/dist/widgets/spread.js +110 -94
- package/dist/widgets/spread.js.map +1 -1
- package/dist/widgets/stores.js +5 -2
- package/dist/widgets/stores.js.map +1 -1
- package/dist/widgets/table.js +422 -436
- package/dist/widgets/table.js.map +1 -1
- package/dist/widgets/timeseries.js +52 -46
- package/dist/widgets/timeseries.js.map +1 -1
- package/dist/widgets/toolbar-actions.js +101 -6693
- package/dist/widgets/toolbar-actions.js.map +1 -1
- package/dist/widgets/utils.js +16 -14
- package/dist/widgets/utils.js.map +1 -1
- package/dist/widgets/wrapper.js +156 -158
- package/dist/widgets/wrapper.js.map +1 -1
- package/dist/widgets.js +4 -4
- package/package.json +5 -4
- package/src/hooks/use-widget-ref.ts +3 -4
- package/src/widgets/README.md +3 -3
- package/src/widgets/actions/brush-toggle/brush-toggle.tsx +60 -79
- package/src/widgets/actions/brush-toggle/types.ts +8 -2
- package/src/widgets/actions/change-column/change-column.tsx +15 -15
- package/src/widgets/actions/change-column/sortable-column-item.tsx +3 -1
- package/src/widgets/actions/download/download.tsx +4 -3
- package/src/widgets/actions/fullscreen/fullscreen.tsx +7 -11
- package/src/widgets/actions/lock-selection/lock-selection.tsx +12 -15
- package/src/widgets/actions/relative-data/relative-data.tsx +22 -26
- package/src/widgets/actions/searcher/searcher-toggle.tsx +11 -12
- package/src/widgets/actions/searcher/searcher.tsx +20 -21
- package/src/widgets/actions/stack-toggle/stack-toggle.tsx +15 -21
- package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +27 -43
- package/src/widgets/bar/config.ts +22 -14
- package/src/widgets/category/category-ui.tsx +31 -27
- package/src/widgets/category/components/category-row-multi.tsx +6 -2
- package/src/widgets/category/components/category-row-single.tsx +5 -1
- package/src/widgets/category/types.ts +1 -0
- package/src/widgets/echart/echart-ui.test.tsx +20 -16
- package/src/widgets/echart/echart-ui.tsx +6 -12
- package/src/widgets/echart/echart.tsx +13 -27
- package/src/widgets/echart/shared-resize-observer.ts +45 -0
- package/src/widgets/echart/types.ts +2 -0
- package/src/widgets/error/error.tsx +7 -9
- package/src/widgets/formula/components/prefix.tsx +4 -6
- package/src/widgets/formula/components/row.tsx +4 -4
- package/src/widgets/formula/components/series.tsx +4 -6
- package/src/widgets/formula/components/suffix.tsx +4 -6
- package/src/widgets/formula/components/value.tsx +9 -16
- package/src/widgets/histogram/config.ts +101 -20
- package/src/widgets/histogram/index.ts +6 -1
- package/src/widgets/histogram/types.ts +9 -3
- package/src/widgets/loader/loader.tsx +31 -44
- package/src/widgets/markdown/markdown.tsx +4 -7
- package/src/widgets/no-data/no-data.tsx +7 -10
- package/src/widgets/pie/config.ts +17 -5
- package/src/widgets/range/components/range-item.tsx +20 -18
- package/src/widgets/scatterplot/config.ts +8 -3
- package/src/widgets/skeleton-loader/skeleton-loader.tsx +2 -5
- package/src/widgets/spread/components/max-value.tsx +14 -16
- package/src/widgets/spread/components/min-value.tsx +14 -16
- package/src/widgets/stores/index.ts +2 -1
- package/src/widgets/stores/types.ts +2 -0
- package/src/widgets/stores/use-widget-selector.ts +47 -0
- package/src/widgets/stores/widget-store-performance.test.ts +750 -0
- package/src/widgets/stores/widget-store.test.ts +81 -0
- package/src/widgets/stores/widget-store.ts +225 -44
- package/src/widgets/table/config.ts +0 -1
- package/src/widgets/table/hooks/use-pagination.ts +28 -52
- package/src/widgets/table/hooks/use-selection.ts +20 -24
- package/src/widgets/table/hooks/use-sort.ts +22 -39
- package/src/widgets/table/types.ts +1 -1
- package/src/widgets/timeseries/config.ts +21 -13
- package/src/widgets/utils/chart-config/index.ts +1 -1
- package/src/widgets/utils/chart-config/option-builders.ts +22 -12
- package/src/widgets/utils/formatter.ts +2 -1
- package/src/widgets/utils/index.ts +1 -1
- package/src/widgets/wrapper/wrapper-ui.tsx +12 -13
- package/src/widgets/wrapper/wrapper.tsx +4 -6
- package/dist/error-CEkRPccv.js +0 -39
- package/dist/error-CEkRPccv.js.map +0 -1
- package/dist/formatter-B1Xh8XDH.js +0 -5
- package/dist/formatter-B1Xh8XDH.js.map +0 -1
- package/dist/no-data-hR3KcJ-_.js +0 -60
- package/dist/no-data-hR3KcJ-_.js.map +0 -1
- package/dist/row-DTCV0Ocm.js +0 -35
- package/dist/row-DTCV0Ocm.js.map +0 -1
- package/dist/series-CYNOu2Ju.js +0 -91
- package/dist/series-CYNOu2Ju.js.map +0 -1
- package/dist/styles-C_8vOEep.js +0 -167
- package/dist/styles-C_8vOEep.js.map +0 -1
- package/dist/use-widget-ref-wtFLDFCD.js +0 -25
- package/dist/use-widget-ref-wtFLDFCD.js.map +0 -1
- package/dist/widget-store-CzDt8oSK.js +0 -163
- package/dist/widget-store-CzDt8oSK.js.map +0 -1
|
@@ -5,8 +5,7 @@ import type {
|
|
|
5
5
|
EchartOptionsProps,
|
|
6
6
|
} from './types'
|
|
7
7
|
import { EchartUI } from './echart-ui'
|
|
8
|
-
import {
|
|
9
|
-
import { useShallow } from 'zustand/shallow'
|
|
8
|
+
import { useWidgetSelector } from '../stores/use-widget-selector'
|
|
10
9
|
import { useMemo } from 'react'
|
|
11
10
|
|
|
12
11
|
/**
|
|
@@ -16,31 +15,18 @@ import { useMemo } from 'react'
|
|
|
16
15
|
* Transforms widget data into ECharts dataset format and delegates rendering to {@link EchartUI}.
|
|
17
16
|
*/
|
|
18
17
|
export function Echart(props: EchartProps) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const widgetOption = useWidgetStore(
|
|
33
|
-
useShallow((state) => state.getWidget<EchartWidgetState>(props.id)?.option),
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
const onEvents = useWidgetStore(
|
|
37
|
-
useShallow(
|
|
38
|
-
(state) => state.getWidget<EchartWidgetState>(props.id)?.onEvents,
|
|
39
|
-
),
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
const init = useWidgetStore(
|
|
43
|
-
(state) => state.getWidget<EchartWidgetState>(props.id)?.init,
|
|
18
|
+
// Single consolidated subscription instead of 5 separate ones.
|
|
19
|
+
const { id, data, widgetOption, onEvents, init } = useWidgetSelector(
|
|
20
|
+
props.id,
|
|
21
|
+
(w) => ({
|
|
22
|
+
id: w?.id,
|
|
23
|
+
data: (w as EchartWidgetState | undefined)?.data as
|
|
24
|
+
| EchartWidgetData
|
|
25
|
+
| undefined,
|
|
26
|
+
widgetOption: (w as EchartWidgetState | undefined)?.option,
|
|
27
|
+
onEvents: (w as EchartWidgetState | undefined)?.onEvents,
|
|
28
|
+
init: (w as EchartWidgetState | undefined)?.init,
|
|
29
|
+
}),
|
|
44
30
|
)
|
|
45
31
|
|
|
46
32
|
// Memoize dataset transformation to avoid re-computing on every render
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ResizeObserver singleton for all ECharts instances.
|
|
3
|
+
*
|
|
4
|
+
* Instead of creating 100+ individual ResizeObserver instances (one per chart),
|
|
5
|
+
* this module provides a single shared observer that efficiently handles resize
|
|
6
|
+
* callbacks for all registered elements.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
type ResizeCallback = () => void
|
|
10
|
+
|
|
11
|
+
const callbacks = new Map<Element, ResizeCallback>()
|
|
12
|
+
|
|
13
|
+
let observer: ResizeObserver | null = null
|
|
14
|
+
|
|
15
|
+
function getObserver(): ResizeObserver {
|
|
16
|
+
observer ??= new ResizeObserver((entries) => {
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
const callback = callbacks.get(entry.target)
|
|
19
|
+
callback?.()
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
return observer
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function observeResize(
|
|
26
|
+
element: Element,
|
|
27
|
+
callback: ResizeCallback,
|
|
28
|
+
): () => void {
|
|
29
|
+
callbacks.set(element, callback)
|
|
30
|
+
getObserver().observe(element)
|
|
31
|
+
|
|
32
|
+
return () => {
|
|
33
|
+
callbacks.delete(element)
|
|
34
|
+
getObserver().unobserve(element)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Reset the shared observer (for testing only). */
|
|
39
|
+
export function resetSharedResizeObserver(): void {
|
|
40
|
+
if (observer) {
|
|
41
|
+
observer.disconnect()
|
|
42
|
+
observer = null
|
|
43
|
+
}
|
|
44
|
+
callbacks.clear()
|
|
45
|
+
}
|
|
@@ -33,6 +33,7 @@ export interface EchartWidgetOptionProps<D> {
|
|
|
33
33
|
data?: D
|
|
34
34
|
theme: typeof CartoTheme
|
|
35
35
|
formatter?: (value: number) => string
|
|
36
|
+
labelFormatter?: (value: string | number) => string | number
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export interface EchartWidgetProps {
|
|
@@ -40,4 +41,5 @@ export interface EchartWidgetProps {
|
|
|
40
41
|
option: EchartUIProps['option']
|
|
41
42
|
onEvents?: EchartUIProps['onEvents']
|
|
42
43
|
formatter?: (value: number) => string
|
|
44
|
+
labelFormatter?: (value: string | number) => string | number
|
|
43
45
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Alert, AlertTitle } from '@mui/material'
|
|
2
|
-
import {
|
|
3
|
-
import { useShallow } from 'zustand/shallow'
|
|
2
|
+
import { useWidgetSelector } from '../stores/use-widget-selector'
|
|
4
3
|
import type { WidgetErrorProps } from './types'
|
|
5
4
|
|
|
6
5
|
/**
|
|
@@ -19,13 +18,12 @@ export function WidgetError({
|
|
|
19
18
|
title: titleProp,
|
|
20
19
|
description,
|
|
21
20
|
}: WidgetErrorProps) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
)
|
|
28
|
-
const error = useWidgetStore(useShallow((state) => state.widgets[id]?.error))
|
|
21
|
+
// Single consolidated subscription instead of 3 separate ones.
|
|
22
|
+
const { isLoading, isFetching, error } = useWidgetSelector(id, (w) => ({
|
|
23
|
+
isLoading: w?.isLoading,
|
|
24
|
+
isFetching: w?.isFetching,
|
|
25
|
+
error: w?.error,
|
|
26
|
+
}))
|
|
29
27
|
|
|
30
28
|
// Don't show error during loading/fetching states
|
|
31
29
|
if (isLoading || isFetching) {
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { type FormulaWidgetState, type ValueProps } from '../types'
|
|
2
|
-
import { useWidgetStore } from '../../stores/widget-store'
|
|
3
2
|
import { Item } from './item'
|
|
4
|
-
import {
|
|
3
|
+
import { useWidgetSelector } from '../../stores/use-widget-selector'
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Renders the prefix content (e.g., currency symbol) before a formula value, if defined in the data item.
|
|
8
7
|
*/
|
|
9
8
|
export function Prefix({ id, index = 0, ...props }: ValueProps) {
|
|
10
|
-
const prefix =
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
),
|
|
9
|
+
const prefix = useWidgetSelector(
|
|
10
|
+
id,
|
|
11
|
+
(w) => (w as FormulaWidgetState | undefined)?.data?.[index]?.prefix,
|
|
14
12
|
)
|
|
15
13
|
|
|
16
14
|
if (!prefix) {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { Box } from '@mui/material'
|
|
2
2
|
import { styles } from '../style'
|
|
3
3
|
import type { FormulaWidgetState, RowProps } from '../types'
|
|
4
|
-
import {
|
|
5
|
-
import { useShallow } from 'zustand/shallow'
|
|
4
|
+
import { useWidgetSelector } from '../../stores/use-widget-selector'
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Iterates over the widget's data items and renders a row for each one using render props.
|
|
9
8
|
*/
|
|
10
9
|
export function Row(props: RowProps) {
|
|
11
|
-
const data =
|
|
12
|
-
|
|
10
|
+
const data = useWidgetSelector(
|
|
11
|
+
props.id,
|
|
12
|
+
(w) => (w as FormulaWidgetState | undefined)?.data,
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
return data?.map((_, index) => {
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { Avatar } from '@mui/material'
|
|
2
2
|
import type { FormulaWidgetState, ValueProps } from '../types'
|
|
3
|
-
import {
|
|
4
|
-
import { useShallow } from 'zustand/shallow'
|
|
3
|
+
import { useWidgetSelector } from '../../stores/use-widget-selector'
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Renders a colored avatar badge showing the first letter of the series name for multi-value formula widgets.
|
|
8
7
|
*/
|
|
9
8
|
export function Series({ id, index = 0 }: Pick<ValueProps, 'id' | 'index'>) {
|
|
10
|
-
const serie =
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
),
|
|
9
|
+
const serie = useWidgetSelector(
|
|
10
|
+
id,
|
|
11
|
+
(w) => (w as FormulaWidgetState | undefined)?.series?.[index],
|
|
14
12
|
)
|
|
15
13
|
if (!serie) {
|
|
16
14
|
return null
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { type FormulaWidgetState, type ValueProps } from '../types'
|
|
2
|
-
import { useWidgetStore } from '../../stores/widget-store'
|
|
3
2
|
import { Item } from './item'
|
|
4
3
|
import type { Theme } from '@mui/material'
|
|
5
|
-
import {
|
|
4
|
+
import { useWidgetSelector } from '../../stores/use-widget-selector'
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Renders the suffix content (e.g., unit label) after a formula value, if defined in the data item.
|
|
9
8
|
*/
|
|
10
9
|
export function Suffix({ id, index = 0, ...props }: ValueProps) {
|
|
11
|
-
const suffix =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
),
|
|
10
|
+
const suffix = useWidgetSelector(
|
|
11
|
+
id,
|
|
12
|
+
(w) => (w as FormulaWidgetState | undefined)?.data?.[index]?.suffix,
|
|
15
13
|
)
|
|
16
14
|
|
|
17
15
|
if (!suffix) {
|
|
@@ -1,27 +1,20 @@
|
|
|
1
1
|
import { type FormulaWidgetState, type ValueProps } from '../types'
|
|
2
|
-
import { useWidgetStore } from '../../stores/widget-store'
|
|
3
2
|
import { Item } from './item'
|
|
4
|
-
import { useShallow } from 'zustand/shallow'
|
|
5
3
|
import { defaultFormatter } from '../../utils/formatter'
|
|
4
|
+
import { useWidgetSelector } from '../../stores/use-widget-selector'
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Displays the formatted numeric value for a formula widget data item, applying the widget's formatter and color.
|
|
9
8
|
*/
|
|
10
9
|
export function Value({ id, index = 0, ...props }: ValueProps) {
|
|
11
|
-
const value =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
),
|
|
20
|
-
)
|
|
21
|
-
const formatter =
|
|
22
|
-
useWidgetStore(
|
|
23
|
-
useShallow((state) => state.getWidget<FormulaWidgetState>(id)?.formatter),
|
|
24
|
-
) ?? defaultFormatter
|
|
10
|
+
const { value, color, formatter } = useWidgetSelector(id, (w) => {
|
|
11
|
+
const widget = w as FormulaWidgetState | undefined
|
|
12
|
+
return {
|
|
13
|
+
value: widget?.data?.[index]?.value,
|
|
14
|
+
color: widget?.data?.[index]?.color,
|
|
15
|
+
formatter: widget?.formatter ?? defaultFormatter,
|
|
16
|
+
}
|
|
17
|
+
})
|
|
25
18
|
|
|
26
19
|
return (
|
|
27
20
|
<Item TypographyProps={{ color }} {...props}>
|
|
@@ -3,27 +3,104 @@ import {
|
|
|
3
3
|
mergeEchartWidgetConfig,
|
|
4
4
|
type EchartOptionsProps,
|
|
5
5
|
} from '../echart'
|
|
6
|
-
import type {
|
|
7
|
-
HistogramConfig,
|
|
8
|
-
HistogramWidgetConfig,
|
|
9
|
-
HistogramWidgetData,
|
|
10
|
-
} from './types'
|
|
6
|
+
import type { HistogramConfig, HistogramWidgetConfig } from './types'
|
|
11
7
|
import {
|
|
12
|
-
flattenObjectArrayToCSV,
|
|
13
8
|
buildLegendConfig,
|
|
14
9
|
buildGridConfig,
|
|
15
10
|
createTooltipPositioner,
|
|
16
11
|
createTooltipFormatter,
|
|
17
|
-
createChartDownloadConfig,
|
|
18
12
|
niceNum,
|
|
19
13
|
} from '../utils/chart-config'
|
|
14
|
+
import { downloadToCSV, downloadToPNG, type DownloadItem } from '../actions'
|
|
15
|
+
import type { ConfigProps } from '../loader/types'
|
|
16
|
+
|
|
17
|
+
export interface HistogramDownloadConfigProps extends ConfigProps {
|
|
18
|
+
ticks: number[]
|
|
19
|
+
labelFormatter?: (value: string | number) => string | number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function histogramDataToCSV(
|
|
23
|
+
data: number[][],
|
|
24
|
+
ticks: number[],
|
|
25
|
+
labelFormatter?: (value: string | number) => string | number,
|
|
26
|
+
): string[][] {
|
|
27
|
+
if (!data?.length || data[0]?.length === 0) return []
|
|
28
|
+
|
|
29
|
+
const dataLength = data[0]?.length ?? 0
|
|
30
|
+
const labels = createAxisLabels(dataLength, ticks, labelFormatter)
|
|
31
|
+
const seriesCount = data.length
|
|
32
|
+
const isMulti = seriesCount > 1
|
|
33
|
+
|
|
34
|
+
const headers = isMulti
|
|
35
|
+
? [
|
|
36
|
+
'Bin',
|
|
37
|
+
...Array.from({ length: seriesCount }, (_, i) => `Series ${i + 1}`),
|
|
38
|
+
]
|
|
39
|
+
: ['Bin', 'Value']
|
|
40
|
+
|
|
41
|
+
return [
|
|
42
|
+
headers,
|
|
43
|
+
...labels.map((label, i) => [
|
|
44
|
+
label,
|
|
45
|
+
...data.map((series) => String(series[i] ?? 0)),
|
|
46
|
+
]),
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function histogramDownloadConfig({
|
|
51
|
+
refUI,
|
|
52
|
+
ticks,
|
|
53
|
+
labelFormatter,
|
|
54
|
+
}: HistogramDownloadConfigProps): DownloadItem<number[][]>[] {
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
...downloadToPNG,
|
|
58
|
+
modifier: () => downloadToPNG.modifier(refUI),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
...downloadToCSV,
|
|
62
|
+
modifier: async (data) =>
|
|
63
|
+
downloadToCSV.modifier(histogramDataToCSV(data, ticks, labelFormatter)),
|
|
64
|
+
},
|
|
65
|
+
]
|
|
66
|
+
}
|
|
20
67
|
|
|
21
|
-
export const histogramDownloadConfig =
|
|
22
|
-
createChartDownloadConfig<HistogramWidgetData>(flattenObjectArrayToCSV)
|
|
23
68
|
/**
|
|
24
|
-
*
|
|
69
|
+
* Creates formatted axis labels from tick boundaries.
|
|
25
70
|
*
|
|
26
|
-
* @param
|
|
71
|
+
* @param dataLength - Number of data points (determines number of labels).
|
|
72
|
+
* @param ticks - Bin boundaries. If `ticks.length === dataLength + 1`, all
|
|
73
|
+
* bins are ranges. If `ticks.length === dataLength`, the last bin is
|
|
74
|
+
* open-ended (`+`). A last tick of `Infinity` also produces `+`.
|
|
75
|
+
* @param labelFormatter - Optional formatter applied to each individual tick
|
|
76
|
+
* value when building the bin range label.
|
|
77
|
+
*/
|
|
78
|
+
function createAxisLabels(
|
|
79
|
+
dataLength: number,
|
|
80
|
+
ticks: number[],
|
|
81
|
+
labelFormatter?: (value: string | number) => string | number,
|
|
82
|
+
): string[] {
|
|
83
|
+
const fmt = (v: number) =>
|
|
84
|
+
labelFormatter ? String(labelFormatter(v)) : String(v)
|
|
85
|
+
|
|
86
|
+
return Array.from({ length: dataLength }, (_, i) => {
|
|
87
|
+
const low = ticks[i] ?? i
|
|
88
|
+
const high = ticks[i + 1]
|
|
89
|
+
return high !== undefined && isFinite(high)
|
|
90
|
+
? `${fmt(low)}-${fmt(high)}`
|
|
91
|
+
: `${fmt(low)}+`
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Generates ECharts configuration for distribution histogram widgets with
|
|
97
|
+
* adjacent bars (minimal gap) and axis formatting styled with the CARTO theme.
|
|
98
|
+
*
|
|
99
|
+
* Accepts raw `number[][]` data and `ticks` boundaries. The ticks and
|
|
100
|
+
* `labelFormatter` are used to create the x-axis category labels; the raw
|
|
101
|
+
* numeric data is embedded directly in each series.
|
|
102
|
+
*
|
|
103
|
+
* @param props - Histogram configuration including raw data, ticks, and theme.
|
|
27
104
|
* @returns Widget config with ECharts option object.
|
|
28
105
|
*/
|
|
29
106
|
export function histogramConfig(props: HistogramConfig): HistogramWidgetConfig {
|
|
@@ -33,21 +110,27 @@ export function histogramConfig(props: HistogramConfig): HistogramWidgetConfig {
|
|
|
33
110
|
formatter: props.formatter,
|
|
34
111
|
}
|
|
35
112
|
}
|
|
113
|
+
|
|
36
114
|
function getOption({
|
|
37
115
|
data = [],
|
|
116
|
+
ticks,
|
|
38
117
|
theme,
|
|
39
118
|
formatter,
|
|
119
|
+
labelFormatter,
|
|
40
120
|
}: HistogramConfig): EchartOptionsProps {
|
|
41
121
|
const hasLegend = (data?.length ?? 0) > 1
|
|
122
|
+
const dataLength = data[0]?.length ?? 0
|
|
123
|
+
const axisLabels = createAxisLabels(dataLength, ticks, labelFormatter)
|
|
42
124
|
|
|
43
125
|
let niceMin = 0
|
|
44
126
|
let niceMax = 1
|
|
45
127
|
|
|
46
128
|
return {
|
|
47
|
-
legend: buildLegendConfig(hasLegend),
|
|
129
|
+
legend: buildLegendConfig({ hasLegend }),
|
|
48
130
|
grid: buildGridConfig(hasLegend, theme),
|
|
49
131
|
xAxis: {
|
|
50
132
|
type: 'category',
|
|
133
|
+
data: axisLabels,
|
|
51
134
|
axisLine: {
|
|
52
135
|
show: false,
|
|
53
136
|
},
|
|
@@ -117,14 +200,12 @@ function getOption({
|
|
|
117
200
|
tooltip: {
|
|
118
201
|
position: createTooltipPositioner(theme),
|
|
119
202
|
formatter: createTooltipFormatter((item) => {
|
|
120
|
-
const
|
|
121
|
-
const index = item.dimensionNames?.[item.encode?.y?.at(0) ?? 1]
|
|
122
|
-
const _value = value[index ?? '']
|
|
203
|
+
const _value = item.value as number
|
|
123
204
|
|
|
124
205
|
const formattedValue =
|
|
125
206
|
typeof _value === 'number' && formatter
|
|
126
207
|
? formatter(_value)
|
|
127
|
-
: (_value ?? '')
|
|
208
|
+
: String(_value ?? '')
|
|
128
209
|
|
|
129
210
|
const marker = typeof item.marker === 'string' ? item.marker : ''
|
|
130
211
|
const seriesName = item.seriesName ? `${item.seriesName}: ` : ''
|
|
@@ -133,11 +214,11 @@ function getOption({
|
|
|
133
214
|
return { name, seriesName, marker, value: formattedValue }
|
|
134
215
|
}),
|
|
135
216
|
},
|
|
136
|
-
series: data.map((
|
|
137
|
-
datasetIndex: index,
|
|
217
|
+
series: data.map((seriesData: number[]) => ({
|
|
138
218
|
type: 'bar',
|
|
139
|
-
|
|
140
|
-
|
|
219
|
+
data: seriesData,
|
|
220
|
+
barGap: '1%',
|
|
221
|
+
barCategoryGap: '1%',
|
|
141
222
|
emphasis: {
|
|
142
223
|
focus: 'series',
|
|
143
224
|
},
|
|
@@ -4,5 +4,10 @@ export type {
|
|
|
4
4
|
HistogramWidgetData,
|
|
5
5
|
HistogramWidgetState,
|
|
6
6
|
} from './types'
|
|
7
|
-
export {
|
|
7
|
+
export type { HistogramDownloadConfigProps } from './config'
|
|
8
|
+
export {
|
|
9
|
+
histogramConfig,
|
|
10
|
+
histogramDataToCSV,
|
|
11
|
+
histogramDownloadConfig,
|
|
12
|
+
} from './config'
|
|
8
13
|
export { HistogramSkeleton } from './skeleton'
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { EchartWidgetState } from '../echart'
|
|
2
2
|
import type {
|
|
3
3
|
EchartWidgetOptionProps,
|
|
4
4
|
EchartWidgetProps,
|
|
5
5
|
} from '../echart/types'
|
|
6
6
|
import type { ConfigProps } from '../loader'
|
|
7
7
|
|
|
8
|
-
export type HistogramWidgetData =
|
|
8
|
+
export type HistogramWidgetData = number[][]
|
|
9
9
|
|
|
10
10
|
export type HistogramWidgetState = EchartWidgetState
|
|
11
11
|
|
|
@@ -14,4 +14,10 @@ export type HistogramWidgetConfig = EchartWidgetProps & {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export type HistogramConfig = ConfigProps &
|
|
17
|
-
|
|
17
|
+
Pick<
|
|
18
|
+
EchartWidgetOptionProps<unknown>,
|
|
19
|
+
'theme' | 'formatter' | 'labelFormatter'
|
|
20
|
+
> & {
|
|
21
|
+
data?: number[][]
|
|
22
|
+
ticks: number[]
|
|
23
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useEffect, useRef
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
2
|
import type { WidgetLoaderProps } from './types'
|
|
3
|
-
import { useWidgetStore } from '../stores/widget-store'
|
|
3
|
+
import { useWidgetStore, widgetStoreActions } from '../stores/widget-store'
|
|
4
4
|
import type { WrapperState } from '../wrapper'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -28,17 +28,11 @@ import type { WrapperState } from '../wrapper'
|
|
|
28
28
|
export function WidgetLoader<T extends object = Record<string, unknown>>(
|
|
29
29
|
props: WidgetLoaderProps<T>,
|
|
30
30
|
) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
(state) => state.executeConfigPipeline,
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
const registeredTools = useSyncExternalStore(
|
|
40
|
-
useWidgetStore.subscribe,
|
|
41
|
-
() => useWidgetStore.getState().widgets[props.id]?.registeredTools,
|
|
31
|
+
// Subscribe only to this widget's registeredTools via Zustand selector.
|
|
32
|
+
// The selector returns a stable reference when other widgets change,
|
|
33
|
+
// avoiding unnecessary re-evaluations of Effect 4.
|
|
34
|
+
const registeredTools = useWidgetStore(
|
|
35
|
+
(state) => state.widgets[props.id]?.registeredTools,
|
|
42
36
|
)
|
|
43
37
|
|
|
44
38
|
const dataRef = useRef(props.data)
|
|
@@ -50,56 +44,49 @@ export function WidgetLoader<T extends object = Record<string, unknown>>(
|
|
|
50
44
|
configRef.current = props.config
|
|
51
45
|
})
|
|
52
46
|
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// accidentally resetting other properties.
|
|
56
|
-
//
|
|
57
|
-
// - Effect 1: Type (can be modified by tools that change visualization type)
|
|
58
|
-
// - Effect 2: Loading/Error states (change during fetch lifecycle)
|
|
59
|
-
// - Effect 3: Config (can be modified by tools that change widget configuration)
|
|
60
|
-
// - Effect 4: Data pipeline execution (transforms data through registered tools)
|
|
61
|
-
// - Effect 5: Re-execute pipeline when tool state changes
|
|
62
|
-
|
|
63
|
-
// Effect 1: Type updates
|
|
47
|
+
// Effect 1: Metadata — type, loading, and error states in a single setWidget call.
|
|
48
|
+
// Merged to reduce store updates from 2 to 1 per widget during initialization.
|
|
64
49
|
useEffect(() => {
|
|
65
|
-
setWidget<WrapperState>(props.id, {
|
|
50
|
+
widgetStoreActions.setWidget<WrapperState>(props.id, {
|
|
66
51
|
type: props.type,
|
|
67
|
-
})
|
|
68
|
-
}, [props.id, props.type, setWidget])
|
|
69
|
-
|
|
70
|
-
// Effect 2: Loading and error states
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
setWidget<WrapperState>(props.id, {
|
|
73
52
|
isLoading: props.isLoading ?? false,
|
|
74
53
|
isFetching: props.isFetching ?? false,
|
|
75
54
|
error: props.error,
|
|
76
55
|
})
|
|
77
|
-
}, [props.id, props.isLoading, props.isFetching, props.error
|
|
56
|
+
}, [props.id, props.type, props.isLoading, props.isFetching, props.error])
|
|
78
57
|
|
|
79
|
-
// Effect
|
|
58
|
+
// Effect 2: Config updates — run through config pipeline
|
|
80
59
|
useEffect(() => {
|
|
81
60
|
if (props.config) {
|
|
82
|
-
void executeConfigPipeline(props.id, props.config)
|
|
61
|
+
void widgetStoreActions.executeConfigPipeline(props.id, props.config)
|
|
83
62
|
}
|
|
84
|
-
}, [props.id, props.config
|
|
63
|
+
}, [props.id, props.config])
|
|
85
64
|
|
|
86
|
-
// Effect
|
|
65
|
+
// Effect 3: Execute tool pipeline when props.data changes
|
|
87
66
|
useEffect(() => {
|
|
88
|
-
void executeToolPipeline(props.id, props.data)
|
|
89
|
-
}, [props.id, props.data
|
|
67
|
+
void widgetStoreActions.executeToolPipeline(props.id, props.data)
|
|
68
|
+
}, [props.id, props.data])
|
|
90
69
|
|
|
91
|
-
// Effect
|
|
70
|
+
// Effect 4: Re-execute pipelines when registered tools change.
|
|
71
|
+
// Uses requestAnimationFrame to coalesce rapid successive registeredTools
|
|
72
|
+
// changes (e.g., 6 action components each calling registerTool on mount)
|
|
73
|
+
// into a single pipeline execution instead of 6 pairs.
|
|
92
74
|
useEffect(() => {
|
|
93
75
|
if (!isMountedRef.current) {
|
|
94
76
|
isMountedRef.current = true
|
|
95
77
|
return
|
|
96
78
|
}
|
|
97
79
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
void
|
|
101
|
-
|
|
102
|
-
|
|
80
|
+
const rafId = requestAnimationFrame(() => {
|
|
81
|
+
const { executeToolPipeline, executeConfigPipeline } = widgetStoreActions
|
|
82
|
+
void executeToolPipeline(props.id, dataRef.current)
|
|
83
|
+
if (configRef.current) {
|
|
84
|
+
void executeConfigPipeline(props.id, configRef.current)
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return () => cancelAnimationFrame(rafId)
|
|
89
|
+
}, [registeredTools, props.id])
|
|
103
90
|
|
|
104
91
|
return props.children
|
|
105
92
|
}
|
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import type { MarkdownUIProps } from './types'
|
|
2
|
-
import { useWidgetStore } from '../stores/widget-store'
|
|
3
2
|
import type { MarkdownWidgetData } from './types'
|
|
4
3
|
import { MarkdownUI } from '.'
|
|
5
|
-
import {
|
|
4
|
+
import { useWidgetSelector } from '../stores/use-widget-selector'
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Stateful markdown widget component that reads content from the widget store and renders it as formatted rich text.
|
|
9
8
|
*/
|
|
10
9
|
export function Markdown({ id }: MarkdownUIProps) {
|
|
11
|
-
const content =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
(state.getWidget(id)?.data as MarkdownWidgetData | undefined)?.content,
|
|
15
|
-
),
|
|
10
|
+
const content = useWidgetSelector(
|
|
11
|
+
id,
|
|
12
|
+
(w) => (w?.data as MarkdownWidgetData | undefined)?.content,
|
|
16
13
|
)
|
|
17
14
|
|
|
18
15
|
if (!content) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Box, Typography } from '@mui/material'
|
|
2
|
-
import {
|
|
3
|
-
import { useWidgetStore } from '../stores/widget-store'
|
|
2
|
+
import { useWidgetSelector } from '../stores/use-widget-selector'
|
|
4
3
|
import type { WidgetNoDataProps } from './types'
|
|
5
4
|
import { styles } from './style'
|
|
6
5
|
|
|
@@ -44,14 +43,12 @@ export function WidgetNoData({
|
|
|
44
43
|
description = 'There are no results for the combination of filters applied to your data. Try tweaking your filters, or zoom and pan the map to adjust filters',
|
|
45
44
|
isEmpty = defaultIsEmpty,
|
|
46
45
|
}: WidgetNoDataProps) {
|
|
47
|
-
//
|
|
48
|
-
const isLoading =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
54
|
-
const data = useWidgetStore(useShallow((state) => state.widgets[id]?.data))
|
|
46
|
+
// Single consolidated subscription instead of 3 separate ones.
|
|
47
|
+
const { isLoading, isFetching, data } = useWidgetSelector(id, (w) => ({
|
|
48
|
+
isLoading: w?.isLoading,
|
|
49
|
+
isFetching: w?.isFetching,
|
|
50
|
+
data: w?.data,
|
|
51
|
+
}))
|
|
55
52
|
|
|
56
53
|
// If loading or fetching, show children
|
|
57
54
|
// SkeletonLoader handles loading state, this allows proper composition
|