@pharos-ds/charts 0.0.0-bootstrap.0 → 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 +118 -0
  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 +72 -4
  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,167 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file formatters.ts
5
+ * @output Built-in, locale-aware tick/value format utilities for common data types
6
+ * @position Utility; consumed by ChartAxis via tickFormat prop
7
+ *
8
+ * Formatters degrade gracefully: non-finite numbers (NaN/±Infinity) and
9
+ * unparseable values are passed through as-is rather than producing garbage
10
+ * like "InfinityB", and no input throws. Numeric/date formatting is delegated
11
+ * to cached `Intl` formatters so output is locale-aware and rounding is correct
12
+ * across the full magnitude range (K/M/B/T and beyond).
13
+ */
14
+
15
+ /** Lazily create a value once and cache it — keeps `Intl` construction off the import path. */
16
+ function once<T>(create: () => T): () => T {
17
+ let cached: {value: T} | null = null;
18
+ return () => (cached ??= {value: create()}).value;
19
+ }
20
+
21
+ const getCompactFormat = once(
22
+ () =>
23
+ new Intl.NumberFormat(undefined, {
24
+ notation: 'compact',
25
+ maximumFractionDigits: 1,
26
+ }),
27
+ );
28
+
29
+ const getPercentFormat = once(
30
+ () =>
31
+ new Intl.NumberFormat(undefined, {
32
+ style: 'percent',
33
+ maximumFractionDigits: 1,
34
+ }),
35
+ );
36
+
37
+ const getShortDateFormat = once(
38
+ () => new Intl.DateTimeFormat(undefined, {month: 'short', day: 'numeric'}),
39
+ );
40
+
41
+ const getMonthYearFormat = once(
42
+ () => new Intl.DateTimeFormat(undefined, {month: 'short', year: 'numeric'}),
43
+ );
44
+
45
+ /**
46
+ * Compact number formatter (e.g. 1200 → "1.2K", 1500000 → "1.5M", 1e12 → "1T").
47
+ *
48
+ * Non-numeric values (e.g. category labels) and non-finite numbers pass through
49
+ * unchanged, so it is safe to use as a general-purpose tick formatter.
50
+ */
51
+ export function compactNumber(value: unknown): string {
52
+ const n = Number(value);
53
+ if (!Number.isFinite(n)) {
54
+ return String(value);
55
+ }
56
+ return getCompactFormat().format(n);
57
+ }
58
+
59
+ /**
60
+ * Currency formatter factory. Returns a tick format function.
61
+ *
62
+ * The symbol prefixes the magnitude and the sign leads the symbol, so negatives
63
+ * read as "-$1.5K" rather than "$-1.5K". Large values use compact notation.
64
+ *
65
+ * @example
66
+ * ```
67
+ * <ChartAxis tickFormat={currency()} /> // $1.5K
68
+ * <ChartAxis tickFormat={currency('€')} /> // €1.5K
69
+ * <ChartAxis tickFormat={currency('¥')} /> // ¥1.5K
70
+ * ```
71
+ */
72
+ export function currency(symbol = '$'): (value: unknown) => string {
73
+ return (value: unknown) => {
74
+ const n = Number(value);
75
+ if (!Number.isFinite(n)) {
76
+ return String(value);
77
+ }
78
+ const sign = n < 0 ? '-' : '';
79
+ const abs = Math.abs(n);
80
+ const body =
81
+ abs >= 1000 ? getCompactFormat().format(abs) : abs.toLocaleString();
82
+ return `${sign}${symbol}${body}`;
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Percent formatter (e.g. 0.45 → "45%", 1.2 → "120%", 0.005 → "0.5%").
88
+ *
89
+ * The input is a ratio; it is multiplied by 100 and localized. Whole percents
90
+ * render without a decimal, and up to one fractional digit is shown otherwise.
91
+ */
92
+ export function percent(value: unknown): string {
93
+ const n = Number(value);
94
+ if (!Number.isFinite(n)) {
95
+ return String(value);
96
+ }
97
+ return getPercentFormat().format(n);
98
+ }
99
+
100
+ /**
101
+ * Coerce a value into a Date.
102
+ *
103
+ * Accepts a `Date`, epoch milliseconds (number or numeric string), or a date
104
+ * string (ISO 8601, RFC 2822, …). Returns an invalid Date for anything else so
105
+ * callers can pass the original value through unchanged.
106
+ *
107
+ * Date-only ISO strings (`YYYY-MM-DD`) are constructed at local midnight rather
108
+ * than the spec's UTC midnight, so the calendar day survives local formatting
109
+ * in negative-offset timezones (`"2024-01-05"` stays "Jan 5", not "Jan 4").
110
+ */
111
+ function toDate(value: unknown): Date {
112
+ if (value instanceof Date) {
113
+ return value;
114
+ }
115
+ if (typeof value === 'number') {
116
+ return new Date(value);
117
+ }
118
+ if (typeof value === 'string') {
119
+ const s = value.trim();
120
+ if (s === '') {
121
+ return new Date(NaN);
122
+ }
123
+ const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
124
+ if (dateOnly) {
125
+ const year = Number(dateOnly[1]);
126
+ const month = Number(dateOnly[2]);
127
+ const day = Number(dateOnly[3]);
128
+ const local = new Date(year, month - 1, day);
129
+ // Reject out-of-range parts (e.g. "2024-02-31") that the Date constructor
130
+ // would otherwise silently roll into a different day; treat them as
131
+ // invalid so the raw value passes through unchanged.
132
+ if (
133
+ local.getFullYear() !== year ||
134
+ local.getMonth() !== month - 1 ||
135
+ local.getDate() !== day
136
+ ) {
137
+ return new Date(NaN);
138
+ }
139
+ return local;
140
+ }
141
+ const asNumber = Number(s);
142
+ return new Date(Number.isFinite(asNumber) ? asNumber : s);
143
+ }
144
+ return new Date(NaN);
145
+ }
146
+
147
+ /**
148
+ * Date formatter — short date (e.g. "Jan 5", "Mar 12").
149
+ */
150
+ export function shortDate(value: unknown): string {
151
+ const d = toDate(value);
152
+ if (Number.isNaN(d.getTime())) {
153
+ return String(value);
154
+ }
155
+ return getShortDateFormat().format(d);
156
+ }
157
+
158
+ /**
159
+ * Date formatter — month/year (e.g. "Jan 2024", "Mar 2025").
160
+ */
161
+ export function monthYear(value: unknown): string {
162
+ const d = toDate(value);
163
+ if (Number.isNaN(d.getTime())) {
164
+ return String(value);
165
+ }
166
+ return getMonthYearFormat().format(d);
167
+ }
@@ -0,0 +1,114 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {describe, expect, it} from 'vitest';
4
+ import {
5
+ getChartColors,
6
+ getChartColorsFromResolver,
7
+ type TokenResolver,
8
+ } from './getChartColors';
9
+ import {pharosTheme} from '../../themes/pharos/src/pharosTheme';
10
+
11
+ // A fake resolver with a hand-picked greyscale ramp so interpolated values are
12
+ // easy to verify by hand: 0x00, 0x40, 0x80, 0xC0, 0xFF from step 5 → 1.
13
+ const RAMP: Record<string, string> = {
14
+ '--color-data-gray-5': '#000000',
15
+ '--color-data-gray-4': '#404040',
16
+ '--color-data-gray-3': '#808080',
17
+ '--color-data-gray-2': '#C0C0C0',
18
+ '--color-data-gray-1': '#FFFFFF',
19
+ };
20
+
21
+ const resolve: TokenResolver = name => RAMP[name] ?? `resolved(${name})`;
22
+
23
+ describe('getChartColorsFromResolver', () => {
24
+ it('returns exact token stops when n <= ramp length', () => {
25
+ const colors = getChartColorsFromResolver(resolve);
26
+ expect(colors.sequential.gray(5)).toEqual([
27
+ '#000000',
28
+ '#404040',
29
+ '#808080',
30
+ '#C0C0C0',
31
+ '#FFFFFF',
32
+ ]);
33
+ });
34
+
35
+ it('interpolates between stops when n > ramp length', () => {
36
+ const colors = getChartColorsFromResolver(resolve);
37
+ // n=9 over 5 stops: odd indices land halfway between adjacent stops.
38
+ expect(colors.sequential.gray(9)).toEqual([
39
+ '#000000',
40
+ '#202020',
41
+ '#404040',
42
+ '#606060',
43
+ '#808080',
44
+ '#A0A0A0',
45
+ '#C0C0C0',
46
+ '#E0E0E0',
47
+ '#FFFFFF',
48
+ ]);
49
+ });
50
+
51
+ it('falls back to the nearer endpoint when interpolating non-hex stops', () => {
52
+ const colors = getChartColorsFromResolver(name =>
53
+ name === '--color-data-gray-5' ? 'var(--custom)' : (RAMP[name] ?? ''),
54
+ );
55
+ const nine = colors.sequential.gray(9);
56
+ // The unparseable first stop is preserved verbatim at its slot and as the
57
+ // nearer endpoint of the first interpolated position.
58
+ expect(nine[0]).toBe('var(--custom)');
59
+ expect(nine[1]).toBe('#404040');
60
+ });
61
+
62
+ describe('alpha', () => {
63
+ it('applies opacity to a hex color', () => {
64
+ const colors = getChartColorsFromResolver(resolve);
65
+ expect(colors.alpha('#0064E0', 0.2)).toBe('rgba(0, 100, 224, 0.2)');
66
+ });
67
+
68
+ it('applies opacity to bare hex without the leading # (pre-#3739 behavior)', () => {
69
+ const colors = getChartColorsFromResolver(resolve);
70
+ expect(colors.alpha('0064E0', 0.2)).toBe('rgba(0, 100, 224, 0.2)');
71
+ });
72
+
73
+ it('applies opacity to rgba() input, overriding its alpha', () => {
74
+ const colors = getChartColorsFromResolver(resolve);
75
+ expect(colors.alpha('rgba(0, 100, 224, 0.9)', 0.2)).toBe(
76
+ 'rgba(0, 100, 224, 0.2)',
77
+ );
78
+ });
79
+
80
+ it('clamps out-of-range opacity and defaults non-finite opacity to 1', () => {
81
+ const colors = getChartColorsFromResolver(resolve);
82
+ expect(colors.alpha('#000000', 2)).toBe('#000000');
83
+ expect(colors.alpha('#000000', -1)).toBe('rgba(0, 0, 0, 0)');
84
+ expect(colors.alpha('#000000', NaN)).toBe('#000000');
85
+ });
86
+
87
+ it('preserves unparseable input unchanged', () => {
88
+ const colors = getChartColorsFromResolver(resolve);
89
+ expect(colors.alpha('var(--color-accent)', 0.5)).toBe(
90
+ 'var(--color-accent)',
91
+ );
92
+ });
93
+ });
94
+ });
95
+
96
+ describe('getChartColors with the Pharos theme', () => {
97
+ // Brand order: azul, roxo, laranja, amarelo, chumbo. Dark mode lifts azul,
98
+ // roxo and chumbo for contrast on the dark surfaces.
99
+ it.each([
100
+ ['light', ['#034694', '#7C51A1', '#F26641', '#FAA61A', '#383838']],
101
+ ['dark', ['#6BA3F5', '#A67BC4', '#F26641', '#FAA61A', '#D4D4D4']],
102
+ ] as const)('follows the brand categorical order (%s)', (mode, expected) => {
103
+ const colors = getChartColors(pharosTheme, mode);
104
+ expect(colors.categorical(5).map(c => c.toUpperCase())).toEqual(expected);
105
+ });
106
+
107
+ it('only reaches the core extra slots after the five brand series', () => {
108
+ const light = getChartColors(pharosTheme, 'light').categorical(6);
109
+ expect(light[5]).not.toBe(light[0]);
110
+ expect(light.slice(0, 5).map(c => c.toUpperCase())).not.toContain(
111
+ light[5].toUpperCase(),
112
+ );
113
+ });
114
+ });
@@ -0,0 +1,282 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file getChartColors.ts
5
+ * @output Pure function to resolve chart colors from a theme — no React required
6
+ * @position Foundation utility; consumed by useChartColors and non-React contexts
7
+ *
8
+ * Takes a token resolver function (or a defined theme + mode) and returns
9
+ * the full chart colors API. The useChartColors hook wraps this.
10
+ *
11
+ * Palettes degrade gracefully for any count: the categorical scale wraps
12
+ * around its slots, and the sequential/diverging scales pick hand-tuned token
13
+ * stops when enough exist and interpolate across the ramp when more are asked
14
+ * for. The theme entry point resolves through the canonical token resolver, so
15
+ * data tokens fall back to their defaults even when a theme doesn't override
16
+ * them.
17
+ */
18
+
19
+ import {resolveThemeTokens, type DefinedTheme} from '@pharos-ds/core/theme';
20
+ import {
21
+ parseHex,
22
+ parseColor,
23
+ formatHex,
24
+ formatColor,
25
+ } from '@pharos-ds/core/utils';
26
+
27
+ // =============================================================================
28
+ // Types
29
+ // =============================================================================
30
+
31
+ /** Hue names available in the sequential ramps */
32
+ export type SequentialHue =
33
+ | 'blue'
34
+ | 'shamrock'
35
+ | 'orange'
36
+ | 'pink'
37
+ | 'purple'
38
+ | 'red'
39
+ | 'teal'
40
+ | 'yellow'
41
+ | 'gray';
42
+
43
+ /** The chart colors API returned by both getChartColors and useChartColors */
44
+ export interface ChartColorsAPI {
45
+ categorical(n: number): string[];
46
+ sequential: Record<SequentialHue, (n: number) => string[]>;
47
+ diverging: {
48
+ positiveNegative(n: number): string[];
49
+ coldHot(n: number): string[];
50
+ custom(
51
+ neg: SequentialHue,
52
+ pos: SequentialHue,
53
+ n: number,
54
+ midpoint?: string,
55
+ ): string[];
56
+ };
57
+ semantic: {
58
+ positive: string;
59
+ negative: string;
60
+ warning: string;
61
+ neutral: string;
62
+ };
63
+ /** Structural colors for chart chrome — axes, grid, ticks, labels */
64
+ structural: {
65
+ axis: string;
66
+ grid: string;
67
+ tick: string;
68
+ label: string;
69
+ };
70
+ alpha(hex: string, opacity: number): string;
71
+ }
72
+
73
+ /** A function that resolves a token name to its current value */
74
+ export type TokenResolver = (name: string) => string;
75
+
76
+ // =============================================================================
77
+ // Internals
78
+ // =============================================================================
79
+
80
+ const CATEGORICAL_TOKENS = [
81
+ '--color-data-categorical-blue',
82
+ '--color-data-categorical-purple',
83
+ '--color-data-categorical-orange',
84
+ '--color-data-categorical-yellow',
85
+ '--color-data-categorical-gray',
86
+ '--color-data-categorical-green',
87
+ '--color-data-categorical-pink',
88
+ '--color-data-categorical-cyan',
89
+ '--color-data-categorical-red',
90
+ '--color-data-categorical-teal',
91
+ '--color-data-categorical-brown',
92
+ '--color-data-categorical-indigo',
93
+ ] as const;
94
+
95
+ const SEQUENTIAL_HUES: SequentialHue[] = [
96
+ 'blue',
97
+ 'shamrock',
98
+ 'orange',
99
+ 'pink',
100
+ 'purple',
101
+ 'red',
102
+ 'teal',
103
+ 'yellow',
104
+ 'gray',
105
+ ];
106
+
107
+ /** Interpolate between two hex colors in sRGB. Non-hex input falls back to the nearer endpoint. */
108
+ function lerpHex(a: string, b: string, t: number): string {
109
+ const ca = parseHex(a);
110
+ const cb = parseHex(b);
111
+ if (ca === null || cb === null) {
112
+ return t < 0.5 ? a : b;
113
+ }
114
+ const mix = (x: number, y: number): number => x + (y - x) * t;
115
+ return formatHex(mix(ca.r, cb.r), mix(ca.g, cb.g), mix(ca.b, cb.b));
116
+ }
117
+
118
+ /**
119
+ * Sample `n` colors from an ordered ramp of stops.
120
+ * - `n <= stops.length` → picks the nearest exact stops, preserving the
121
+ * hand-tuned design-token values.
122
+ * - `n > stops.length` → interpolates across the ramp so any count is supported
123
+ * instead of silently capping at the number of stops.
124
+ * - `n === 1` → the middle stop, a single representative tone.
125
+ */
126
+ function sampleRamp(stops: string[], n: number): string[] {
127
+ const len = stops.length;
128
+ // A non-finite count (e.g. Infinity) would make the Array.from calls below
129
+ // attempt a ~2**53 element allocation and hang, so treat it as empty.
130
+ if (!Number.isFinite(n) || n <= 0 || len === 0) {
131
+ return [];
132
+ }
133
+ if (n === 1) {
134
+ return [stops[Math.floor((len - 1) / 2)]];
135
+ }
136
+ if (len === 1) {
137
+ return Array.from({length: n}, () => stops[0]);
138
+ }
139
+ if (n <= len) {
140
+ return Array.from(
141
+ {length: n},
142
+ (_, i) => stops[Math.round((i * (len - 1)) / (n - 1))],
143
+ );
144
+ }
145
+ return Array.from({length: n}, (_, i) => {
146
+ const t = (i * (len - 1)) / (n - 1);
147
+ const lo = Math.floor(t);
148
+ const frac = t - lo;
149
+ return frac === 0 ? stops[lo] : lerpHex(stops[lo], stops[lo + 1], frac);
150
+ });
151
+ }
152
+
153
+ /**
154
+ * Apply an opacity to a concrete CSS color (hex — with or without the leading
155
+ * `#` — `rgb()`/`rgba()`, named). The `opacity` argument always wins over any
156
+ * alpha already in the color. Unparseable input (e.g. `var()`) is returned
157
+ * unchanged.
158
+ */
159
+ function hexAlpha(hex: string, opacity: number): string {
160
+ // parseColor requires the `#` for hex; fall back to parseHex so bare hex
161
+ // strings (`0064E0`) keep working as they did before #3739.
162
+ const rgba = parseColor(hex) ?? parseHex(hex);
163
+ if (rgba === null) {
164
+ return hex;
165
+ }
166
+ const a = Number.isFinite(opacity) ? Math.max(0, Math.min(1, opacity)) : 1;
167
+ return formatColor({...rgba, a});
168
+ }
169
+
170
+ // =============================================================================
171
+ // Public API
172
+ // =============================================================================
173
+
174
+ /**
175
+ * Build chart colors from a token resolver function.
176
+ * This is the lowest-level API — pass any function that maps token names to values.
177
+ */
178
+ export function getChartColorsFromResolver(
179
+ resolve: TokenResolver,
180
+ ): ChartColorsAPI {
181
+ const categorical = CATEGORICAL_TOKENS.map(t => resolve(t));
182
+
183
+ const ramp = (hue: SequentialHue): string[] =>
184
+ [5, 4, 3, 2, 1].map(step => resolve(`--color-data-${hue}-${step}`));
185
+
186
+ const gray1 = resolve('--color-data-gray-1');
187
+
188
+ function buildDiverging(
189
+ negHue: SequentialHue,
190
+ posHue: SequentialHue,
191
+ n: number,
192
+ midpoint: string = gray1,
193
+ ): string[] {
194
+ if (n <= 0) {
195
+ return [];
196
+ }
197
+ if (n === 1) {
198
+ return [midpoint];
199
+ }
200
+ const half = Math.floor(n / 2);
201
+ const hasCenter = n % 2 === 1;
202
+ const negSide = sampleRamp(ramp(negHue), half);
203
+ const posSide = sampleRamp(ramp(posHue), half).reverse();
204
+ if (hasCenter) {
205
+ return [...negSide, midpoint, ...posSide];
206
+ }
207
+ return [...negSide, ...posSide];
208
+ }
209
+
210
+ return {
211
+ categorical(n: number): string[] {
212
+ // A non-finite count (e.g. Infinity) would make the wrap-around Array.from
213
+ // below attempt a ~2**53 element allocation and hang, so treat it as empty.
214
+ if (!Number.isFinite(n) || n <= 0 || categorical.length === 0) {
215
+ return [];
216
+ }
217
+ // Wrap around the palette so any series count gets a deterministic color.
218
+ return Array.from(
219
+ {length: n},
220
+ (_, i) => categorical[i % categorical.length],
221
+ );
222
+ },
223
+
224
+ sequential: Object.fromEntries(
225
+ SEQUENTIAL_HUES.map(hue => [
226
+ hue,
227
+ (n: number): string[] => sampleRamp(ramp(hue), n),
228
+ ]),
229
+ ) as Record<SequentialHue, (n: number) => string[]>,
230
+
231
+ diverging: {
232
+ positiveNegative: (n: number) => buildDiverging('shamrock', 'red', n),
233
+ coldHot: (n: number) => buildDiverging('blue', 'red', n),
234
+ custom: (
235
+ neg: SequentialHue,
236
+ pos: SequentialHue,
237
+ n: number,
238
+ mid?: string,
239
+ ) => buildDiverging(neg, pos, n, mid),
240
+ },
241
+
242
+ semantic: {
243
+ positive: resolve('--color-data-categorical-green'),
244
+ negative: resolve('--color-data-categorical-red'),
245
+ warning: resolve('--color-data-categorical-orange'),
246
+ neutral: resolve('--color-data-neutral'),
247
+ },
248
+
249
+ structural: {
250
+ axis: resolve('--color-border-emphasized'),
251
+ grid: resolve('--color-border'),
252
+ tick: resolve('--color-border-emphasized'),
253
+ label: resolve('--color-text-secondary'),
254
+ },
255
+
256
+ alpha: hexAlpha,
257
+ };
258
+ }
259
+
260
+ /**
261
+ * Build chart colors from a defined theme and color mode.
262
+ *
263
+ * Use this in non-React contexts — WebGL setup, SSR, tests, Node scripts.
264
+ *
265
+ * Resolves through the canonical theme token resolver, so data tokens fall back
266
+ * to their defaults when the theme doesn't override them, and both
267
+ * `light-dark()` strings and `[light, dark]` tuples are handled for `mode`.
268
+ *
269
+ * @example
270
+ * ```
271
+ * import { neutralTheme } from '@pharos-ds/theme-neutral';
272
+ * const colors = getChartColors(neutralTheme, 'light');
273
+ * colors.categorical(5)
274
+ * ```
275
+ */
276
+ export function getChartColors(
277
+ theme: DefinedTheme,
278
+ mode: 'light' | 'dark' = 'light',
279
+ ): ChartColorsAPI {
280
+ const tokens = resolveThemeTokens(theme, {mode});
281
+ return getChartColorsFromResolver(name => tokens[name] ?? '');
282
+ }
package/src/index.ts ADDED
@@ -0,0 +1,88 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ export {Chart, type ChartProps} from './Chart';
4
+ export {ChartAxis, type ChartAxisProps} from './ChartAxis';
5
+ export {ChartGrid, type ChartGridProps} from './ChartGrid';
6
+ export {
7
+ ChartLegend,
8
+ type ChartLegendProps,
9
+ type LegendPosition,
10
+ type LegendAlignment,
11
+ } from './ChartLegend';
12
+ export {type LegendItem} from './legend';
13
+ export {
14
+ ChartSwatch,
15
+ swatchVariantForType,
16
+ type ChartSwatchProps,
17
+ type ChartSwatchVariant,
18
+ } from './ChartSwatch';
19
+ export {
20
+ ChartTooltip,
21
+ type ChartTooltipProps,
22
+ type ChartTooltipPlacement,
23
+ } from './ChartTooltip';
24
+ export {type TooltipSeriesValue} from './tooltip';
25
+ export {
26
+ bar,
27
+ line,
28
+ dot,
29
+ area,
30
+ band,
31
+ candlestick,
32
+ errorBar,
33
+ referenceLine,
34
+ dotGL,
35
+ dotGLInteractive,
36
+ heatmapGL,
37
+ streamGL,
38
+ } from './marks';
39
+ export type {
40
+ BarOptions,
41
+ LineOptions,
42
+ DotOptions,
43
+ AreaOptions,
44
+ BandOptions,
45
+ CandlestickOptions,
46
+ ErrorBarOptions,
47
+ ReferenceLineOptions,
48
+ DotGLOptions,
49
+ DotGLInteractiveOptions,
50
+ HeatmapGLOptions,
51
+ StreamGLOptions,
52
+ StreamGLHandle,
53
+ } from './marks';
54
+ export {useChart} from './ChartContext';
55
+ export type {
56
+ SeriesDef,
57
+ ChartContext,
58
+ ChartPointerEvent,
59
+ ResolvedPoint,
60
+ ResolvedPositions,
61
+ SeriesContext,
62
+ ChartMargin,
63
+ ChartScale,
64
+ YBaseline,
65
+ ColorAccessor,
66
+ } from './types';
67
+
68
+ // Color palette — theme-aware data-visualization colors
69
+ export {useChartColors} from './useChartColors';
70
+ export {
71
+ getChartColors,
72
+ getChartColorsFromResolver,
73
+ type ChartColorsAPI,
74
+ type SequentialHue,
75
+ type TokenResolver,
76
+ } from './getChartColors';
77
+
78
+ // Tick / value formatters
79
+ export {
80
+ compactNumber,
81
+ currency,
82
+ percent,
83
+ shortDate,
84
+ monthYear,
85
+ } from './formatters';
86
+
87
+ // Scale utilities
88
+ export {isBandScale, xPixel} from './utils';