@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
@@ -0,0 +1,273 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Chart.test.tsx
5
+ * @input Uses vitest, @testing-library/react, Chart + bar/line/area marks,
6
+ * core's Theme and the Pharos theme source
7
+ * @output Rendering tests: marks per datum, accessible name/description, the
8
+ * hidden data table, horizontal orientation, and theme colors (light/dark)
9
+ * @position Testing; validates Chart.tsx, marks/bar.tsx, marks/line.tsx
10
+ *
11
+ * SYNC: When Chart.tsx, dataTable.ts or the marks change, update these tests
12
+ */
13
+
14
+ import {describe, it, expect, vi} from 'vitest';
15
+ import {render, screen, within} from '@testing-library/react';
16
+ import {renderToString} from 'react-dom/server';
17
+ import type {ReactNode} from 'react';
18
+ import {Theme} from '@pharos-ds/core/theme';
19
+ import {pharosTheme} from '../../themes/pharos/src/pharosTheme';
20
+ import {Chart} from './Chart';
21
+ import {ChartAxis} from './ChartAxis';
22
+ import {ChartGrid} from './ChartGrid';
23
+ import {bar, line, area, referenceLine} from './marks';
24
+
25
+ // jsdom has no layout: report a fixed width so the chart leaves its
26
+ // pre-measurement placeholder and draws the SVG.
27
+ const WIDTH = 600;
28
+ class MockResizeObserver {
29
+ callback: ResizeObserverCallback;
30
+ constructor(callback: ResizeObserverCallback) {
31
+ this.callback = callback;
32
+ }
33
+ observe(target: Element) {
34
+ this.callback(
35
+ [{contentRect: {width: WIDTH}, target} as unknown as ResizeObserverEntry],
36
+ this as unknown as ResizeObserver,
37
+ );
38
+ }
39
+ unobserve() {}
40
+ disconnect() {}
41
+ }
42
+ vi.stubGlobal('ResizeObserver', MockResizeObserver);
43
+
44
+ const data = [
45
+ {canal: 'Instagram', leads: 120, meta: 100},
46
+ {canal: 'Google', leads: 80, meta: 90},
47
+ {canal: 'Indicação', leads: 45, meta: 50},
48
+ ];
49
+
50
+ function inTheme(mode: 'light' | 'dark', children: ReactNode) {
51
+ return (
52
+ <Theme theme={pharosTheme} mode={mode}>
53
+ {children}
54
+ </Theme>
55
+ );
56
+ }
57
+
58
+ /** Mark paths inside the clipped plot group (excludes grid/axis lines). */
59
+ function markPaths(container: HTMLElement): SVGPathElement[] {
60
+ return Array.from(
61
+ container.querySelectorAll<SVGPathElement>('g[clip-path] path'),
62
+ );
63
+ }
64
+
65
+ describe('Chart — bar', () => {
66
+ it('renders one bar per datum', () => {
67
+ const {container} = render(
68
+ <Chart data={data} xKey="canal" series={[bar('leads')]} />,
69
+ );
70
+ const bars = markPaths(container);
71
+ expect(bars).toHaveLength(data.length);
72
+ for (const b of bars) {
73
+ expect(b.getAttribute('d')).toMatch(/^M[\d.]+,[\d.]+/);
74
+ }
75
+ });
76
+
77
+ it('exposes the chart as an image named by its title', () => {
78
+ render(
79
+ <Chart
80
+ title="Leads por canal"
81
+ data={data}
82
+ xKey="canal"
83
+ series={[bar('leads', {label: 'Leads'})]}
84
+ />,
85
+ );
86
+ const img = screen.getByRole('img', {name: 'Leads por canal'});
87
+ expect(img.tagName.toLowerCase()).toBe('svg');
88
+ // Plot internals are decorative — the text alternative is the table.
89
+ expect(img.querySelector('g[aria-hidden="true"]')).not.toBeNull();
90
+ // No SVG <title>: it would show a native tooltip over the custom one.
91
+ expect(img.querySelector('title')).toBeNull();
92
+ });
93
+
94
+ it('renders a hidden data table with caption, headers and one row per x', () => {
95
+ render(
96
+ <Chart
97
+ title="Leads por canal"
98
+ data={data}
99
+ xKey="canal"
100
+ series={[
101
+ bar('leads', {label: 'Leads'}),
102
+ line('meta', {label: 'Meta'}),
103
+ referenceLine({y: 100, label: 'Alvo'}),
104
+ ]}
105
+ dataTable={{xLabel: 'Canal'}}
106
+ />,
107
+ );
108
+ const table = screen.getByRole('table', {name: 'Leads por canal'});
109
+ const headers = within(table)
110
+ .getAllByRole('columnheader')
111
+ .map(h => h.textContent);
112
+ // Utility marks (referenceLine) get no column.
113
+ expect(headers).toEqual(['Canal', 'Leads', 'Meta']);
114
+ const rows = within(table).getAllByRole('row').slice(1);
115
+ expect(rows).toHaveLength(data.length);
116
+ expect(
117
+ within(rows[0])
118
+ .getAllByRole('cell')
119
+ .map(c => c.textContent),
120
+ ).toEqual([(120).toLocaleString(), (100).toLocaleString()]);
121
+ expect(within(rows[2]).getByRole('rowheader').textContent).toBe(
122
+ 'Indicação',
123
+ );
124
+ });
125
+
126
+ it('omits the table when dataTable={false}', () => {
127
+ render(
128
+ <Chart
129
+ title="Leads"
130
+ data={data}
131
+ xKey="canal"
132
+ series={[bar('leads')]}
133
+ dataTable={false}
134
+ />,
135
+ );
136
+ expect(screen.queryByRole('table')).toBeNull();
137
+ expect(screen.getByRole('img', {name: 'Leads'})).toBeInTheDocument();
138
+ });
139
+
140
+ it('includes the text alternative in server HTML (before measurement)', () => {
141
+ const html = renderToString(
142
+ <Chart title="Leads" data={data} xKey="canal" series={[bar('leads')]} />,
143
+ );
144
+ expect(html).toContain('<table');
145
+ expect(html).toContain('<caption>Leads</caption>');
146
+ expect(html).toContain('Instagram');
147
+ });
148
+
149
+ it('draws horizontal bars through a transpose, with categories on the left axis', () => {
150
+ const {container} = render(
151
+ <Chart
152
+ title="Leads por canal"
153
+ orientation="horizontal"
154
+ data={data}
155
+ xKey="canal"
156
+ series={[bar('leads')]}
157
+ grid={<ChartGrid />}
158
+ axes={
159
+ <>
160
+ <ChartAxis position="left" />
161
+ <ChartAxis position="bottom" />
162
+ </>
163
+ }
164
+ />,
165
+ );
166
+ expect(markPaths(container)).toHaveLength(data.length);
167
+ const transposed = container.querySelector('g[clip-path] > g[transform]');
168
+ expect(transposed?.getAttribute('transform')).toMatch(/^matrix\(0,1,-1,0,/);
169
+ const left = container.querySelector('g[aria-label="left axis"]');
170
+ const labels = Array.from(left?.querySelectorAll('text') ?? []).map(
171
+ t => t.textContent,
172
+ );
173
+ expect(labels).toEqual(data.map(d => d.canal));
174
+ // Value axis at the bottom shows numbers, starting at zero.
175
+ const bottom = container.querySelector('g[aria-label="bottom axis"]');
176
+ expect(bottom?.querySelector('text')?.textContent).toBe('0');
177
+ // Default grid follows the value axis: vertical lines only.
178
+ const gridLines = Array.from(container.querySelectorAll('svg line')).filter(
179
+ l => l.getAttribute('x1') === l.getAttribute('x2'),
180
+ );
181
+ expect(gridLines.length).toBeGreaterThan(0);
182
+ });
183
+ });
184
+
185
+ describe('Chart — line', () => {
186
+ it('renders one path for the series, plus dots when asked', () => {
187
+ const {container} = render(
188
+ <Chart data={data} xKey="canal" series={[line('leads', {dots: true})]} />,
189
+ );
190
+ const paths = markPaths(container);
191
+ expect(paths).toHaveLength(1);
192
+ expect(paths[0].getAttribute('fill')).toBe('none');
193
+ expect(paths[0].getAttribute('d')).toMatch(/^M/);
194
+ expect(container.querySelectorAll('g[clip-path] circle')).toHaveLength(
195
+ data.length,
196
+ );
197
+ });
198
+
199
+ it('prefers aria-label and wires subtitle + description', () => {
200
+ render(
201
+ <Chart
202
+ title="Evolução"
203
+ subtitle="Últimos 3 canais"
204
+ aria-label="Evolução de leads por canal"
205
+ description="Instagram lidera com 120 leads."
206
+ data={data}
207
+ xKey="canal"
208
+ series={[line('leads', {label: 'Leads'})]}
209
+ />,
210
+ );
211
+ const img = screen.getByRole('img', {
212
+ name: 'Evolução de leads por canal',
213
+ });
214
+ expect(img).toHaveAccessibleDescription(
215
+ 'Últimos 3 canais. Instagram lidera com 120 leads.',
216
+ );
217
+ // The table caption follows the accessible name.
218
+ expect(
219
+ screen.getByRole('table', {name: 'Evolução de leads por canal'}),
220
+ ).toBeInTheDocument();
221
+ });
222
+
223
+ it('falls back to the series labels for the accessible name', () => {
224
+ render(
225
+ <Chart
226
+ data={data}
227
+ xKey="canal"
228
+ series={[
229
+ line('leads', {label: 'Leads'}),
230
+ area('meta', {label: 'Meta'}),
231
+ ]}
232
+ />,
233
+ );
234
+ expect(screen.getByRole('img', {name: 'Leads, Meta'})).toBeInTheDocument();
235
+ });
236
+ });
237
+
238
+ describe('Chart — theme colors', () => {
239
+ const LIGHT = ['#034694', '#7C51A1', '#F26641'];
240
+ const DARK = ['#6BA3F5', '#A67BC4', '#F26641'];
241
+
242
+ it.each([
243
+ ['light', LIGHT],
244
+ ['dark', DARK],
245
+ ] as const)(
246
+ 'colors auto series in the Pharos categorical order (%s)',
247
+ (mode, expected) => {
248
+ const {container} = render(
249
+ inTheme(
250
+ mode,
251
+ <Chart
252
+ data={data}
253
+ xKey="canal"
254
+ series={[
255
+ bar('leads', {group: 'g'}),
256
+ bar('meta', {group: 'g'}),
257
+ line('leads'),
258
+ ]}
259
+ />,
260
+ ),
261
+ );
262
+ const paths = markPaths(container);
263
+ const barFills = new Set(
264
+ paths
265
+ .filter(p => p.getAttribute('fill') !== 'none')
266
+ .map(p => p.getAttribute('fill')?.toUpperCase()),
267
+ );
268
+ expect([...barFills]).toEqual(expected.slice(0, 2));
269
+ const linePath = paths.find(p => p.getAttribute('fill') === 'none');
270
+ expect(linePath?.getAttribute('stroke')?.toUpperCase()).toBe(expected[2]);
271
+ },
272
+ );
273
+ });