@galaxy-io/dls 1.3.0 → 1.3.1
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/charts/BarChart.d.ts +3 -3
- package/dist/charts/BarChart.js +68 -104
- package/dist/charts/ChartCategoryTargets.d.ts +27 -0
- package/dist/charts/ChartCategoryTargets.js +75 -0
- package/dist/charts/ChartFrame.d.ts +34 -6
- package/dist/charts/ChartFrame.js +72 -15
- package/dist/charts/ChartPrimitives.d.ts +35 -0
- package/dist/charts/ChartPrimitives.js +75 -38
- package/dist/charts/ChartTooltip.d.ts +3 -3
- package/dist/charts/ChartTooltip.js +2 -2
- package/dist/charts/LineChart.js +47 -62
- package/dist/charts/PieChart.js +48 -26
- package/dist/charts/barChartGeometry.d.ts +66 -33
- package/dist/charts/barChartGeometry.js +7 -15
- package/dist/charts/barChartLegend.d.ts +25 -0
- package/dist/charts/barChartLegend.js +23 -0
- package/dist/charts/chartFormat.d.ts +9 -1
- package/dist/charts/chartFormat.js +13 -1
- package/dist/charts/chartScales.d.ts +58 -22
- package/dist/charts/chartScales.js +92 -38
- package/dist/charts/constants.d.ts +22 -6
- package/dist/charts/constants.js +22 -1
- package/dist/charts/lineChartGeometry.d.ts +53 -15
- package/dist/charts/lineChartGeometry.js +6 -15
- package/dist/charts/pieChartGeometry.d.ts +20 -8
- package/dist/charts/pieChartGeometry.js +3 -13
- package/dist/charts/types.d.ts +51 -145
- package/dist/charts/types.js +22 -3
- package/dist/charts/useCartesianChartLayout.d.ts +70 -0
- package/dist/charts/useCartesianChartLayout.js +56 -0
- package/dist/charts/useChartDimensions.d.ts +10 -3
- package/dist/charts/useChartDimensions.js +14 -4
- package/dist/charts/useChartInteraction.d.ts +13 -8
- package/dist/charts/useChartInteraction.js +7 -11
- package/dist/charts/useSeriesColorResolver.d.ts +37 -0
- package/dist/charts/useSeriesColorResolver.js +27 -0
- package/dist/styles.css +13 -11
- package/package.json +1 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ChartLabelFormatter, ChartMargin, ChartValueDomain, ChartValueFormatter, ChartValueUnit } from "./types";
|
|
2
|
+
import type { ChartDimensions } from "./useChartDimensions";
|
|
3
|
+
/**
|
|
4
|
+
* Everything a cartesian chart resolves *before* it can be measured.
|
|
5
|
+
*
|
|
6
|
+
* There is a hard ordering inside bar and line alike:
|
|
7
|
+
*
|
|
8
|
+
* ```
|
|
9
|
+
* domain → ticks → left margin → dimensions → plot size → geometry
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* The gutter has to fit the tick labels, the ticks come from the domain, and
|
|
13
|
+
* the domain comes from the data — none of which needs the plot. Everything
|
|
14
|
+
* after `geometry` does need it, which is why this hook stops there rather than
|
|
15
|
+
* trying to own the whole chart: the geometry builders are generic over the
|
|
16
|
+
* chart's own key types, and threading those through here would buy nothing.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately **not generic**. `getBarValueDomain` and `getLineValueDomain`
|
|
19
|
+
* have already erased the key types by the time they return an extent, so
|
|
20
|
+
* nothing in this phase needs them.
|
|
21
|
+
*/
|
|
22
|
+
interface UseCartesianChartLayoutOptions {
|
|
23
|
+
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
24
|
+
/**
|
|
25
|
+
* The data's own extent, before any override or nicing.
|
|
26
|
+
*
|
|
27
|
+
* Free to be a fresh array each render — the memo below keys on the two
|
|
28
|
+
* numbers, not the array's identity.
|
|
29
|
+
*/
|
|
30
|
+
rawDomain: [number, number];
|
|
31
|
+
valueDomain?: Partial<ChartValueDomain>;
|
|
32
|
+
width?: number;
|
|
33
|
+
height?: number;
|
|
34
|
+
fillWidth?: boolean;
|
|
35
|
+
fillHeight?: boolean;
|
|
36
|
+
aspectRatio?: number;
|
|
37
|
+
margin?: Partial<ChartMargin>;
|
|
38
|
+
valueUnit?: ChartValueUnit;
|
|
39
|
+
valueFormatter?: ChartValueFormatter;
|
|
40
|
+
labelFormatter?: ChartLabelFormatter;
|
|
41
|
+
/**
|
|
42
|
+
* Rewrites the resolved value formatter for the axis alone.
|
|
43
|
+
*
|
|
44
|
+
* Takes the resolved formatter rather than replacing it, because the only
|
|
45
|
+
* caller — a percent-normalized bar chart — falls back to it whenever the
|
|
46
|
+
* consumer asked for something specific. Applies to the tick labels *and* the
|
|
47
|
+
* gutter estimate, which is the point: sizing the gutter with a different
|
|
48
|
+
* formatter than the one that renders into it is how a gutter ends up too
|
|
49
|
+
* narrow for its own labels.
|
|
50
|
+
*
|
|
51
|
+
* Must be stable; memoize it at the call site.
|
|
52
|
+
*/
|
|
53
|
+
deriveAxisFormatter?: (formatValue: ChartValueFormatter) => ChartValueFormatter;
|
|
54
|
+
valueAxisTitle?: string;
|
|
55
|
+
categoryAxisTitle?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface CartesianChartLayout {
|
|
58
|
+
dimensions: ChartDimensions;
|
|
59
|
+
/** Override applied, zero included, degenerate widened, `nice()` applied. */
|
|
60
|
+
domain: [number, number];
|
|
61
|
+
valueTicks: number[];
|
|
62
|
+
/** Tooltip rows and value labels. */
|
|
63
|
+
formatValue: ChartValueFormatter;
|
|
64
|
+
/** Category names — the axis and the tooltip heading. */
|
|
65
|
+
formatLabel: ChartLabelFormatter;
|
|
66
|
+
/** Axis ticks. Identical to `formatValue` unless `deriveAxisFormatter` said otherwise. */
|
|
67
|
+
formatAxisValue: ChartValueFormatter;
|
|
68
|
+
}
|
|
69
|
+
export declare function useCartesianChartLayout({ containerRef, rawDomain, valueDomain, width, height, fillWidth, fillHeight, aspectRatio, margin, valueUnit, valueFormatter, labelFormatter, deriveAxisFormatter, valueAxisTitle, categoryAxisTitle, }: UseCartesianChartLayoutOptions): CartesianChartLayout;
|
|
70
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { addAxisTitleSpace, estimateValueAxisMargin, getValueTicks, resolveValueDomain } from "./chartScales.js";
|
|
2
|
+
import { useChartFormatters } from "./useChartFormatters.js";
|
|
3
|
+
import { useChartDimensions } from "./useChartDimensions.js";
|
|
4
|
+
import { useMemo } from "react";
|
|
5
|
+
//#region src/charts/useCartesianChartLayout.ts
|
|
6
|
+
function useCartesianChartLayout({ containerRef, rawDomain, valueDomain, width, height, fillWidth, fillHeight, aspectRatio, margin, valueUnit, valueFormatter, labelFormatter, deriveAxisFormatter, valueAxisTitle, categoryAxisTitle }) {
|
|
7
|
+
const { formatValue, formatLabel } = useChartFormatters({
|
|
8
|
+
valueFormatter,
|
|
9
|
+
valueUnit,
|
|
10
|
+
labelFormatter
|
|
11
|
+
});
|
|
12
|
+
const formatAxisValue = useMemo(() => deriveAxisFormatter?.(formatValue) ?? formatValue, [deriveAxisFormatter, formatValue]);
|
|
13
|
+
const [rawMin, rawMax] = rawDomain;
|
|
14
|
+
const domain = useMemo(() => resolveValueDomain([rawMin, rawMax], valueDomain), [
|
|
15
|
+
rawMin,
|
|
16
|
+
rawMax,
|
|
17
|
+
valueDomain
|
|
18
|
+
]);
|
|
19
|
+
const valueTicks = useMemo(() => getValueTicks(domain, valueDomain), [domain, valueDomain]);
|
|
20
|
+
const dimensions = useChartDimensions({
|
|
21
|
+
containerRef,
|
|
22
|
+
width,
|
|
23
|
+
height,
|
|
24
|
+
fillWidth,
|
|
25
|
+
fillHeight,
|
|
26
|
+
aspectRatio,
|
|
27
|
+
margin: addAxisTitleSpace(useMemo(() => ({
|
|
28
|
+
...margin,
|
|
29
|
+
left: margin?.left ?? estimateValueAxisMargin(valueTicks, formatAxisValue)
|
|
30
|
+
}), [
|
|
31
|
+
margin,
|
|
32
|
+
valueTicks,
|
|
33
|
+
formatAxisValue
|
|
34
|
+
]), {
|
|
35
|
+
hasValueTitle: Boolean(valueAxisTitle),
|
|
36
|
+
hasCategoryTitle: Boolean(categoryAxisTitle)
|
|
37
|
+
})
|
|
38
|
+
});
|
|
39
|
+
return useMemo(() => ({
|
|
40
|
+
dimensions,
|
|
41
|
+
domain,
|
|
42
|
+
valueTicks,
|
|
43
|
+
formatValue,
|
|
44
|
+
formatLabel,
|
|
45
|
+
formatAxisValue
|
|
46
|
+
}), [
|
|
47
|
+
dimensions,
|
|
48
|
+
domain,
|
|
49
|
+
valueTicks,
|
|
50
|
+
formatValue,
|
|
51
|
+
formatLabel,
|
|
52
|
+
formatAxisValue
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { useCartesianChartLayout };
|
|
@@ -21,6 +21,15 @@ interface UseChartDimensionsOptions {
|
|
|
21
21
|
fillHeight?: boolean;
|
|
22
22
|
aspectRatio?: number;
|
|
23
23
|
margin?: Partial<ChartMargin>;
|
|
24
|
+
/**
|
|
25
|
+
* Fallback for whichever fields `margin` leaves out.
|
|
26
|
+
*
|
|
27
|
+
* Radial charts pass their own, since the cartesian default reserves gutters
|
|
28
|
+
* for axes they do not draw. Merged per field, so a caller's partial margin
|
|
29
|
+
* lands on top of *this* rather than reverting the chart to the cartesian
|
|
30
|
+
* defaults.
|
|
31
|
+
*/
|
|
32
|
+
defaultMargin?: ChartMargin;
|
|
24
33
|
}
|
|
25
34
|
export interface ChartDimensions {
|
|
26
35
|
width: number;
|
|
@@ -33,8 +42,6 @@ export interface ChartDimensions {
|
|
|
33
42
|
/** Handed to the plot box so it reserves space before measurement lands. */
|
|
34
43
|
reservedHeight?: number;
|
|
35
44
|
reservedAspectRatio?: number;
|
|
36
|
-
/** Makes the plot box claim the flex track instead of reserving a height. */
|
|
37
|
-
fillHeight?: boolean;
|
|
38
45
|
}
|
|
39
|
-
export declare function useChartDimensions({ containerRef, width, height, fillWidth, fillHeight, aspectRatio, margin: marginOverride, }: UseChartDimensionsOptions): ChartDimensions;
|
|
46
|
+
export declare function useChartDimensions({ containerRef, width, height, fillWidth, fillHeight, aspectRatio, margin: marginOverride, defaultMargin, }: UseChartDimensionsOptions): ChartDimensions;
|
|
40
47
|
export {};
|
|
@@ -1,22 +1,33 @@
|
|
|
1
1
|
import { useElementClientSize } from "../hooks/useElementClientSize.js";
|
|
2
|
+
import { DEFAULT_CHART_MARGIN } from "./constants.js";
|
|
2
3
|
import { getPlotDimensions, resolveMargin } from "./chartScales.js";
|
|
3
4
|
import { useEffect, useMemo } from "react";
|
|
4
5
|
//#region src/charts/useChartDimensions.ts
|
|
5
|
-
function useChartDimensions({ containerRef, width, height, fillWidth = false, fillHeight = false, aspectRatio, margin: marginOverride }) {
|
|
6
|
+
function useChartDimensions({ containerRef, width, height, fillWidth = false, fillHeight = false, aspectRatio, margin: marginOverride, defaultMargin }) {
|
|
6
7
|
const { width: measuredWidth, height: measuredHeight } = useElementClientSize(containerRef);
|
|
7
8
|
const resolvedWidth = fillWidth ? measuredWidth ?? 0 : width ?? 0;
|
|
8
9
|
const resolvedHeight = fillHeight ? measuredHeight ?? 0 : aspectRatio ? resolvedWidth / aspectRatio : height ?? 0;
|
|
9
10
|
const { top: marginTop, right: marginRight, bottom: marginBottom, left: marginLeft } = marginOverride ?? {};
|
|
11
|
+
const { top: defaultTop, right: defaultRight, bottom: defaultBottom, left: defaultLeft } = defaultMargin ?? DEFAULT_CHART_MARGIN;
|
|
10
12
|
const margin = useMemo(() => resolveMargin({
|
|
11
13
|
top: marginTop,
|
|
12
14
|
right: marginRight,
|
|
13
15
|
bottom: marginBottom,
|
|
14
16
|
left: marginLeft
|
|
17
|
+
}, {
|
|
18
|
+
top: defaultTop,
|
|
19
|
+
right: defaultRight,
|
|
20
|
+
bottom: defaultBottom,
|
|
21
|
+
left: defaultLeft
|
|
15
22
|
}), [
|
|
16
23
|
marginTop,
|
|
17
24
|
marginRight,
|
|
18
25
|
marginBottom,
|
|
19
|
-
marginLeft
|
|
26
|
+
marginLeft,
|
|
27
|
+
defaultTop,
|
|
28
|
+
defaultRight,
|
|
29
|
+
defaultBottom,
|
|
30
|
+
defaultLeft
|
|
20
31
|
]);
|
|
21
32
|
const { plotWidth, plotHeight } = useMemo(() => getPlotDimensions(resolvedWidth, resolvedHeight, margin), [
|
|
22
33
|
resolvedWidth,
|
|
@@ -36,8 +47,7 @@ function useChartDimensions({ containerRef, width, height, fillWidth = false, fi
|
|
|
36
47
|
margin,
|
|
37
48
|
isMeasured: resolvedWidth > 0 && resolvedHeight > 0,
|
|
38
49
|
reservedHeight: fillHeight || aspectRatio ? void 0 : height,
|
|
39
|
-
reservedAspectRatio: fillHeight ? void 0 : aspectRatio
|
|
40
|
-
fillHeight
|
|
50
|
+
reservedAspectRatio: fillHeight ? void 0 : aspectRatio
|
|
41
51
|
}), [
|
|
42
52
|
resolvedWidth,
|
|
43
53
|
resolvedHeight,
|
|
@@ -11,8 +11,8 @@ import { ChartMarkState } from "./types";
|
|
|
11
11
|
*
|
|
12
12
|
* Both live here rather than one of them in the charts, so there is a single
|
|
13
13
|
* `isEngaged` boolean. That matters more than it looks: `ChartPlotGroup` uses it
|
|
14
|
-
* to cancel
|
|
15
|
-
* would sit through that delay before muting anything and feel broken.
|
|
14
|
+
* to cancel the mark restore delay, and a second hover source that didn't feed
|
|
15
|
+
* it would sit through that delay before muting anything and feel broken.
|
|
16
16
|
*
|
|
17
17
|
* Escape or a click on the plot background clears both pins.
|
|
18
18
|
*/
|
|
@@ -23,10 +23,18 @@ interface UseChartInteractionOptions {
|
|
|
23
23
|
seriesKeys?: string[];
|
|
24
24
|
/** Suppress all interaction — used while loading and when the tooltip is off. */
|
|
25
25
|
disabled?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Render every mark muted, whatever the two axes say.
|
|
28
|
+
*
|
|
29
|
+
* Distinct from {@link disabled}, which suppresses *input*. Loading suppresses
|
|
30
|
+
* *emphasis*: there is nothing yet worth drawing attention to. Folded in here
|
|
31
|
+
* rather than left to each mark, because the alternative is the same
|
|
32
|
+
* `isLoading ? MUTED : getMarkState(…)` ternary hand-copied at every mark —
|
|
33
|
+
* which is exactly how one of them came to be missing it.
|
|
34
|
+
*/
|
|
35
|
+
isLoading?: boolean;
|
|
26
36
|
/** Notified when the pinned category changes. */
|
|
27
37
|
onPinnedChange?: (index: number | null) => void;
|
|
28
|
-
/** Notified when the pinned series changes. */
|
|
29
|
-
onPinnedSeriesChange?: (seriesKey: string | null) => void;
|
|
30
38
|
}
|
|
31
39
|
interface MarkStateArgs {
|
|
32
40
|
/** Omit for marks that belong to no particular category, such as a line. */
|
|
@@ -35,14 +43,11 @@ interface MarkStateArgs {
|
|
|
35
43
|
seriesKey?: string;
|
|
36
44
|
}
|
|
37
45
|
export interface ChartInteraction {
|
|
38
|
-
hoveredIndex: number | null;
|
|
39
46
|
pinnedIndex: number | null;
|
|
40
47
|
/** Pinned wins over hovered — this is what the tooltip follows. */
|
|
41
48
|
activeIndex: number | null;
|
|
42
49
|
hoveredSeriesKey: string | null;
|
|
43
50
|
pinnedSeriesKey: string | null;
|
|
44
|
-
/** Pinned wins over hovered, matching the category axis. */
|
|
45
|
-
activeSeriesKey: string | null;
|
|
46
51
|
/** True while *either* axis is engaged; cancels the mark restore delay. */
|
|
47
52
|
isEngaged: boolean;
|
|
48
53
|
/**
|
|
@@ -67,5 +72,5 @@ export interface ChartInteraction {
|
|
|
67
72
|
/** Attach to the plot backdrop; clears both pins. */
|
|
68
73
|
onBackdropClick: () => void;
|
|
69
74
|
}
|
|
70
|
-
export declare function useChartInteraction({ categoryCount, seriesKeys, disabled,
|
|
75
|
+
export declare function useChartInteraction({ categoryCount, seriesKeys, disabled, isLoading, onPinnedChange, }: UseChartInteractionOptions): ChartInteraction;
|
|
71
76
|
export {};
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import { ChartMarkState } from "./types.js";
|
|
2
|
+
import { match } from "ts-pattern";
|
|
2
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
4
|
//#region src/charts/useChartInteraction.ts
|
|
4
5
|
/** How far back a state sits, so two axes can be compared and the worse taken. */
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
function useChartInteraction({ categoryCount, seriesKeys, disabled = false, onPinnedChange, onPinnedSeriesChange }) {
|
|
6
|
+
function severity(state) {
|
|
7
|
+
return match(state).with(ChartMarkState.ACTIVE, () => 0).with(ChartMarkState.MUTED, () => 1).with(ChartMarkState.FADED, () => 2).exhaustive();
|
|
8
|
+
}
|
|
9
|
+
function useChartInteraction({ categoryCount, seriesKeys, disabled = false, isLoading = false, onPinnedChange }) {
|
|
10
10
|
const [hoveredIndex, setHoveredIndex] = useState(null);
|
|
11
11
|
const [pinnedIndex, setPinnedIndex] = useState(null);
|
|
12
12
|
const [hoveredSeriesKey, setHoveredSeriesKey] = useState(null);
|
|
13
13
|
const [pinnedSeriesKey, setPinnedSeriesKey] = useState(null);
|
|
14
14
|
const onPinnedChangeRef = useRef(onPinnedChange);
|
|
15
|
-
const onPinnedSeriesChangeRef = useRef(onPinnedSeriesChange);
|
|
16
15
|
useEffect(() => {
|
|
17
16
|
onPinnedChangeRef.current = onPinnedChange;
|
|
18
|
-
onPinnedSeriesChangeRef.current = onPinnedSeriesChange;
|
|
19
17
|
});
|
|
20
18
|
const safeHovered = hoveredIndex !== null && hoveredIndex < categoryCount ? hoveredIndex : null;
|
|
21
19
|
const safePinned = pinnedIndex !== null && pinnedIndex < categoryCount ? pinnedIndex : null;
|
|
@@ -26,7 +24,6 @@ function useChartInteraction({ categoryCount, seriesKeys, disabled = false, onPi
|
|
|
26
24
|
setPinnedIndex(null);
|
|
27
25
|
setPinnedSeriesKey(null);
|
|
28
26
|
onPinnedChangeRef.current?.(null);
|
|
29
|
-
onPinnedSeriesChangeRef.current?.(null);
|
|
30
27
|
}, []);
|
|
31
28
|
const hasPin = safePinned !== null || safePinnedSeries !== null;
|
|
32
29
|
useEffect(() => {
|
|
@@ -65,9 +62,9 @@ function useChartInteraction({ categoryCount, seriesKeys, disabled = false, onPi
|
|
|
65
62
|
const next = pinnedSeriesKey === seriesKey ? null : seriesKey;
|
|
66
63
|
setPinnedSeriesKey(next);
|
|
67
64
|
if (next === null) setHoveredSeriesKey(null);
|
|
68
|
-
onPinnedSeriesChangeRef.current?.(next);
|
|
69
65
|
}, [pinnedSeriesKey]);
|
|
70
66
|
const getMarkState = useCallback(({ categoryIndex, seriesKey }) => {
|
|
67
|
+
if (isLoading) return ChartMarkState.MUTED;
|
|
71
68
|
const categoryState = (() => {
|
|
72
69
|
if (categoryIndex === void 0) return ChartMarkState.ACTIVE;
|
|
73
70
|
if (safePinned !== null) {
|
|
@@ -90,18 +87,17 @@ function useChartInteraction({ categoryCount, seriesKeys, disabled = false, onPi
|
|
|
90
87
|
})();
|
|
91
88
|
return severity(categoryState) >= severity(seriesState) ? categoryState : seriesState;
|
|
92
89
|
}, [
|
|
90
|
+
isLoading,
|
|
93
91
|
safePinned,
|
|
94
92
|
safeHovered,
|
|
95
93
|
safePinnedSeries,
|
|
96
94
|
safeHoveredSeries
|
|
97
95
|
]);
|
|
98
96
|
return useMemo(() => ({
|
|
99
|
-
hoveredIndex: safeHovered,
|
|
100
97
|
pinnedIndex: safePinned,
|
|
101
98
|
activeIndex: safePinned ?? safeHovered,
|
|
102
99
|
hoveredSeriesKey: safeHoveredSeries,
|
|
103
100
|
pinnedSeriesKey: safePinnedSeries,
|
|
104
|
-
activeSeriesKey: safePinnedSeries ?? safeHoveredSeries,
|
|
105
101
|
isEngaged: safePinned !== null || safeHovered !== null || safePinnedSeries !== null || safeHoveredSeries !== null,
|
|
106
102
|
getMarkState,
|
|
107
103
|
onCategoryEnter,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ChartPalette, ChartSeriesStyles } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Palette assignment for a chart's series, shared by all three charts.
|
|
4
|
+
*
|
|
5
|
+
* Every chart resolved colour the same way — look the key up in the `series`
|
|
6
|
+
* record, take its slot, fall back to the palette — but each spelled it
|
|
7
|
+
* differently and each rebuilt the key list itself. Doing it once here is what
|
|
8
|
+
* makes "adding a series never recolours the existing ones" a property of the
|
|
9
|
+
* design rather than of three separate call sites remembering to use
|
|
10
|
+
* `indexOf` instead of the loop counter.
|
|
11
|
+
*/
|
|
12
|
+
export interface SeriesColorResolver<TKey extends string> {
|
|
13
|
+
/**
|
|
14
|
+
* Series keys in `series` record order.
|
|
15
|
+
*
|
|
16
|
+
* Insertion order is what makes a key's palette slot stable regardless of
|
|
17
|
+
* which data happens to contain it, so this is never sorted.
|
|
18
|
+
*/
|
|
19
|
+
keys: TKey[];
|
|
20
|
+
/** Colour for a series. The record's own `color` wins over the slot. */
|
|
21
|
+
resolve: (key: TKey, explicit?: ChartPalette) => string;
|
|
22
|
+
/**
|
|
23
|
+
* Colour by palette position, for marks that are not series.
|
|
24
|
+
*
|
|
25
|
+
* A stacked bar's segments are a second dimension with no record of their
|
|
26
|
+
* own, so they take the ramp by position rather than by key.
|
|
27
|
+
*/
|
|
28
|
+
resolveByIndex: (index: number, explicit?: ChartPalette) => string;
|
|
29
|
+
/**
|
|
30
|
+
* Neutral, for a mark that names no series at all.
|
|
31
|
+
*
|
|
32
|
+
* The pie's folded bucket is not one of the slices, so borrowing a hue would
|
|
33
|
+
* imply it is.
|
|
34
|
+
*/
|
|
35
|
+
neutral: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function useSeriesColorResolver<TKey extends string>(series: ChartSeriesStyles<TKey>): SeriesColorResolver<TKey>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useGalaxyTheme } from "../theme/GalaxyTheme.js";
|
|
2
|
+
import { resolveChartPaletteColor, resolveSeriesColor } from "./chartTheme.js";
|
|
3
|
+
import { useCallback, useMemo } from "react";
|
|
4
|
+
//#region src/charts/useSeriesColorResolver.ts
|
|
5
|
+
function useSeriesColorResolver(series) {
|
|
6
|
+
const { theme } = useGalaxyTheme();
|
|
7
|
+
const keys = useMemo(() => Object.keys(series), [series]);
|
|
8
|
+
const resolveByIndex = useCallback((index, explicit) => explicit ? resolveChartPaletteColor(theme, explicit) : resolveSeriesColor(theme, index), [theme]);
|
|
9
|
+
const resolve = useCallback((key, explicit) => resolveSeriesColor(theme, keys.indexOf(key), explicit ?? series[key]?.color), [
|
|
10
|
+
theme,
|
|
11
|
+
keys,
|
|
12
|
+
series
|
|
13
|
+
]);
|
|
14
|
+
return useMemo(() => ({
|
|
15
|
+
keys,
|
|
16
|
+
resolve,
|
|
17
|
+
resolveByIndex,
|
|
18
|
+
neutral: theme.color.text.tertiary
|
|
19
|
+
}), [
|
|
20
|
+
keys,
|
|
21
|
+
resolve,
|
|
22
|
+
resolveByIndex,
|
|
23
|
+
theme
|
|
24
|
+
]);
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { useSeriesColorResolver };
|
package/dist/styles.css
CHANGED
|
@@ -32,17 +32,19 @@
|
|
|
32
32
|
.ceqlpek{stroke:none;pointer-events:none;opacity:1;-webkit-transition:opacity 100ms ease-in-out 200ms;transition:opacity 100ms ease-in-out 200ms;}.ceqlpek[data-state="muted"]{opacity:0.1;}.ceqlpek[data-state="faded"]{opacity:0.04;}@media (prefers-reduced-motion: reduce){.ceqlpek{-webkit-transition:none;transition:none;}}
|
|
33
33
|
.c1fcbddl{pointer-events:none;opacity:1;-webkit-transition:opacity 100ms ease-in-out 200ms;transition:opacity 100ms ease-in-out 200ms;}.c1fcbddl[data-state="muted"]{opacity:0.1;}.c1fcbddl[data-state="faded"]{opacity:0.04;}@media (prefers-reduced-motion: reduce){.c1fcbddl{-webkit-transition:none;transition:none;}}
|
|
34
34
|
.cpcxr59{opacity:1;-webkit-transition:opacity 100ms ease-in-out 200ms;transition:opacity 100ms ease-in-out 200ms;}.cpcxr59[data-state="muted"]{opacity:0.1;}.cpcxr59[data-state="faded"]{opacity:0.04;}@media (prefers-reduced-motion: reduce){.cpcxr59{-webkit-transition:none;transition:none;}}
|
|
35
|
-
.c1wvzvu4{
|
|
36
|
-
.c17guewt{fill:transparent;}.c17guewt[data-interactive="true"]{cursor:pointer;}
|
|
37
|
-
.cblgiyf{
|
|
38
|
-
.c1lr03cc{
|
|
39
|
-
.cizy1s8{
|
|
40
|
-
.c1ukq11d{
|
|
41
|
-
.cwcko2m{
|
|
42
|
-
.cqwhvyz{position:absolute;
|
|
43
|
-
.c1iaray{
|
|
44
|
-
.cz6jod1{
|
|
45
|
-
.c1de5mqk{display:-webkit-
|
|
35
|
+
.c1wvzvu4{color:var(--c1wvzvu4-0);}
|
|
36
|
+
.c17guewt{fill:transparent;outline:none;}.c17guewt[data-interactive="true"]{cursor:pointer;}.c17guewt:focus-visible{outline:none;stroke:currentColor;stroke-width:2;}
|
|
37
|
+
.cblgiyf{fill:transparent;outline:none;}.cblgiyf[data-interactive="true"]{cursor:pointer;}.cblgiyf:focus-visible{outline:none;stroke:currentColor;stroke-width:2;}
|
|
38
|
+
.c1lr03cc{fill:transparent;}.c1lr03cc[data-interactive="true"]{cursor:pointer;}
|
|
39
|
+
.cizy1s8{stroke:var(--cizy1s8-0);shape-rendering:crispEdges;}
|
|
40
|
+
.c1ukq11d{stroke:var(--c1ukq11d-0);pointer-events:none;}
|
|
41
|
+
.cwcko2m{overflow:visible;pointer-events:none;}
|
|
42
|
+
.cqwhvyz{position:absolute;z-index:10;pointer-events:none;left:var(--cqwhvyz-0);top:var(--cqwhvyz-1);min-width:168px;max-width:320px;padding:10px 12px;border:0.5px solid var(--cqwhvyz-2);border-radius:6px;background-color:var(--cqwhvyz-3);opacity:var(--cqwhvyz-4);-webkit-transition:opacity 100ms ease-in-out,left 100ms ease-out,top 100ms ease-out;transition:opacity 100ms ease-in-out,left 100ms ease-out,top 100ms ease-out;}@media (prefers-reduced-motion: reduce){.cqwhvyz{-webkit-transition:none;transition:none;}}
|
|
43
|
+
.c1iaray{position:absolute;left:var(--c1iaray-0);top:var(--c1iaray-1);width:var(--c1iaray-2);height:var(--c1iaray-2);-webkit-transform:translate(-50%, -50%);-moz-transform:translate(-50%, -50%);-ms-transform:translate(-50%, -50%);transform:translate(-50%, -50%);pointer-events:none;text-align:center;}
|
|
44
|
+
.cz6jod1{position:absolute;top:38%;left:50%;-webkit-transform:translate(-50%, -50%);-moz-transform:translate(-50%, -50%);-ms-transform:translate(-50%, -50%);transform:translate(-50%, -50%);max-width:60%;padding:12px;border:0.5px solid var(--cz6jod1-0);border-radius:6px;background-color:var(--cz6jod1-1);pointer-events:none;text-align:center;}
|
|
45
|
+
.c1de5mqk{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex-wrap:wrap;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px 16px;min-width:0;padding-top:10px;padding-left:var(--c1de5mqk-0);}.c1de5mqk[data-active="true"]>*{transition-delay:0ms;}
|
|
46
|
+
.cjirv6a{display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:6px;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;appearance:none;background:none;border:0;padding:0;margin:0;font:inherit;color:inherit;cursor:pointer;border-radius:2px;opacity:1;-webkit-transition:opacity 100ms ease-in-out 200ms;transition:opacity 100ms ease-in-out 200ms;}.cjirv6a[data-state="muted"]{opacity:0.1;}.cjirv6a[data-state="faded"]{opacity:0.04;}@media (prefers-reduced-motion: reduce){.cjirv6a{-webkit-transition:none;transition:none;}}.cjirv6a:focus-visible{outline:1.5px solid currentColor;outline-offset:2px;}
|
|
47
|
+
.c1armna5{display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:6px;opacity:1;-webkit-transition:opacity 100ms ease-in-out 200ms;transition:opacity 100ms ease-in-out 200ms;}.c1armna5[data-state="muted"]{opacity:0.1;}.c1armna5[data-state="faded"]{opacity:0.04;}@media (prefers-reduced-motion: reduce){.c1armna5{-webkit-transition:none;transition:none;}}
|
|
46
48
|
.cmbdg2q{width:var(--cmbdg2q-0);height:var(--cmbdg2q-0);border-radius:50%;background-color:var(--cmbdg2q-1);}
|
|
47
49
|
.shdcix9{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;gap:4px;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;-webkit-justify-content:center;justify-content:center;box-sizing:border-box;min-width:16px;cursor:var(--shdcix9-0);-webkit-transition:border-color 0.1s ease-out;transition:border-color 0.1s ease-out;height:var(--shdcix9-1);padding:var(--shdcix9-2);background-color:var(--shdcix9-3);border:0.5px solid var(--shdcix9-4);border-radius:var(--shdcix9-5);}
|
|
48
50
|
.szfsuud{margin-left:0.5px;}.szfsuud>span{display:inline-block;-webkit-animation:ellipsisPulse-szfsuud var(--szfsuud-0) infinite ease-in-out both;animation:ellipsisPulse-szfsuud var(--szfsuud-0) infinite ease-in-out both;-webkit-animation-play-state:var(--szfsuud-1);animation-play-state:var(--szfsuud-1);}.szfsuud>span:nth-child(1){-webkit-animation-delay:0s;animation-delay:0s;}.szfsuud>span:nth-child(2){-webkit-animation-delay:0.2s;animation-delay:0.2s;}.szfsuud>span:nth-child(3){-webkit-animation-delay:0.4s;animation-delay:0.4s;}@-webkit-keyframes ellipsisPulse-szfsuud{0%,80%,100%{opacity:0.5;}40%{opacity:1;}}@keyframes ellipsisPulse-szfsuud{0%,80%,100%{opacity:0.5;}40%{opacity:1;}}
|