@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.
Files changed (139) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/LICENSE +24 -0
  3. package/README.md +117 -2
  4. package/dist/Chart.d.ts +62 -0
  5. package/dist/Chart.d.ts.map +1 -0
  6. package/dist/Chart.js +401 -0
  7. package/dist/ChartAxis.d.ts +37 -0
  8. package/dist/ChartAxis.d.ts.map +1 -0
  9. package/dist/ChartAxis.js +233 -0
  10. package/dist/ChartContext.d.ts +4 -0
  11. package/dist/ChartContext.d.ts.map +1 -0
  12. package/dist/ChartContext.js +15 -0
  13. package/dist/ChartGrid.d.ts +26 -0
  14. package/dist/ChartGrid.d.ts.map +1 -0
  15. package/dist/ChartGrid.js +118 -0
  16. package/dist/ChartLegend.d.ts +14 -0
  17. package/dist/ChartLegend.d.ts.map +1 -0
  18. package/dist/ChartLegend.js +62 -0
  19. package/dist/ChartSwatch.d.ts +36 -0
  20. package/dist/ChartSwatch.d.ts.map +1 -0
  21. package/dist/ChartSwatch.js +85 -0
  22. package/dist/ChartTooltip.d.ts +46 -0
  23. package/dist/ChartTooltip.d.ts.map +1 -0
  24. package/dist/ChartTooltip.js +330 -0
  25. package/dist/charts.css +33 -0
  26. package/dist/dataTable.d.ts +56 -0
  27. package/dist/dataTable.d.ts.map +1 -0
  28. package/dist/dataTable.js +88 -0
  29. package/dist/formatters.d.ts +37 -0
  30. package/dist/formatters.d.ts.map +1 -0
  31. package/dist/formatters.js +156 -0
  32. package/dist/getChartColors.d.ts +67 -0
  33. package/dist/getChartColors.d.ts.map +1 -0
  34. package/dist/getChartColors.js +191 -0
  35. package/dist/index.d.ts +17 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +19 -0
  38. package/dist/layout.d.ts +40 -0
  39. package/dist/layout.d.ts.map +1 -0
  40. package/dist/layout.js +322 -0
  41. package/dist/legend.d.ts +17 -0
  42. package/dist/legend.d.ts.map +1 -0
  43. package/dist/legend.js +40 -0
  44. package/dist/markColor.d.ts +20 -0
  45. package/dist/markColor.d.ts.map +1 -0
  46. package/dist/markColor.js +56 -0
  47. package/dist/marks/area.d.ts +19 -0
  48. package/dist/marks/area.d.ts.map +1 -0
  49. package/dist/marks/area.js +121 -0
  50. package/dist/marks/band.d.ts +9 -0
  51. package/dist/marks/band.d.ts.map +1 -0
  52. package/dist/marks/band.js +64 -0
  53. package/dist/marks/bar.d.ts +21 -0
  54. package/dist/marks/bar.d.ts.map +1 -0
  55. package/dist/marks/bar.js +161 -0
  56. package/dist/marks/candlestick.d.ts +16 -0
  57. package/dist/marks/candlestick.d.ts.map +1 -0
  58. package/dist/marks/candlestick.js +110 -0
  59. package/dist/marks/dot.d.ts +19 -0
  60. package/dist/marks/dot.d.ts.map +1 -0
  61. package/dist/marks/dot.js +98 -0
  62. package/dist/marks/dotGL.d.ts +16 -0
  63. package/dist/marks/dotGL.d.ts.map +1 -0
  64. package/dist/marks/dotGL.js +231 -0
  65. package/dist/marks/dotGLInteractive.d.ts +35 -0
  66. package/dist/marks/dotGLInteractive.d.ts.map +1 -0
  67. package/dist/marks/dotGLInteractive.js +584 -0
  68. package/dist/marks/errorBar.d.ts +29 -0
  69. package/dist/marks/errorBar.d.ts.map +1 -0
  70. package/dist/marks/errorBar.js +95 -0
  71. package/dist/marks/heatmapGL.d.ts +27 -0
  72. package/dist/marks/heatmapGL.d.ts.map +1 -0
  73. package/dist/marks/heatmapGL.js +335 -0
  74. package/dist/marks/index.d.ts +13 -0
  75. package/dist/marks/index.d.ts.map +1 -0
  76. package/dist/marks/index.js +14 -0
  77. package/dist/marks/line.d.ts +18 -0
  78. package/dist/marks/line.d.ts.map +1 -0
  79. package/dist/marks/line.js +83 -0
  80. package/dist/marks/referenceLine.d.ts +37 -0
  81. package/dist/marks/referenceLine.d.ts.map +1 -0
  82. package/dist/marks/referenceLine.js +204 -0
  83. package/dist/marks/streamGL.d.ts +41 -0
  84. package/dist/marks/streamGL.d.ts.map +1 -0
  85. package/dist/marks/streamGL.js +317 -0
  86. package/dist/tooltip.d.ts +38 -0
  87. package/dist/tooltip.d.ts.map +1 -0
  88. package/dist/tooltip.js +57 -0
  89. package/dist/types.d.ts +193 -0
  90. package/dist/types.d.ts.map +1 -0
  91. package/dist/types.js +70 -0
  92. package/dist/useChartColors.d.ts +16 -0
  93. package/dist/useChartColors.d.ts.map +1 -0
  94. package/dist/useChartColors.js +37 -0
  95. package/dist/utils.d.ts +15 -0
  96. package/dist/utils.d.ts.map +1 -0
  97. package/dist/utils.js +31 -0
  98. package/dist/webgl.d.ts +77 -0
  99. package/dist/webgl.d.ts.map +1 -0
  100. package/dist/webgl.js +235 -0
  101. package/package.json +71 -3
  102. package/src/Chart.doc.mjs +174 -0
  103. package/src/Chart.test.tsx +273 -0
  104. package/src/Chart.tsx +535 -0
  105. package/src/ChartAxis.tsx +289 -0
  106. package/src/ChartContext.ts +18 -0
  107. package/src/ChartGrid.tsx +136 -0
  108. package/src/ChartLegend.tsx +77 -0
  109. package/src/ChartSwatch.doc.mjs +17 -0
  110. package/src/ChartSwatch.tsx +89 -0
  111. package/src/ChartTooltip.tsx +452 -0
  112. package/src/charts.css.d.ts +3 -0
  113. package/src/dataTable.test.ts +86 -0
  114. package/src/dataTable.ts +124 -0
  115. package/src/formatters.ts +167 -0
  116. package/src/getChartColors.test.ts +114 -0
  117. package/src/getChartColors.ts +282 -0
  118. package/src/index.ts +88 -0
  119. package/src/layout.ts +372 -0
  120. package/src/legend.ts +44 -0
  121. package/src/markColor.ts +70 -0
  122. package/src/marks/area.tsx +142 -0
  123. package/src/marks/band.tsx +86 -0
  124. package/src/marks/bar.tsx +210 -0
  125. package/src/marks/candlestick.tsx +128 -0
  126. package/src/marks/dot.tsx +119 -0
  127. package/src/marks/dotGL.tsx +274 -0
  128. package/src/marks/dotGLInteractive.tsx +660 -0
  129. package/src/marks/errorBar.tsx +119 -0
  130. package/src/marks/heatmapGL.tsx +392 -0
  131. package/src/marks/index.ts +17 -0
  132. package/src/marks/line.tsx +118 -0
  133. package/src/marks/referenceLine.tsx +252 -0
  134. package/src/marks/streamGL.tsx +381 -0
  135. package/src/tooltip.ts +73 -0
  136. package/src/types.ts +212 -0
  137. package/src/useChartColors.ts +41 -0
  138. package/src/utils.ts +38 -0
  139. package/src/webgl.ts +262 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,73 @@
