@pharos-ds/charts 0.0.0-stage → 1.0.10
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/CHANGELOG.md +73 -0
- package/LICENSE +24 -0
- package/README.md +117 -2
- package/dist/Chart.d.ts +62 -0
- package/dist/Chart.d.ts.map +1 -0
- package/dist/Chart.js +401 -0
- package/dist/ChartAxis.d.ts +37 -0
- package/dist/ChartAxis.d.ts.map +1 -0
- package/dist/ChartAxis.js +233 -0
- package/dist/ChartContext.d.ts +4 -0
- package/dist/ChartContext.d.ts.map +1 -0
- package/dist/ChartContext.js +15 -0
- package/dist/ChartGrid.d.ts +26 -0
- package/dist/ChartGrid.d.ts.map +1 -0
- package/dist/ChartGrid.js +118 -0
- package/dist/ChartLegend.d.ts +14 -0
- package/dist/ChartLegend.d.ts.map +1 -0
- package/dist/ChartLegend.js +62 -0
- package/dist/ChartSwatch.d.ts +36 -0
- package/dist/ChartSwatch.d.ts.map +1 -0
- package/dist/ChartSwatch.js +85 -0
- package/dist/ChartTooltip.d.ts +46 -0
- package/dist/ChartTooltip.d.ts.map +1 -0
- package/dist/ChartTooltip.js +330 -0
- package/dist/charts.css +33 -0
- package/dist/dataTable.d.ts +56 -0
- package/dist/dataTable.d.ts.map +1 -0
- package/dist/dataTable.js +88 -0
- package/dist/formatters.d.ts +37 -0
- package/dist/formatters.d.ts.map +1 -0
- package/dist/formatters.js +156 -0
- package/dist/getChartColors.d.ts +67 -0
- package/dist/getChartColors.d.ts.map +1 -0
- package/dist/getChartColors.js +191 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/layout.d.ts +40 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +322 -0
- package/dist/legend.d.ts +17 -0
- package/dist/legend.d.ts.map +1 -0
- package/dist/legend.js +40 -0
- package/dist/markColor.d.ts +20 -0
- package/dist/markColor.d.ts.map +1 -0
- package/dist/markColor.js +56 -0
- package/dist/marks/area.d.ts +19 -0
- package/dist/marks/area.d.ts.map +1 -0
- package/dist/marks/area.js +121 -0
- package/dist/marks/band.d.ts +9 -0
- package/dist/marks/band.d.ts.map +1 -0
- package/dist/marks/band.js +64 -0
- package/dist/marks/bar.d.ts +21 -0
- package/dist/marks/bar.d.ts.map +1 -0
- package/dist/marks/bar.js +161 -0
- package/dist/marks/candlestick.d.ts +16 -0
- package/dist/marks/candlestick.d.ts.map +1 -0
- package/dist/marks/candlestick.js +110 -0
- package/dist/marks/dot.d.ts +19 -0
- package/dist/marks/dot.d.ts.map +1 -0
- package/dist/marks/dot.js +98 -0
- package/dist/marks/dotGL.d.ts +16 -0
- package/dist/marks/dotGL.d.ts.map +1 -0
- package/dist/marks/dotGL.js +231 -0
- package/dist/marks/dotGLInteractive.d.ts +35 -0
- package/dist/marks/dotGLInteractive.d.ts.map +1 -0
- package/dist/marks/dotGLInteractive.js +584 -0
- package/dist/marks/errorBar.d.ts +29 -0
- package/dist/marks/errorBar.d.ts.map +1 -0
- package/dist/marks/errorBar.js +95 -0
- package/dist/marks/heatmapGL.d.ts +27 -0
- package/dist/marks/heatmapGL.d.ts.map +1 -0
- package/dist/marks/heatmapGL.js +335 -0
- package/dist/marks/index.d.ts +13 -0
- package/dist/marks/index.d.ts.map +1 -0
- package/dist/marks/index.js +14 -0
- package/dist/marks/line.d.ts +18 -0
- package/dist/marks/line.d.ts.map +1 -0
- package/dist/marks/line.js +83 -0
- package/dist/marks/referenceLine.d.ts +37 -0
- package/dist/marks/referenceLine.d.ts.map +1 -0
- package/dist/marks/referenceLine.js +204 -0
- package/dist/marks/streamGL.d.ts +41 -0
- package/dist/marks/streamGL.d.ts.map +1 -0
- package/dist/marks/streamGL.js +317 -0
- package/dist/tooltip.d.ts +38 -0
- package/dist/tooltip.d.ts.map +1 -0
- package/dist/tooltip.js +57 -0
- package/dist/types.d.ts +193 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +70 -0
- package/dist/useChartColors.d.ts +16 -0
- package/dist/useChartColors.d.ts.map +1 -0
- package/dist/useChartColors.js +37 -0
- package/dist/utils.d.ts +15 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +31 -0
- package/dist/webgl.d.ts +77 -0
- package/dist/webgl.d.ts.map +1 -0
- package/dist/webgl.js +235 -0
- package/package.json +71 -3
- package/src/Chart.doc.mjs +174 -0
- package/src/Chart.test.tsx +273 -0
- package/src/Chart.tsx +535 -0
- package/src/ChartAxis.tsx +289 -0
- package/src/ChartContext.ts +18 -0
- package/src/ChartGrid.tsx +136 -0
- package/src/ChartLegend.tsx +77 -0
- package/src/ChartSwatch.doc.mjs +17 -0
- package/src/ChartSwatch.tsx +89 -0
- package/src/ChartTooltip.tsx +452 -0
- package/src/charts.css.d.ts +3 -0
- package/src/dataTable.test.ts +86 -0
- package/src/dataTable.ts +124 -0
- package/src/formatters.ts +167 -0
- package/src/getChartColors.test.ts +114 -0
- package/src/getChartColors.ts +282 -0
- package/src/index.ts +88 -0
- package/src/layout.ts +372 -0
- package/src/legend.ts +44 -0
- package/src/markColor.ts +70 -0
- package/src/marks/area.tsx +142 -0
- package/src/marks/band.tsx +86 -0
- package/src/marks/bar.tsx +210 -0
- package/src/marks/candlestick.tsx +128 -0
- package/src/marks/dot.tsx +119 -0
- package/src/marks/dotGL.tsx +274 -0
- package/src/marks/dotGLInteractive.tsx +660 -0
- package/src/marks/errorBar.tsx +119 -0
- package/src/marks/heatmapGL.tsx +392 -0
- package/src/marks/index.ts +17 -0
- package/src/marks/line.tsx +118 -0
- package/src/marks/referenceLine.tsx +252 -0
- package/src/marks/streamGL.tsx +381 -0
- package/src/tooltip.ts +73 -0
- package/src/types.ts +212 -0
- package/src/useChartColors.ts +41 -0
- package/src/utils.ts +38 -0
- package/src/webgl.ts +262 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type TooltipSeriesValue } from './tooltip';
|
|
3
|
+
import type { SeriesDef } from './types';
|
|
4
|
+
export type ChartTooltipPlacement = 'auto' | 'right' | 'left' | 'top';
|
|
5
|
+
export interface ChartTooltipProps {
|
|
6
|
+
/**
|
|
7
|
+
* Series definitions — used to map the hovered datum to per-series rows.
|
|
8
|
+
* When rendered via `<Chart tooltip />`, Chart injects this automatically.
|
|
9
|
+
* When composing directly, pass the same `series` array you gave Chart.
|
|
10
|
+
*/
|
|
11
|
+
series?: readonly SeriesDef[];
|
|
12
|
+
/**
|
|
13
|
+
* Custom render function. Receives the x-value and one row per visible series.
|
|
14
|
+
* Returning `null` hides the card while still rendering the band highlight + dots.
|
|
15
|
+
*/
|
|
16
|
+
render?: (xValue: unknown, seriesValues: TooltipSeriesValue[]) => ReactNode;
|
|
17
|
+
/**
|
|
18
|
+
* Whether to draw a hover indicator at the focused x-position.
|
|
19
|
+
* Picks the right shape automatically:
|
|
20
|
+
* - **band-highlight rect** when the chart includes bar series on a band scale
|
|
21
|
+
* - **vertical crosshair line** for line / area / dot charts
|
|
22
|
+
*
|
|
23
|
+
* Default: `true`.
|
|
24
|
+
*/
|
|
25
|
+
hoverIndicator?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Whether to draw a hover dot at each series' point (skipped for bars).
|
|
28
|
+
* Default: `true`.
|
|
29
|
+
*/
|
|
30
|
+
showHoverDots?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Card placement relative to the hovered point:
|
|
33
|
+
* - `'auto'` (default): right of the point, flips to left if it would overflow
|
|
34
|
+
* - `'right'` / `'left'` / `'top'`: pinned position
|
|
35
|
+
*/
|
|
36
|
+
placement?: ChartTooltipPlacement;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Composable grouped tooltip for Chart.
|
|
40
|
+
*
|
|
41
|
+
* Subscribes to the chart's pointer event stream and only re-renders when the
|
|
42
|
+
* hovered data index changes (not on every pointer move). Card position is
|
|
43
|
+
* updated imperatively via a ref to keep pointer-move cost ~free.
|
|
44
|
+
*/
|
|
45
|
+
export declare function ChartTooltip({ series, render, hoverIndicator, showHoverDots, placement, }: ChartTooltipProps): import("react").JSX.Element;
|
|
46
|
+
//# sourceMappingURL=ChartTooltip.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChartTooltip.d.ts","sourceRoot":"","sources":["../src/ChartTooltip.tsx"],"names":[],"mappings":"AAkCA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAaf,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAC,SAAS,EAAoB,MAAM,SAAS,CAAC;AAG1D,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;AAEtE,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,KAAK,SAAS,CAAC;IAC5E;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,qBAAqB,CAAC;CACnC;AA2DD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,EAC3B,MAAW,EACX,MAAM,EACN,cAAqB,EACrB,aAAoB,EACpB,SAAkB,GACnB,EAAE,iBAAiB,+BA0RnB"}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file ChartTooltip.tsx
|
|
5
|
+
* @output Grouped chart tooltip — shows all series values at the hovered x-position.
|
|
6
|
+
* Composable: drop into `<Chart tooltip>` or render directly inside a chart
|
|
7
|
+
* to take full control of props.
|
|
8
|
+
* @position Reads from ChartContext — must render inside an <Chart>.
|
|
9
|
+
*
|
|
10
|
+
* Pointer-only: the card is portaled with `pointer-events: none` and holds no
|
|
11
|
+
* focusable content, so it never traps keyboard focus. Keyboard/screen-reader
|
|
12
|
+
* users get the same values from the chart's hidden data table instead.
|
|
13
|
+
*
|
|
14
|
+
* In a horizontal chart the tooltip renders in the vertical plot space (Chart
|
|
15
|
+
* wraps it in the same transpose as the marks), so only the screen position of
|
|
16
|
+
* the card needs to know about `orientation`.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```
|
|
20
|
+
* // Default — declarative on Chart
|
|
21
|
+
* <Chart tooltip />
|
|
22
|
+
*
|
|
23
|
+
* // Custom render function via Chart prop
|
|
24
|
+
* <Chart tooltip={{render: (x, rows) => <MyCard x={x} rows={rows} />}} />
|
|
25
|
+
*
|
|
26
|
+
* // Composable — drop into the chart's children for full control
|
|
27
|
+
* <Chart>
|
|
28
|
+
* <ChartTooltip hoverIndicator={false} placement="top" />
|
|
29
|
+
* </Chart>
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
'use client';
|
|
34
|
+
|
|
35
|
+
import { memo, useState, useEffect, useMemo, useRef, useCallback } from 'react';
|
|
36
|
+
import { createPortal } from 'react-dom';
|
|
37
|
+
import * as stylex from '@stylexjs/stylex';
|
|
38
|
+
import '@pharos-ds/core/theme/tokens.stylex';
|
|
39
|
+
import { colorVars, radiusVars, shadowVars, spacingVars } from '@pharos-ds/core/theme/tokens.stylex';
|
|
40
|
+
import { Text } from '@pharos-ds/core';
|
|
41
|
+
import { VStack, HStack } from '@pharos-ds/core';
|
|
42
|
+
import { useChart } from "./ChartContext.js";
|
|
43
|
+
import { ChartSwatch, swatchVariantForType } from "./ChartSwatch.js";
|
|
44
|
+
import { deriveTooltipSeriesValues, shouldRenderHoverDot } from "./tooltip.js";
|
|
45
|
+
import { isBandScale, xPixel } from "./utils.js";
|
|
46
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
47
|
+
/** Default tooltip body — used when `render` is not provided. */
|
|
48
|
+
const DefaultTooltipContent = /*#__PURE__*/memo(function DefaultTooltipContent({
|
|
49
|
+
xValue,
|
|
50
|
+
rows
|
|
51
|
+
}) {
|
|
52
|
+
return /*#__PURE__*/_jsxs(VStack, {
|
|
53
|
+
gap: 1,
|
|
54
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
55
|
+
type: "body",
|
|
56
|
+
weight: "semibold",
|
|
57
|
+
children: String(xValue)
|
|
58
|
+
}), rows.length === 1 ? /*#__PURE__*/_jsx(Text, {
|
|
59
|
+
type: "supporting",
|
|
60
|
+
children: String(rows[0].value)
|
|
61
|
+
}) : /*#__PURE__*/_jsx(VStack, {
|
|
62
|
+
gap: 1,
|
|
63
|
+
children: rows.map((row, i) => /*#__PURE__*/_jsxs(HStack, {
|
|
64
|
+
gap: 2,
|
|
65
|
+
vAlign: "center",
|
|
66
|
+
children: [/*#__PURE__*/_jsx(ChartSwatch, {
|
|
67
|
+
color: row.color,
|
|
68
|
+
variant: swatchVariantForType(row.type)
|
|
69
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
70
|
+
type: "supporting",
|
|
71
|
+
children: row.label
|
|
72
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
73
|
+
type: "supporting",
|
|
74
|
+
weight: "semibold",
|
|
75
|
+
children: String(row.value)
|
|
76
|
+
})]
|
|
77
|
+
}, `${row.key}-${i}`))
|
|
78
|
+
})]
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Composable grouped tooltip for Chart.
|
|
84
|
+
*
|
|
85
|
+
* Subscribes to the chart's pointer event stream and only re-renders when the
|
|
86
|
+
* hovered data index changes (not on every pointer move). Card position is
|
|
87
|
+
* updated imperatively via a ref to keep pointer-move cost ~free.
|
|
88
|
+
*/
|
|
89
|
+
export function ChartTooltip({
|
|
90
|
+
series = [],
|
|
91
|
+
render,
|
|
92
|
+
hoverIndicator = true,
|
|
93
|
+
showHoverDots = true,
|
|
94
|
+
placement = 'auto'
|
|
95
|
+
}) {
|
|
96
|
+
const {
|
|
97
|
+
data,
|
|
98
|
+
xKey,
|
|
99
|
+
resolved,
|
|
100
|
+
onPointer,
|
|
101
|
+
svgRef,
|
|
102
|
+
margin,
|
|
103
|
+
xScale,
|
|
104
|
+
width,
|
|
105
|
+
height,
|
|
106
|
+
orientation
|
|
107
|
+
} = useChart();
|
|
108
|
+
const isHorizontal = orientation === 'horizontal';
|
|
109
|
+
const [hoveredIndex, setHoveredIndex] = useState(null);
|
|
110
|
+
const cardRef = useRef(null);
|
|
111
|
+
// Whether the card is currently suppressed (no content, or a custom `render`
|
|
112
|
+
// opted out by returning null). positionCard reads this so a same-index
|
|
113
|
+
// pointer move — which repositions the card without a React commit — can't
|
|
114
|
+
// re-reveal a card the visibility effect below has hidden.
|
|
115
|
+
const cardHiddenRef = useRef(false);
|
|
116
|
+
|
|
117
|
+
// Stable Set of resolved series keys — used by deriveTooltipSeriesValues.
|
|
118
|
+
// Recomputed only when the resolved map identity changes (per layout pass).
|
|
119
|
+
const resolvedKeys = useMemo(() => new Set(resolved.keys()), [resolved]);
|
|
120
|
+
|
|
121
|
+
// ─── Card positioning ──────────────────────────────────────────────────
|
|
122
|
+
// Imperative — the card is portaled, and we don't want pointer-move to
|
|
123
|
+
// trigger React renders.
|
|
124
|
+
const positionCard = useCallback(e => {
|
|
125
|
+
const card = cardRef.current;
|
|
126
|
+
const svg = svgRef.current;
|
|
127
|
+
if (!card) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!svg || !e.nearest || cardHiddenRef.current) {
|
|
131
|
+
card.style.display = 'none';
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const svgRect = svg.getBoundingClientRect();
|
|
135
|
+
const cardWidth = card.offsetWidth;
|
|
136
|
+
const cardHeight = card.offsetHeight;
|
|
137
|
+
// Plot space → screen. Horizontal charts are transposed: the category
|
|
138
|
+
// (px) runs down the y-axis and the value end of the mark (py) maps to
|
|
139
|
+
// x = plot height − py, so the card sits beside the hovered row.
|
|
140
|
+
const screenX = isHorizontal ? svgRect.left + margin.left + (Number.isFinite(e.nearest.py) ? height - e.nearest.py : 0) : svgRect.left + margin.left + e.nearest.px;
|
|
141
|
+
const screenY = isHorizontal ? svgRect.top + margin.top + e.nearest.px - cardHeight / 2 : svgRect.top + margin.top;
|
|
142
|
+
const viewportWidth = window.innerWidth;
|
|
143
|
+
const viewportHeight = window.innerHeight;
|
|
144
|
+
const gap = 8;
|
|
145
|
+
let x;
|
|
146
|
+
let y = screenY;
|
|
147
|
+
switch (placement) {
|
|
148
|
+
case 'left':
|
|
149
|
+
x = screenX - cardWidth - gap;
|
|
150
|
+
break;
|
|
151
|
+
case 'right':
|
|
152
|
+
x = screenX + gap;
|
|
153
|
+
break;
|
|
154
|
+
case 'top':
|
|
155
|
+
x = screenX - cardWidth / 2;
|
|
156
|
+
y = screenY - cardHeight - gap;
|
|
157
|
+
break;
|
|
158
|
+
case 'auto':
|
|
159
|
+
default:
|
|
160
|
+
{
|
|
161
|
+
const rightX = screenX + gap;
|
|
162
|
+
x = rightX + cardWidth > viewportWidth ? screenX - cardWidth - gap : rightX;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Clamp to the viewport so the card never clips off-screen — it's
|
|
167
|
+
// portaled and position:fixed, so this keeps it fully visible for every
|
|
168
|
+
// placement (pinned placements and the near-edge `auto` flip alike).
|
|
169
|
+
x = Math.max(gap, Math.min(x, viewportWidth - cardWidth - gap));
|
|
170
|
+
y = Math.max(gap, Math.min(y, viewportHeight - cardHeight - gap));
|
|
171
|
+
card.style.left = `${x}px`;
|
|
172
|
+
card.style.top = `${y}px`;
|
|
173
|
+
card.style.display = 'block';
|
|
174
|
+
}, [svgRef, margin, placement, isHorizontal, height]);
|
|
175
|
+
|
|
176
|
+
// ─── Subscribe to pointer events ───────────────────────────────────────
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
let currentIndex = null;
|
|
179
|
+
const unsub = onPointer(e => {
|
|
180
|
+
const newIndex = e.nearest?.dataIndex ?? null;
|
|
181
|
+
if (newIndex !== currentIndex) {
|
|
182
|
+
currentIndex = newIndex;
|
|
183
|
+
// Optimistically treat the new index as showable; the visibility effect
|
|
184
|
+
// re-hides it after commit if there's no content (e.g. custom `render`
|
|
185
|
+
// returned null). This keeps the card appearing on the first move.
|
|
186
|
+
cardHiddenRef.current = false;
|
|
187
|
+
setHoveredIndex(newIndex);
|
|
188
|
+
}
|
|
189
|
+
positionCard(e);
|
|
190
|
+
});
|
|
191
|
+
return unsub;
|
|
192
|
+
}, [onPointer, positionCard]);
|
|
193
|
+
|
|
194
|
+
// ─── Derived data ──────────────────────────────────────────────────────
|
|
195
|
+
const datum = hoveredIndex != null ? data[hoveredIndex] : undefined;
|
|
196
|
+
const xValue = datum?.[xKey];
|
|
197
|
+
const seriesValues = useMemo(() => deriveTooltipSeriesValues(series, datum, resolvedKeys), [series, datum, resolvedKeys]);
|
|
198
|
+
|
|
199
|
+
// Hover dots — read from the resolved map at the hovered index.
|
|
200
|
+
const dots = useMemo(() => {
|
|
201
|
+
if (!showHoverDots || hoveredIndex == null) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
const elements = [];
|
|
205
|
+
for (const s of series) {
|
|
206
|
+
if (!shouldRenderHoverDot(s.type)) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const points = s._uid ? resolved.get(s._uid) : undefined;
|
|
210
|
+
const point = points?.find(p => p.dataIndex === hoveredIndex);
|
|
211
|
+
// A resolved point exists at every index, but its coordinates are NaN when
|
|
212
|
+
// this series' value here is missing/non-finite (the mark itself filters
|
|
213
|
+
// these at render time — mirror that so we never emit cx/cy="NaN").
|
|
214
|
+
if (!point || !Number.isFinite(point.px) || !Number.isFinite(point.py)) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
elements.push(/*#__PURE__*/_jsx("circle", {
|
|
218
|
+
cx: point.px,
|
|
219
|
+
cy: point.py,
|
|
220
|
+
r: 4,
|
|
221
|
+
fill: "var(--color-background-surface)",
|
|
222
|
+
stroke: s.color ?? s._resolvedColor ?? 'var(--color-border-emphasized)',
|
|
223
|
+
strokeWidth: 2,
|
|
224
|
+
pointerEvents: "none"
|
|
225
|
+
}, s._uid));
|
|
226
|
+
}
|
|
227
|
+
return elements;
|
|
228
|
+
}, [showHoverDots, hoveredIndex, series, resolved]);
|
|
229
|
+
|
|
230
|
+
// Hover indicator — band-highlight rect for bar charts, vertical crosshair
|
|
231
|
+
// line for line/area charts. Same source-of-truth (hoveredIndex), distinct
|
|
232
|
+
// visual idioms picked automatically based on the series mix.
|
|
233
|
+
const hoverIndicatorElement = useMemo(() => {
|
|
234
|
+
if (!hoverIndicator || hoveredIndex == null) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const hoveredDatum = data[hoveredIndex];
|
|
238
|
+
const xv = hoveredDatum?.[xKey];
|
|
239
|
+
if (hoveredDatum == null || xv == null) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const hasBars = series.some(s => s.type === 'bar');
|
|
243
|
+
|
|
244
|
+
// Bar charts on a band scale → soft column highlight.
|
|
245
|
+
if (hasBars && isBandScale(xScale)) {
|
|
246
|
+
const x = xScale(String(xv)) ?? 0;
|
|
247
|
+
const bw = xScale.bandwidth();
|
|
248
|
+
const pad = 8;
|
|
249
|
+
const rectX = Math.max(0, x - pad);
|
|
250
|
+
const rectRight = Math.min(width, x + bw + pad);
|
|
251
|
+
const rectWidth = rectRight - rectX;
|
|
252
|
+
// Plot-space "top" is the display's right edge when horizontal.
|
|
253
|
+
const topPad = Math.min(pad, isHorizontal ? margin.right : margin.top);
|
|
254
|
+
return /*#__PURE__*/_jsx("rect", {
|
|
255
|
+
x: rectX,
|
|
256
|
+
y: -topPad,
|
|
257
|
+
width: rectWidth,
|
|
258
|
+
height: height + topPad,
|
|
259
|
+
fill: "currentColor",
|
|
260
|
+
opacity: 0.06,
|
|
261
|
+
rx: 4,
|
|
262
|
+
pointerEvents: "none"
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Line / area / dot charts → vertical crosshair through the point
|
|
267
|
+
// (band-center or linear position, resolved by the shared helper).
|
|
268
|
+
const px = xPixel(hoveredDatum, xKey, xScale);
|
|
269
|
+
// xPixel returns NaN for a non-finite x on a linear scale; skip rather than
|
|
270
|
+
// emit x1/x2="NaN". (The band branch above is always finite via `?? 0`.)
|
|
271
|
+
if (!Number.isFinite(px)) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
return /*#__PURE__*/_jsx("line", {
|
|
275
|
+
x1: px,
|
|
276
|
+
x2: px,
|
|
277
|
+
y1: 0,
|
|
278
|
+
y2: height,
|
|
279
|
+
...{
|
|
280
|
+
className: "x1lo1t26 xnpr05h"
|
|
281
|
+
},
|
|
282
|
+
pointerEvents: "none"
|
|
283
|
+
});
|
|
284
|
+
}, [hoverIndicator, hoveredIndex, series, xScale, data, xKey, width, height, margin.top, margin.right, isHorizontal]);
|
|
285
|
+
|
|
286
|
+
// Single source of truth for whether the card has anything to show. It's
|
|
287
|
+
// hidden when there's no hover, when the hovered index is stale/out-of-range
|
|
288
|
+
// (e.g. `data` shrank while hovering, so `datum` is undefined), or when a
|
|
289
|
+
// custom `render` opts out by returning null — the documented contract, which
|
|
290
|
+
// still shows the band highlight + dots. The decision is stored in a ref so
|
|
291
|
+
// the imperative positionCard honors it on subsequent same-index moves
|
|
292
|
+
// (otherwise it would re-reveal an empty bordered box). Runs after commit, so
|
|
293
|
+
// `render` (which may return null, or throw) never executes on the SSR path.
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
const hidden = hoveredIndex == null || datum == null || render != null && render(xValue, seriesValues) == null;
|
|
296
|
+
cardHiddenRef.current = hidden;
|
|
297
|
+
if (hidden) {
|
|
298
|
+
const card = cardRef.current;
|
|
299
|
+
if (card) {
|
|
300
|
+
card.style.display = 'none';
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}, [render, hoveredIndex, datum, xValue, seriesValues]);
|
|
304
|
+
|
|
305
|
+
// ─── Render ────────────────────────────────────────────────────────────
|
|
306
|
+
if (typeof document === 'undefined') {
|
|
307
|
+
// SSR: no DOM portal target; band highlight + dots still render in SVG.
|
|
308
|
+
return /*#__PURE__*/_jsxs(_Fragment, {
|
|
309
|
+
children: [hoverIndicatorElement, dots]
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Mirrors the visibility effect's first two conditions: no card when there's
|
|
314
|
+
// no hover or the hovered datum is stale/out-of-range (so `render` and the
|
|
315
|
+
// default body are never called with an undefined x-value + empty rows).
|
|
316
|
+
const cardContent = hoveredIndex == null || datum == null ? null : render ? render(xValue, seriesValues) : /*#__PURE__*/_jsx(DefaultTooltipContent, {
|
|
317
|
+
xValue: xValue,
|
|
318
|
+
rows: seriesValues
|
|
319
|
+
});
|
|
320
|
+
return /*#__PURE__*/_jsxs(_Fragment, {
|
|
321
|
+
children: [hoverIndicatorElement, dots, /*#__PURE__*/createPortal(/*#__PURE__*/_jsx("div", {
|
|
322
|
+
ref: cardRef,
|
|
323
|
+
role: "tooltip",
|
|
324
|
+
...{
|
|
325
|
+
className: "xixxii4 x1prclbq x1hviunn xce4md1 xrrkdod x14hfi27 x47corl xuxw1ft x1q2oy4v x1s85apg"
|
|
326
|
+
},
|
|
327
|
+
children: cardContent
|
|
328
|
+
}), document.body)]
|
|
329
|
+
});
|
|
330
|
+
}
|
package/dist/charts.css
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* Pharos Charts Pre-compiled StyleX CSS — data visualization components */
|
|
2
|
+
/* Auto-generated. Do not edit manually. */
|
|
3
|
+
|
|
4
|
+
@layer pharos-base {
|
|
5
|
+
@property --x-backgroundColor { syntax: "*"; inherits: false;}
|
|
6
|
+
.xbvija:not(#\#){border-radius:1.5px}
|
|
7
|
+
.x1cum3z5:not(#\#){border-radius:2px}
|
|
8
|
+
.x1hviunn:not(#\#){border-radius:var(--radius-container)}
|
|
9
|
+
.x98rzlu:not(#\#){flex:1}
|
|
10
|
+
.xckhbgb:not(#\#){margin-inline:1px}
|
|
11
|
+
.xb3r6kr:not(#\#){overflow:hidden}
|
|
12
|
+
.xce4md1:not(#\#){padding-block:var(--spacing-2)}
|
|
13
|
+
.xrrkdod:not(#\#){padding-inline:var(--spacing-3)}
|
|
14
|
+
.x1prclbq:not(#\#):not(#\#){background-color:var(--color-background-popover)}
|
|
15
|
+
.xl8spv7:not(#\#):not(#\#){background-color:var(--x-backgroundColor)}
|
|
16
|
+
.x14hfi27:not(#\#):not(#\#){box-shadow:var(--shadow-med)}
|
|
17
|
+
.x1s85apg:not(#\#):not(#\#){display:none}
|
|
18
|
+
.x2lah0s:not(#\#):not(#\#){flex-shrink:0}
|
|
19
|
+
.x47corl:not(#\#):not(#\#){pointer-events:none}
|
|
20
|
+
.xixxii4:not(#\#):not(#\#){position:fixed}
|
|
21
|
+
.xnpr05h:not(#\#):not(#\#){stroke-width:1}
|
|
22
|
+
.x113i6qz:not(#\#):not(#\#){stroke:var(--color-border-emphasized)}
|
|
23
|
+
.x1lo1t26:not(#\#):not(#\#){stroke:var(--color-text-primary)}
|
|
24
|
+
.xuxw1ft:not(#\#):not(#\#){white-space:nowrap}
|
|
25
|
+
.x1q2oy4v:not(#\#):not(#\#){z-index:9999}
|
|
26
|
+
.xuoj239:not(#\#):not(#\#):not(#\#){height:3px}
|
|
27
|
+
.xdk7pt:not(#\#):not(#\#):not(#\#){height:8px}
|
|
28
|
+
.x1yztbdb:not(#\#):not(#\#):not(#\#){margin-bottom:16px}
|
|
29
|
+
.xeuugli:not(#\#):not(#\#):not(#\#){min-width:0}
|
|
30
|
+
.xh8yej3:not(#\#):not(#\#):not(#\#){width:100%}
|
|
31
|
+
.x1fsd2vl:not(#\#):not(#\#):not(#\#){width:10px}
|
|
32
|
+
.x1xc55vz:not(#\#):not(#\#):not(#\#){width:8px}
|
|
33
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file dataTable.ts
|
|
3
|
+
* @output Pure builder for the chart's text alternative (WCAG 1.1.1): turns
|
|
4
|
+
* `data` + `xKey` + `series` into a header row and one row per x value.
|
|
5
|
+
* @position Used by Chart to render the visually hidden `<table>` that sits
|
|
6
|
+
* next to the `role="img"` SVG. Mirrors legend.ts / tooltip.ts: data shaping
|
|
7
|
+
* lives here, presentation lives in Chart.tsx.
|
|
8
|
+
*
|
|
9
|
+
* Utility marks (band, errorBar, referenceLine) are annotations, not data the
|
|
10
|
+
* reader compares, so they get no column — same rule as the legend and the
|
|
11
|
+
* tooltip. A mark that reads several keys (candlestick: open/high/low/close)
|
|
12
|
+
* gets one column per key.
|
|
13
|
+
*
|
|
14
|
+
* SYNC: When modified, update /packages/charts/src/Chart.doc.mjs
|
|
15
|
+
*/
|
|
16
|
+
import { type SeriesDef } from './types';
|
|
17
|
+
/** Options for the hidden data table rendered by `<Chart>`. */
|
|
18
|
+
export interface ChartDataTableOptions {
|
|
19
|
+
/** Header of the first column (the x values). Default: the chart's `xKey`. */
|
|
20
|
+
xLabel?: string;
|
|
21
|
+
/** Formats an x value for its row header cell. Default: `String(value)`. */
|
|
22
|
+
formatX?: (value: unknown) => string;
|
|
23
|
+
/**
|
|
24
|
+
* Formats one value cell. Receives the raw value, the series and the data key
|
|
25
|
+
* being read. Default: locale-formatted numbers, `String()` otherwise and an
|
|
26
|
+
* empty cell for missing values.
|
|
27
|
+
*/
|
|
28
|
+
formatValue?: (value: unknown, series: SeriesDef, dataKey: string) => string;
|
|
29
|
+
/**
|
|
30
|
+
* Largest `data.length` that still renders a table. Above it the table is
|
|
31
|
+
* omitted (streaming/WebGL charts with thousands of points) and the chart
|
|
32
|
+
* relies on its accessible name and `description`. Default: `1000`.
|
|
33
|
+
*/
|
|
34
|
+
maxRows?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface ChartDataTableModel {
|
|
37
|
+
headers: string[];
|
|
38
|
+
rows: {
|
|
39
|
+
key: string;
|
|
40
|
+
header: string;
|
|
41
|
+
cells: string[];
|
|
42
|
+
}[];
|
|
43
|
+
}
|
|
44
|
+
/** Default row cap — see `ChartDataTableOptions.maxRows`. */
|
|
45
|
+
export declare const DATA_TABLE_MAX_ROWS = 1000;
|
|
46
|
+
/**
|
|
47
|
+
* Build the table model, or `null` when there is nothing to tabulate (no data,
|
|
48
|
+
* no data series) or `data` exceeds `maxRows`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function buildDataTable(data: readonly Record<string, unknown>[], xKey: string, series: readonly SeriesDef[], options?: ChartDataTableOptions): ChartDataTableModel | null;
|
|
51
|
+
/**
|
|
52
|
+
* Accessible name fallback when the chart has neither `aria-label` nor
|
|
53
|
+
* `title`: the data series labels, so the name is never empty.
|
|
54
|
+
*/
|
|
55
|
+
export declare function fallbackChartLabel(series: readonly SeriesDef[]): string;
|
|
56
|
+
//# sourceMappingURL=dataTable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataTable.d.ts","sourceRoot":"","sources":["../src/dataTable.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAoB,KAAK,SAAS,EAAC,MAAM,SAAS,CAAC;AAE1D,+DAA+D;AAC/D,MAAM,WAAW,qBAAqB;IACpC,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;IACrC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IAC7E;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAC,EAAE,CAAC;CACxD;AAED,6DAA6D;AAC7D,eAAO,MAAM,mBAAmB,OAAO,CAAC;AAsBxC;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACxC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,SAAS,SAAS,EAAE,EAC5B,OAAO,GAAE,qBAA0B,GAClC,mBAAmB,GAAG,IAAI,CAmC5B;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,GAAG,MAAM,CAKvE"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file dataTable.ts
|
|
5
|
+
* @output Pure builder for the chart's text alternative (WCAG 1.1.1): turns
|
|
6
|
+
* `data` + `xKey` + `series` into a header row and one row per x value.
|
|
7
|
+
* @position Used by Chart to render the visually hidden `<table>` that sits
|
|
8
|
+
* next to the `role="img"` SVG. Mirrors legend.ts / tooltip.ts: data shaping
|
|
9
|
+
* lives here, presentation lives in Chart.tsx.
|
|
10
|
+
*
|
|
11
|
+
* Utility marks (band, errorBar, referenceLine) are annotations, not data the
|
|
12
|
+
* reader compares, so they get no column — same rule as the legend and the
|
|
13
|
+
* tooltip. A mark that reads several keys (candlestick: open/high/low/close)
|
|
14
|
+
* gets one column per key.
|
|
15
|
+
*
|
|
16
|
+
* SYNC: When modified, update /packages/charts/src/Chart.doc.mjs
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { isUtilityMarkType } from "./types.js";
|
|
20
|
+
|
|
21
|
+
/** Options for the hidden data table rendered by `<Chart>`. */
|
|
22
|
+
|
|
23
|
+
/** Default row cap — see `ChartDataTableOptions.maxRows`. */
|
|
24
|
+
export const DATA_TABLE_MAX_ROWS = 1000;
|
|
25
|
+
function defaultFormatValue(value) {
|
|
26
|
+
if (value == null) {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
if (typeof value === 'number') {
|
|
30
|
+
return Number.isFinite(value) ? value.toLocaleString() : '';
|
|
31
|
+
}
|
|
32
|
+
if (value instanceof Date) {
|
|
33
|
+
return value.toLocaleDateString();
|
|
34
|
+
}
|
|
35
|
+
return String(value);
|
|
36
|
+
}
|
|
37
|
+
function defaultFormatX(value) {
|
|
38
|
+
if (value instanceof Date) {
|
|
39
|
+
return value.toLocaleDateString();
|
|
40
|
+
}
|
|
41
|
+
return value == null ? '' : String(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build the table model, or `null` when there is nothing to tabulate (no data,
|
|
46
|
+
* no data series) or `data` exceeds `maxRows`.
|
|
47
|
+
*/
|
|
48
|
+
export function buildDataTable(data, xKey, series, options = {}) {
|
|
49
|
+
const maxRows = options.maxRows ?? DATA_TABLE_MAX_ROWS;
|
|
50
|
+
if (data.length === 0 || data.length > maxRows) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const columns = [];
|
|
54
|
+
for (const s of series) {
|
|
55
|
+
if (isUtilityMarkType(s.type)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const label = s.label ?? s.key;
|
|
59
|
+
for (const dataKey of s.dataKeys) {
|
|
60
|
+
columns.push({
|
|
61
|
+
series: s,
|
|
62
|
+
dataKey,
|
|
63
|
+
header: s.dataKeys.length > 1 ? `${label} (${dataKey})` : label
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (columns.length === 0) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const formatX = options.formatX ?? defaultFormatX;
|
|
71
|
+
const formatValue = options.formatValue ?? defaultFormatValue;
|
|
72
|
+
return {
|
|
73
|
+
headers: [options.xLabel ?? xKey, ...columns.map(c => c.header)],
|
|
74
|
+
rows: data.map((d, i) => ({
|
|
75
|
+
key: String(i),
|
|
76
|
+
header: formatX(d[xKey]),
|
|
77
|
+
cells: columns.map(c => formatValue(d[c.dataKey], c.series, c.dataKey))
|
|
78
|
+
}))
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Accessible name fallback when the chart has neither `aria-label` nor
|
|
84
|
+
* `title`: the data series labels, so the name is never empty.
|
|
85
|
+
*/
|
|
86
|
+
export function fallbackChartLabel(series) {
|
|
87
|
+
return series.filter(s => !isUtilityMarkType(s.type)).map(s => s.label ?? s.key).join(', ');
|
|
88
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact number formatter (e.g. 1200 → "1.2K", 1500000 → "1.5M", 1e12 → "1T").
|
|
3
|
+
*
|
|
4
|
+
* Non-numeric values (e.g. category labels) and non-finite numbers pass through
|
|
5
|
+
* unchanged, so it is safe to use as a general-purpose tick formatter.
|
|
6
|
+
*/
|
|
7
|
+
export declare function compactNumber(value: unknown): string;
|
|
8
|
+
/**
|
|
9
|
+
* Currency formatter factory. Returns a tick format function.
|
|
10
|
+
*
|
|
11
|
+
* The symbol prefixes the magnitude and the sign leads the symbol, so negatives
|
|
12
|
+
* read as "-$1.5K" rather than "$-1.5K". Large values use compact notation.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```
|
|
16
|
+
* <ChartAxis tickFormat={currency()} /> // $1.5K
|
|
17
|
+
* <ChartAxis tickFormat={currency('€')} /> // €1.5K
|
|
18
|
+
* <ChartAxis tickFormat={currency('¥')} /> // ¥1.5K
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function currency(symbol?: string): (value: unknown) => string;
|
|
22
|
+
/**
|
|
23
|
+
* Percent formatter (e.g. 0.45 → "45%", 1.2 → "120%", 0.005 → "0.5%").
|
|
24
|
+
*
|
|
25
|
+
* The input is a ratio; it is multiplied by 100 and localized. Whole percents
|
|
26
|
+
* render without a decimal, and up to one fractional digit is shown otherwise.
|
|
27
|
+
*/
|
|
28
|
+
export declare function percent(value: unknown): string;
|
|
29
|
+
/**
|
|
30
|
+
* Date formatter — short date (e.g. "Jan 5", "Mar 12").
|
|
31
|
+
*/
|
|
32
|
+
export declare function shortDate(value: unknown): string;
|
|
33
|
+
/**
|
|
34
|
+
* Date formatter — month/year (e.g. "Jan 2024", "Mar 2025").
|
|
35
|
+
*/
|
|
36
|
+
export declare function monthYear(value: unknown): string;
|
|
37
|
+
//# sourceMappingURL=formatters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatters.d.ts","sourceRoot":"","sources":["../src/formatters.ts"],"names":[],"mappings":"AA4CA;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAMpD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CAAC,MAAM,SAAM,GAAG,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAYjE;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAM9C;AAiDD;;GAEG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAMhD;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAMhD"}
|