1
+ # @pharos-ds/charts
2
+
3
+ # 1.0.10
4
+
5
+ #### New Features
6
+
7
+ - `@pharos-ds/charts` passa a ser publicado no npm junto com o core (mesma versão, peer exato `@pharos-ds/core`, CSS pronto em `@pharos-ds/charts/charts.css`). O `Chart` fica acessível por padrão: SVG com `role="img"` e nome (`aria-label` → `title` → séries), `description` no `aria-describedby` e tabela de dados escondida visualmente (`dataTable`, desligável). Novo `orientation="horizontal"` para barras horizontais, com eixos e grade acompanhando.
8
+
9
+ #### Contributors
10
+
11
+ Thanks to everyone who contributed to this release:
12
+
13
+ - @Caio-Silvestre
14
+
15
+ ---
16
+
17
+ # 1.0.8
18
+
19
+ ---
20
+
21
+ # 1.0.7
22
+
23
+ ---
24
+
25
+ # 1.0.6
26
+
27
+ ---
28
+
29
+ # 1.0.5
30
+
31
+ ---
32
+
33
+ # 1.0.4
34
+
35
+ ---
36
+
37
+ # 1.0.3
38
+
39
+ ---
40
+
41
+ # 1.0.2
42
+
43
+ ---
44
+
45
+ # 1.0.1
46
+
47
+ ---
48
+
49
+ # 1.0.0
50
+
51
+ ---
52
+
53
+ # 0.1.9
54
+
55
+ ---
56
+
57
+ # 0.1.8
58
+
59
+ ---
60
+
61
+ # 0.1.7
62
+
63
+ ---
64
+
65
+ # 0.1.6
66
+
67
+ ---
68
+
69
+ # 0.1.5
70
+
71
+ ---
72
+
73
+ # 0.1.4
package/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Caio Silvestre
4
+
5
+ Copyright (c) 2026 Meta Platforms, Inc.
6
+ (portions derived from Astryx — https://github.com/facebook/astryx)
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,118 @@
1
- # Temporary Holding Version
1
+ # @pharos-ds/charts
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Pharos charts — a config-model data visualization library built on d3, themed by
4
+ Pharos tokens and accessible by default.
5
+
6
+ ```tsx
7
+ import {Chart, ChartAxis, ChartGrid, bar, line} from '@pharos-ds/charts';
8
+
9
+ <Chart
10
+ title="Leads por mês"
11
+ description="Março teve o maior volume do trimestre."
12
+ data={data}
13
+ xKey="mes"
14
+ series={[bar('leads', {label: 'Leads'}), line('meta', {label: 'Meta'})]}
15
+ grid={<ChartGrid />}
16
+ axes={
17
+ <>
18
+ <ChartAxis position="bottom" />
19
+ <ChartAxis position="left" />
20
+ </>
21
+ }
22
+ legend
23
+ tooltip
24
+ />;
25
+ ```
26
+
27
+ Marks are factory functions (`bar`, `line`, `area`, `dot`, `band`, `candlestick`,
28
+ `errorBar`, `referenceLine`, and WebGL variants) that return config objects passed
29
+ via the `series` prop. The chart root owns **one** x/y scale that every mark, axis,
30
+ and grid line reads, so they can never disagree.
31
+
32
+ ## Install
33
+
34
+ `@pharos-ds/charts` is versioned together with `@pharos-ds/core` (same version
35
+ number, exact peer). Install matching versions:
36
+
37
+ ```bash
38
+ npm install @pharos-ds/charts@1.0.x @pharos-ds/core@1.0.x @stylexjs/stylex
39
+ ```
40
+
41
+ Peers: `@pharos-ds/core` (exact version), `@stylexjs/stylex` (runtime only — no
42
+ compiler needed), `react` and `react-dom` ≥ 19.
43
+
44
+ ## Styles
45
+
46
+ The package ships prebuilt CSS, the same way core does. Import it once, after
47
+ core's stylesheet — no StyleX compiler is needed in your app:
48
+
49
+ ```css
50
+ @import '@pharos-ds/core/pharos.css';
51
+ @import '@pharos-ds/theme-pharos/theme.css';
52
+ @import '@pharos-ds/charts/charts.css';
53
+ ```
54
+
55
+ Render charts inside core's `<Theme>` (e.g. with `pharosTheme`). Series without
56
+ an explicit `color` take the theme's categorical palette in order — for Pharos:
57
+ azul, roxo, laranja, amarelo, chumbo — resolved for the active light/dark mode.
58
+
59
+ `Chart` is a client component (`'use client'`): it measures its container, so the
60
+ plot draws after hydration. The hidden data table is part of the server HTML.
61
+
62
+ ## Horizontal bars
63
+
64
+ ```tsx
65
+ <Chart
66
+ title="Leads por canal"
67
+ orientation="horizontal"
68
+ data={porCanal}
69
+ xKey="canal"
70
+ series={[bar('leads', {label: 'Leads'})]}
71
+ margin={{left: 96}}
72
+ grid={<ChartGrid />}
73
+ axes={
74
+ <>
75
+ <ChartAxis position="left" />
76
+ <ChartAxis position="bottom" />
77
+ </>
78
+ }
79
+ tooltip
80
+ />
81
+ ```
82
+
83
+ Categories go on the y-axis and values grow to the right. The grid follows the
84
+ value axis by default. Use the `tooltip` prop (not a `ChartTooltip` child) in
85
+ horizontal charts.
86
+
87
+ ## Accessibility
88
+
89
+ Every chart is exposed as one image with a text alternative (WCAG 1.1.1):
90
+
91
+ - The SVG has `role="img"` and an accessible name: `aria-label` → `title` → the
92
+ data series labels.
93
+ - `subtitle` and `description` become the image description (`aria-describedby`).
94
+ Use `description` for the takeaway.
95
+ - A visually hidden `<table>` is generated from `data`: caption = accessible name,
96
+ header row = x label + series labels, one row per x value. Utility marks
97
+ (`band`, `errorBar`, `referenceLine`) get no column. Customize with
98
+ `dataTable={{xLabel, formatX, formatValue, maxRows}}`, or opt out with
99
+ `dataTable={false}` when the same data is already on the page as a table. Charts
100
+ with more than `maxRows` rows (default 1000) skip the table.
101
+ - Grid, marks and axes are `aria-hidden`.
102
+ - The tooltip is pointer-only: it is never focusable and never traps focus.
103
+ There is no keyboard tooltip; keyboard and screen-reader users get the values
104
+ from the data table.
105
+
106
+ ## Development
107
+
108
+ Inside the monorepo (storybook/sandbox), imports resolve via pnpm workspaces.
109
+ Stories live in `apps/storybook/stories/charts/`. Run the tests with:
110
+
111
+ ```bash
112
+ corepack pnpm exec vitest run packages/charts
113
+ ```
114
+
115
+ > Note: this package is the successor to the experimental `Chart` (formerly
116
+ > `ChartV2`) that used to live in `@pharos-ds/lab`; that code has moved here and
117
+ > d3 is a direct dependency. See the repo-root `docs/CHARTV2_*` docs for the
118
+ > original plan and verification checklist.
@@ -0,0 +1,62 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { SeriesDef, YBaseline, ChartOrientation } from './types';
3
+ import type { ChartMargin } from './types';
4
+ import { type ChartDataTableOptions } from './dataTable';
5
+ import { type ChartLegendProps } from './ChartLegend';
6
+ import { type ChartTooltipProps } from './ChartTooltip';
7
+ export interface ChartProps {
8
+ data: Record<string, unknown>[];
9
+ xKey: string;
10
+ series: SeriesDef[];
11
+ height?: number;
12
+ margin?: Partial<ChartMargin>;
13
+ /** How the y-domain is derived when `yDomain` is not set. Default `'auto'`. */
14
+ yBaseline?: YBaseline;
15
+ /** Explicit y-domain [min, max]. Authoritative — disables baseline/headroom. */
16
+ yDomain?: [number, number];
17
+ /**
18
+ * Explicit x-domain [min, max] for numeric/linear x. Honored even when `data`
19
+ * is empty (stable streaming window). Ignored for categorical (band) scales.
20
+ */
21
+ xDomain?: [number, number];
22
+ grid?: ReactNode;
23
+ axes?: ReactNode;
24
+ legend?: boolean | ChartLegendProps;
25
+ tooltip?: boolean | Omit<ChartTooltipProps, 'series'>;
26
+ interactions?: ReactNode;
27
+ children?: ReactNode;
28
+ /**
29
+ * Visible heading above the chart. Also the chart's accessible name and the
30
+ * hidden data table's caption, unless `aria-label` is given.
31
+ */
32
+ title?: string;
33
+ /** Visible supporting line under the title; exposed as the SVG description. */
34
+ subtitle?: string;
35
+ /**
36
+ * Accessible name for the chart (`role="img"`). Defaults to `title`, then to
37
+ * the data series labels. Use it when the visible title lives outside the
38
+ * chart (e.g. a Card header).
39
+ */
40
+ 'aria-label'?: string;
41
+ /**
42
+ * Text alternative summarizing what the chart shows (trend, highlights). Not
43
+ * rendered visibly; exposed via `aria-describedby` alongside `subtitle`.
44
+ */
45
+ description?: string;
46
+ /**
47
+ * Visually hidden `<table>` with the chart's data, for screen readers
48
+ * (caption = accessible name, header row = x label + series labels, one row
49
+ * per x value). Default `true`. Pass `false` to opt out (e.g. when the same
50
+ * data is already on the page as a table) or an object to customize labels
51
+ * and formatting.
52
+ */
53
+ dataTable?: boolean | ChartDataTableOptions;
54
+ /**
55
+ * `'vertical'` (default) or `'horizontal'` — categories on the y-axis and
56
+ * values growing right (horizontal bars). Axes and grid follow automatically;
57
+ * for the tooltip use the `tooltip` prop (not a `ChartTooltip` child).
58
+ */
59
+ orientation?: ChartOrientation;
60
+ }
61
+ export declare function Chart({ data, xKey, series, height, margin: marginOverride, yBaseline, yDomain, xDomain, grid, axes, legend, tooltip, interactions, children, title, subtitle, 'aria-label': ariaLabel, description, dataTable, orientation, }: ChartProps): import("react").JSX.Element;
62
+ //# sourceMappingURL=Chart.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Chart.d.ts","sourceRoot":"","sources":["../src/Chart.tsx"],"names":[],"mappings":"AA0BA,OAAO,EACL,KAAK,SAAS,EAOf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAC,SAAS,EAAE,SAAS,EAAE,gBAAgB,EAAC,MAAM,SAAS,CAAC;AACpE,OAAO,KAAK,EAAe,WAAW,EAAoB,MAAM,SAAS,CAAC;AAQ1E,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,aAAa,CAAC;AAErB,OAAO,EAAc,KAAK,gBAAgB,EAAC,MAAM,eAAe,CAAC;AAEjE,OAAO,EAAe,KAAK,iBAAiB,EAAC,MAAM,gBAAgB,CAAC;AAIpE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC9B,+EAA+E;IAC/E,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,gFAAgF;IAChF,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAC;IACpC,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CAAC;IACtD,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,qBAAqB,CAAC;IAC5C;;;;OAIG;IACH,WAAW,CAAC,EAAE,gBAAgB,CAAC;CAChC;AAkBD,wBAAgB,KAAK,CAAC,EACpB,IAAI,EACJ,IAAI,EACJ,MAAM,EACN,MAAY,EACZ,MAAM,EAAE,cAAc,EACtB,SAAS,EACT,OAAO,EACP,OAAO,EACP,IAAI,EACJ,IAAI,EACJ,MAAM,EACN,OAAO,EACP,YAAY,EACZ,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,YAAY,EAAE,SAAS,EACvB,WAAW,EACX,SAAgB,EAChB,WAAwB,GACzB,EAAE,UAAU,+BAkYZ"}
package/dist/Chart.js ADDED
@@ -0,0 +1,401 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Chart.tsx (v2)
5
+ * @output Root chart — coordinates layout, rendering, and events
6
+ * @position Top-level; delegates everything to series configs and interaction slots
7
+ *
8
+ * The chart root has ZERO knowledge of mark types. It:
9
+ * 1. Runs the layout engine (scales + stacking + grouping)
10
+ * 2. Calls each series' resolve() and render() methods
11
+ * 3. Provides a single event layer for interaction children
12
+ * 4. Exposes the text alternative (WCAG 1.1.1): the SVG is `role="img"` with
13
+ * an accessible name (`aria-label` → `title` → series labels), optional
14
+ * `<desc>` from `subtitle`/`description`, and a visually hidden data table
15
+ * built from `data` (opt out with `dataTable={false}`). Plot internals are
16
+ * `aria-hidden`.
17
+ * 5. Transposes the plot for `orientation="horizontal"`: marks resolve in the
18
+ * vertical plot space and are drawn through a 90° transform, while axes
19
+ * and grid read display-space scales from context.
20
+ *
21
+ * SYNC: When modified, update /packages/charts/src/Chart.doc.mjs and
22
+ * /packages/charts/README.md
23
+ */
24
+
25
+ 'use client';
26
+
27
+ import { useId, useMemo, useRef, useState, useCallback, useLayoutEffect } from 'react';
28
+ import { computeLayout } from "./layout.js";
29
+ import { ChartProvider } from "./ChartContext.js";
30
+ import { Text } from '@pharos-ds/core';
31
+ import { VStack, HStack } from '@pharos-ds/core';
32
+ import { VisuallyHidden } from '@pharos-ds/core';
33
+ import * as stylex from '@stylexjs/stylex';
34
+ import { scaleLinear } from 'd3-scale';
35
+ import { buildDataTable, fallbackChartLabel } from "./dataTable.js";
36
+ import { isBandScale } from "./utils.js";
37
+ import { ChartLegend } from "./ChartLegend.js";
38
+ import { deriveLegendItems } from "./legend.js";
39
+ import { ChartTooltip } from "./ChartTooltip.js";
40
+ import { useChartColors } from "./useChartColors.js";
41
+ import { isUtilityMarkType } from "./types.js";
42
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
43
+ const DEFAULT_MARGIN = {
44
+ top: 24,
45
+ right: 24,
46
+ bottom: 32,
47
+ left: 48
48
+ };
49
+ export function Chart({
50
+ data,
51
+ xKey,
52
+ series,
53
+ height = 300,
54
+ margin: marginOverride,
55
+ yBaseline,
56
+ yDomain,
57
+ xDomain,
58
+ grid,
59
+ axes,
60
+ legend,
61
+ tooltip,
62
+ interactions,
63
+ children,
64
+ title,
65
+ subtitle,
66
+ 'aria-label': ariaLabel,
67
+ description,
68
+ dataTable = true,
69
+ orientation = 'vertical'
70
+ }) {
71
+ const chartId = useId();
72
+ const descId = `${chartId}-desc`;
73
+ const isHorizontal = orientation === 'horizontal';
74
+ // useId() can contain ':' which is invalid in an SVG url(#id) reference.
75
+ const clipId = `plot-${chartId.replace(/:/g, '')}`;
76
+ const containerRef = useRef(null);
77
+ const svgRef = useRef(null);
78
+ const [containerWidth, setContainerWidth] = useState(0);
79
+ const pointerHandlers = useRef(new Set());
80
+ useLayoutEffect(() => {
81
+ if (!containerRef.current) {
82
+ return;
83
+ }
84
+ const observer = new ResizeObserver(entries => {
85
+ const entry = entries[0];
86
+ if (entry) {
87
+ setContainerWidth(entry.contentRect.width);
88
+ }
89
+ });
90
+ observer.observe(containerRef.current);
91
+ return () => observer.disconnect();
92
+ }, []);
93
+ const margin = useMemo(() => ({
94
+ ...DEFAULT_MARGIN,
95
+ ...marginOverride
96
+ }), [marginOverride]);
97
+ // Clamp plot dimensions to finite, non-negative values. layout.ts hardens
98
+ // against NaN in the *data*, but a non-finite `height` prop or margin override
99
+ // would still collapse every scale range — and the clipPath/event-rect
100
+ // geometry — into NaN. `containerWidth` is always finite (from ResizeObserver).
101
+ const safeHeight = Number.isFinite(height) ? Math.max(0, height) : 0;
102
+ const rawInnerWidth = containerWidth - margin.left - margin.right;
103
+ const rawInnerHeight = safeHeight - margin.top - margin.bottom;
104
+ const innerWidth = Number.isFinite(rawInnerWidth) ? Math.max(0, rawInnerWidth) : 0;
105
+ const innerHeight = Number.isFinite(rawInnerHeight) ? Math.max(0, rawInnerHeight) : 0;
106
+ // Plot space: marks always resolve as if the chart were vertical. For a
107
+ // horizontal chart the category axis runs along the display height and the
108
+ // value axis along the display width, so the plot box is transposed.
109
+ const plotWidth = isHorizontal ? innerHeight : innerWidth;
110
+ const plotHeight = isHorizontal ? innerWidth : innerHeight;
111
+
112
+ // ─── Color assignment ─────────────────────────────────────────────────
113
+ // Give every primary series that doesn't supply a static color (auto-colored
114
+ // or accessor-colored) a distinct color from the theme's categorical palette.
115
+ // Utility marks (band/errorBar/referenceLine) don't consume palette slots.
116
+ // Assigned during render (like the layout pass annotates `_uid`) so the first
117
+ // paint is already colored; intentionally not a `useMemo` — it mutates the
118
+ // caller's series defs, and useMemo is allowed to drop its cached result.
119
+ const chartColors = useChartColors();
120
+ const needsColor = series.filter(s => !isUtilityMarkType(s.type) && s.color == null);
121
+ const palette = chartColors.categorical(Math.max(needsColor.length, 1));
122
+ for (let i = 0; i < needsColor.length; i++) {
123
+ needsColor[i]._resolvedColor = palette[i % palette.length];
124
+ }
125
+
126
+ // ─── Layout pass ──────────────────────────────────────────────────────
127
+ const layout = useMemo(() => computeLayout({
128
+ data,
129
+ xKey,
130
+ series,
131
+ width: plotWidth,
132
+ height: plotHeight,
133
+ yBaseline,
134
+ yDomain,
135
+ xDomain
136
+ }), [data, xKey, series, plotWidth, plotHeight, yBaseline, yDomain, xDomain]);
137
+ const seriesCtx = useMemo(() => ({
138
+ data,
139
+ xKey,
140
+ xScale: layout.xScale,
141
+ yScale: layout.yScale,
142
+ yBandScale: layout.yBandScale,
143
+ width: plotWidth,
144
+ height: plotHeight
145
+ }), [data, xKey, layout, plotWidth, plotHeight]);
146
+
147
+ // ─── Event dispatch ───────────────────────────────────────────────────
148
+ const onPointer = useCallback(handler => {
149
+ pointerHandlers.current.add(handler);
150
+ return () => {
151
+ pointerHandlers.current.delete(handler);
152
+ };
153
+ }, []);
154
+ const dispatch = useCallback(e => {
155
+ for (const handler of pointerHandlers.current) {
156
+ handler(e);
157
+ }
158
+ }, []);
159
+ const handlePointerMove = useCallback(e => {
160
+ const svg = svgRef.current;
161
+ if (!svg) {
162
+ return;
163
+ }
164
+ const point = svg.createSVGPoint();
165
+ point.x = e.clientX;
166
+ point.y = e.clientY;
167
+ const ctm = e.currentTarget.getScreenCTM()?.inverse();
168
+ if (!ctm) {
169
+ return;
170
+ }
171
+ const cursor = point.matrixTransform(ctm);
172
+
173
+ // Find nearest point by category distance only (the full column — or
174
+ // row, when horizontal — is the hover zone). Resolved points live in
175
+ // plot space, where the category position is always `px`.
176
+ const cursorCategory = isHorizontal ? cursor.y : cursor.x;
177
+ let nearest = null;
178
+ let bestXDist = Infinity;
179
+ for (const [seriesKey, points] of layout.resolved) {
180
+ for (const p of points) {
181
+ const xDist = Math.abs(p.px - cursorCategory);
182
+ if (xDist < bestXDist) {
183
+ bestXDist = xDist;
184
+ nearest = {
185
+ ...p,
186
+ seriesKey
187
+ };
188
+ }
189
+ }
190
+ }
191
+ dispatch({
192
+ x: cursor.x,
193
+ y: cursor.y,
194
+ nearest,
195
+ active: e.buttons > 0
196
+ });
197
+ }, [layout.resolved, dispatch, isHorizontal]);
198
+ const handlePointerLeave = useCallback(() => {
199
+ dispatch({
200
+ x: -1,
201
+ y: -1,
202
+ nearest: null,
203
+ active: false
204
+ });
205
+ }, [dispatch]);
206
+
207
+ // ─── Context for interactions + v1 chrome ─────────────────────────────
208
+ // `plotCtx` is the vertical plot space marks resolve in (it is also the
209
+ // display space for vertical charts). For horizontal charts, chrome gets a
210
+ // display-space view: the value scale runs along x (left → right) and the
211
+ // category scale along y (top → bottom).
212
+ const plotCtx = useMemo(() => ({
213
+ orientation,
214
+ width: plotWidth,
215
+ height: plotHeight,
216
+ margin,
217
+ data,
218
+ xKey,
219
+ xScale: layout.xScale,
220
+ yScale: layout.yScale,
221
+ yBandScale: layout.yBandScale,
222
+ resolved: layout.resolved,
223
+ onPointer,
224
+ svgRef
225
+ }), [orientation, plotWidth, plotHeight, margin, data, xKey, layout, onPointer]);
226
+ const ctx = useMemo(() => {
227
+ if (!isHorizontal) {
228
+ return plotCtx;
229
+ }
230
+ const valueScale = layout.yScale.copy().range([0, innerWidth]);
231
+ const plotX = layout.xScale;
232
+ return {
233
+ ...plotCtx,
234
+ width: innerWidth,
235
+ height: innerHeight,
236
+ xScale: valueScale,
237
+ ...(isBandScale(plotX) ? {
238
+ yBandScale: plotX.copy().range([0, innerHeight]),
239
+ // Unused for a categorical y; kept so `yScale` stays a valid scale.
240
+ yScale: scaleLinear().range([innerHeight, 0])
241
+ } : {
242
+ yBandScale: undefined,
243
+ yScale: plotX.copy().range([0, innerHeight])
244
+ })
245
+ };
246
+ }, [plotCtx, isHorizontal, layout, innerWidth, innerHeight]);
247
+
248
+ // ─── Accessibility ────────────────────────────────────────────────────
249
+ const accessibleName = ariaLabel || title || fallbackChartLabel(series) || undefined;
250
+ const descriptionText = [subtitle, description].filter(Boolean).join('. ');
251
+ const tableModel = useMemo(() => dataTable === false ? null : buildDataTable(data, xKey, series, dataTable === true ? {} : dataTable), [data, xKey, series, dataTable]);
252
+ const tableElement = tableModel ? /*#__PURE__*/_jsx(VisuallyHidden, {
253
+ as: "div",
254
+ children: /*#__PURE__*/_jsxs("table", {
255
+ children: [accessibleName && /*#__PURE__*/_jsx("caption", {
256
+ children: accessibleName
257
+ }), /*#__PURE__*/_jsx("thead", {
258
+ children: /*#__PURE__*/_jsx("tr", {
259
+ children: tableModel.headers.map((h, i) => /*#__PURE__*/_jsx("th", {
260
+ scope: "col",
261
+ children: h
262
+ }, i))
263
+ })
264
+ }), /*#__PURE__*/_jsx("tbody", {
265
+ children: tableModel.rows.map(row => /*#__PURE__*/_jsxs("tr", {
266
+ children: [/*#__PURE__*/_jsx("th", {
267
+ scope: "row",
268
+ children: row.header
269
+ }), row.cells.map((cell, i) => /*#__PURE__*/_jsx("td", {
270
+ children: cell
271
+ }, i))]
272
+ }, row.key))
273
+ })]
274
+ })
275
+ }) : null;
276
+
277
+ // ─── Legend ────────────────────────────────────────────────────────────
278
+ const legendConfig = legend === true ? {} : legend || null;
279
+ const legendElement = legendConfig ? /*#__PURE__*/_jsx(ChartLegend, {
280
+ items: legendConfig.items ?? deriveLegendItems(series),
281
+ position: legendConfig.position,
282
+ alignment: legendConfig.alignment
283
+ }) : null;
284
+ const legendPosition = legendConfig?.position ?? 'bottom';
285
+ const isLegendHorizontal = legendPosition === 'start' || legendPosition === 'end';
286
+
287
+ // ─── Render ───────────────────────────────────────────────────────────
288
+ // Before the first measurement (and during SSR) only the sized placeholder
289
+ // renders — plus the hidden data table, which doesn't depend on width, so
290
+ // the text alternative is in the server HTML too.
291
+ if (containerWidth === 0) {
292
+ return /*#__PURE__*/_jsx("div", {
293
+ ref: containerRef,
294
+ ...{
295
+ className: "xh8yej3"
296
+ },
297
+ style: {
298
+ height: safeHeight
299
+ },
300
+ children: tableElement
301
+ });
302
+ }
303
+ return /*#__PURE__*/_jsxs("div", {
304
+ ref: containerRef,
305
+ ...{
306
+ className: "xh8yej3"
307
+ },
308
+ children: [(title || subtitle) && /*#__PURE__*/_jsxs("div", {
309
+ ...{
310
+ className: "x1yztbdb"
311
+ },
312
+ children: [title && /*#__PURE__*/_jsx(Text, {
313
+ type: "body",
314
+ weight: "semibold",
315
+ display: "block",
316
+ children: title
317
+ }), subtitle && /*#__PURE__*/_jsx(Text, {
318
+ type: "supporting",
319
+ display: "block",
320
+ children: subtitle
321
+ })]
322
+ }), /*#__PURE__*/_jsx(ChartProvider, {
323
+ value: ctx,
324
+ children: legendConfig == null ? renderSvg() : isLegendHorizontal ? /*#__PURE__*/_jsxs(HStack, {
325
+ gap: 4,
326
+ children: [legendPosition === 'start' && legendElement, /*#__PURE__*/_jsx("div", {
327
+ ...{
328
+ className: "x98rzlu xeuugli xb3r6kr"
329
+ },
330
+ children: renderSvg()
331
+ }), legendPosition === 'end' && legendElement]
332
+ }) : /*#__PURE__*/_jsxs(VStack, {
333
+ gap: 4,
334
+ children: [legendPosition === 'top' && legendElement, renderSvg(), legendPosition === 'bottom' && legendElement]
335
+ })
336
+ }), tableElement]
337
+ });
338
+ function renderSvg() {
339
+ const plotTransform = isHorizontal ? `matrix(0,1,-1,0,${innerWidth},0)` : undefined;
340
+ return /*#__PURE__*/_jsxs("svg", {
341
+ ref: svgRef,
342
+ width: "100%",
343
+ height: safeHeight,
344
+ role: "img",
345
+ "aria-label": accessibleName,
346
+ "aria-describedby": descriptionText ? descId : undefined,
347
+ children: [descriptionText && /*#__PURE__*/_jsx("desc", {
348
+ id: descId,
349
+ children: descriptionText
350
+ }), /*#__PURE__*/_jsx("defs", {
351
+ children: /*#__PURE__*/_jsx("clipPath", {
352
+ id: clipId,
353
+ children: /*#__PURE__*/_jsx("rect", {
354
+ x: 0,
355
+ y: 0,
356
+ width: innerWidth,
357
+ height: innerHeight
358
+ })
359
+ })
360
+ }), /*#__PURE__*/_jsxs("g", {
361
+ transform: `translate(${margin.left},${margin.top})`,
362
+ children: [/*#__PURE__*/_jsxs("g", {
363
+ "aria-hidden": "true",
364
+ children: [grid, /*#__PURE__*/_jsx("g", {
365
+ clipPath: `url(#${clipId})`,
366
+ children: /*#__PURE__*/_jsx("g", {
367
+ transform: plotTransform,
368
+ children: series.map(s => {
369
+ const resolved = s._uid ? layout.resolved.get(s._uid) : undefined;
370
+ if (!resolved) {
371
+ return null;
372
+ }
373
+ return /*#__PURE__*/_jsx("g", {
374
+ children: s.render(resolved, seriesCtx)
375
+ }, s._uid);
376
+ })
377
+ })
378
+ }), axes]
379
+ }), /*#__PURE__*/_jsx("rect", {
380
+ x: 0,
381
+ y: 0,
382
+ width: innerWidth,
383
+ height: innerHeight,
384
+ fill: "transparent",
385
+ onPointerMove: handlePointerMove,
386
+ onPointerLeave: handlePointerLeave
387
+ }), interactions, tooltip && /*#__PURE__*/_jsx("g", {
388
+ "aria-hidden": "true",
389
+ transform: plotTransform,
390
+ children: /*#__PURE__*/_jsx(ChartProvider, {
391
+ value: plotCtx,
392
+ children: /*#__PURE__*/_jsx(ChartTooltip, {
393
+ series: series,
394
+ ...(tooltip === true ? {} : tooltip)
395
+ })
396
+ })
397
+ }), children]
398
+ })]
399
+ });
400
+ }
401
+ }