panelui-native 0.60.0 → 0.62.0

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 (37) hide show
  1. package/README.md +3 -1
  2. package/lib/module/components/area-chart/index.js +86 -1
  3. package/lib/module/components/area-chart/index.js.map +1 -1
  4. package/lib/module/components/bar-chart/index.js +112 -2
  5. package/lib/module/components/bar-chart/index.js.map +1 -1
  6. package/lib/module/components/live-line-chart/index.js +1063 -0
  7. package/lib/module/components/live-line-chart/index.js.map +1 -0
  8. package/lib/module/components/polar-area-chart/index.js +719 -0
  9. package/lib/module/components/polar-area-chart/index.js.map +1 -0
  10. package/lib/module/components/treemap-chart/index.js +957 -0
  11. package/lib/module/components/treemap-chart/index.js.map +1 -0
  12. package/lib/module/index.js +3 -0
  13. package/lib/module/index.js.map +1 -1
  14. package/lib/module/utils/chart.js +171 -4
  15. package/lib/module/utils/chart.js.map +1 -1
  16. package/lib/typescript/src/components/area-chart/index.d.ts +28 -1
  17. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
  18. package/lib/typescript/src/components/bar-chart/index.d.ts +33 -3
  19. package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -1
  20. package/lib/typescript/src/components/live-line-chart/index.d.ts +293 -0
  21. package/lib/typescript/src/components/live-line-chart/index.d.ts.map +1 -0
  22. package/lib/typescript/src/components/polar-area-chart/index.d.ts +259 -0
  23. package/lib/typescript/src/components/polar-area-chart/index.d.ts.map +1 -0
  24. package/lib/typescript/src/components/treemap-chart/index.d.ts +317 -0
  25. package/lib/typescript/src/components/treemap-chart/index.d.ts.map +1 -0
  26. package/lib/typescript/src/index.d.ts +3 -0
  27. package/lib/typescript/src/index.d.ts.map +1 -1
  28. package/lib/typescript/src/utils/chart.d.ts +27 -1
  29. package/lib/typescript/src/utils/chart.d.ts.map +1 -1
  30. package/package.json +1 -1
  31. package/src/components/area-chart/index.tsx +86 -1
  32. package/src/components/bar-chart/index.tsx +115 -4
  33. package/src/components/live-line-chart/index.tsx +1294 -0
  34. package/src/components/polar-area-chart/index.tsx +928 -0
  35. package/src/components/treemap-chart/index.tsx +1175 -0
  36. package/src/index.ts +52 -0
  37. package/src/utils/chart.ts +202 -4
package/src/index.ts CHANGED
@@ -556,6 +556,41 @@ export {
556
556
  type PieChartStatus,
557
557
  type PieDatum,
558
558
  } from './components/pie-chart';
559
+ export {
560
+ LiveLineChart,
561
+ useLiveLineChart,
562
+ type LiveLineChartProps,
563
+ type LiveLineChartHandle,
564
+ type LiveLineChartHeaderProps,
565
+ type LiveLineChartGridProps,
566
+ type LiveLineChartAreaProps,
567
+ type LiveLineChartLineProps,
568
+ type LiveLineChartTipProps,
569
+ type LiveLineChartXAxisProps,
570
+ type LiveLineChartYAxisProps,
571
+ type LiveLineChartTooltipProps,
572
+ type LiveLineChartSkeletonProps,
573
+ type LiveLineChartStatus,
574
+ type LiveLineMomentum,
575
+ type LiveLineMomentumColors,
576
+ type LiveLinePoint,
577
+ } from './components/live-line-chart';
578
+ export {
579
+ PolarAreaChart,
580
+ usePolarAreaChart,
581
+ type PolarAreaChartProps,
582
+ type PolarAreaChartHandle,
583
+ type PolarAreaChartHeaderProps,
584
+ type PolarAreaChartGridProps,
585
+ type PolarAreaChartWedgesProps,
586
+ type PolarAreaChartLabelsProps,
587
+ type PolarAreaChartTooltipProps,
588
+ type PolarAreaChartLegendProps,
589
+ type PolarAreaChartSkeletonProps,
590
+ type PolarAreaChartStatus,
591
+ type PolarAreaScale,
592
+ type PolarAreaDatum,
593
+ } from './components/polar-area-chart';
559
594
  export { Textarea, type TextareaProps } from './components/textarea';
560
595
  export {
561
596
  TagInput,
@@ -956,6 +991,23 @@ export {
956
991
  type ToastIndicatorProps,
957
992
  type ToastCloseProps,
958
993
  } from './components/toast';
994
+ export {
995
+ TreemapChart,
996
+ useTreemapChart,
997
+ squarifyLayout,
998
+ type TreemapChartProps,
999
+ type TreemapChartHandle,
1000
+ type TreemapChartStatus,
1001
+ type TreemapDatum,
1002
+ type TreemapTile,
1003
+ type TreemapRect,
1004
+ type TreemapChartTilesProps,
1005
+ type TreemapChartLabelsProps,
1006
+ type TreemapChartTooltipProps,
1007
+ type TreemapChartLegendProps,
1008
+ type TreemapChartHeaderProps,
1009
+ type TreemapChartSkeletonProps,
1010
+ } from './components/treemap-chart';
959
1011
  export {
960
1012
  Tree,
961
1013
  type TreeProps,
@@ -36,12 +36,210 @@ export function useSeriesColor(
36
36
  return explicit ?? (typeof token === 'string' ? token : FALLBACK_SERIES[index - 1]!);
37
37
  }
38
38
 
39
- /** `12.4k` rather than `12400` — a readout has one line to say it in. */
39
+ /**
40
+ * The `r, g, b` of a colour string, or `null` for one that cannot be read.
41
+ *
42
+ * Hex and `rgb()` / `rgba()` only, which is every form a theme token and a
43
+ * series colour take. A `null` is the caller's signal to keep whatever it
44
+ * would have used anyway, rather than a guess that could be the wrong end of
45
+ * the scale.
46
+ */
47
+ function channelsOf(color: string): [number, number, number] | null {
48
+ const value = color.trim().toLowerCase();
49
+
50
+ if (value.startsWith('#')) {
51
+ const body =
52
+ value.length === 4 || value.length === 5
53
+ ? value
54
+ .slice(1, 4)
55
+ .split('')
56
+ .map((digit) => digit + digit)
57
+ .join('')
58
+ : value.slice(1, 7);
59
+ if (body.length !== 6 || /[^0-9a-f]/.test(body)) return null;
60
+ return [
61
+ parseInt(body.slice(0, 2), 16),
62
+ parseInt(body.slice(2, 4), 16),
63
+ parseInt(body.slice(4, 6), 16),
64
+ ];
65
+ }
66
+
67
+ const fn = /^rgba?\(([^)]+)\)$/.exec(value);
68
+ if (!fn) return null;
69
+
70
+ const parts = fn[1]!.split(/[\s,/]+/).filter(Boolean);
71
+ if (parts.length < 3) return null;
72
+
73
+ const channels = parts.slice(0, 3).map((raw) => {
74
+ const n = parseFloat(raw);
75
+ if (Number.isNaN(n)) return null;
76
+ return raw.endsWith('%') ? (n / 100) * 255 : n;
77
+ });
78
+
79
+ return channels.some((c) => c === null)
80
+ ? null
81
+ : (channels as [number, number, number]);
82
+ }
83
+
84
+ /** One channel, undone from the sRGB transfer curve. */
85
+ function linear(value: number): number {
86
+ const c = Math.min(1, Math.max(0, value / 255));
87
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
88
+ }
89
+
90
+ /** Relative luminance, 0 for black and 1 for white. */
91
+ function luminanceOf(rgb: [number, number, number]): number {
92
+ return 0.2126 * linear(rgb[0]) + 0.7152 * linear(rgb[1]) + 0.0722 * linear(rgb[2]);
93
+ }
94
+
95
+ /** Text laid over a fill: the solid colour and the softer one under it. */
96
+ export interface ChartInk {
97
+ color: string;
98
+ muted: string;
99
+ }
100
+
101
+ const LIGHT_INK: ChartInk = { color: '#ffffff', muted: 'rgba(255,255,255,0.82)' };
102
+ const DARK_INK: ChartInk = { color: '#0a0a0a', muted: 'rgba(10,10,10,0.82)' };
103
+
104
+ /**
105
+ * The contrast ratio white keeps on a fill before the label is turned over.
106
+ *
107
+ * 3:1, which is the bar for bold text at this size. Higher and the mid-tone
108
+ * hues every theme uses for its second series onwards flip to dark text as
109
+ * well, which is a different chart rather than a legible one; lower and the
110
+ * pale fills this exists for stay unreadable.
111
+ */
112
+ const INK_FLOOR = 3;
113
+
114
+ /**
115
+ * The text colour to lay over `fill`: white while white can be read on it,
116
+ * near-black once it cannot.
117
+ *
118
+ * Neither the fill nor the theme can decide this on its own. A series colour is
119
+ * a colour, not a mode — a theme is free to make it near-white, and a fixed
120
+ * white label on it disappears while the foreground token would be just as
121
+ * wrong on the next tile along.
122
+ *
123
+ * White is kept wherever it holds up, rather than always taking whichever ink
124
+ * scores higher. Charts are drawn white-on-hue, and turning over every tile
125
+ * that a darker label would technically suit changes what the chart looks like
126
+ * to fix the two that were unreadable.
127
+ *
128
+ * `behind` and `opacity` are for a fill drawn part-transparent: what the reader
129
+ * sees is the blend, and a half-strength tile on a pale ground is lighter than
130
+ * the colour it was given. Compositing happens in gamma space, the way the
131
+ * renderer does it, before the luminance is taken.
132
+ */
133
+ /** A colour moved `amount` of the way towards white, or towards black when negative. */
134
+ function tone(color: string, amount: number): string {
135
+ const rgb = channelsOf(color);
136
+ if (!rgb) return color;
137
+
138
+ const target = amount >= 0 ? 255 : 0;
139
+ const t = Math.min(Math.abs(amount), 0.85);
140
+ const [r, g, b] = rgb.map((c) => Math.round(c + (target - c) * t));
141
+ return `rgb(${r}, ${g}, ${b})`;
142
+ }
143
+
144
+ /**
145
+ * The colour for the `index`th thing in a chart that draws an unbounded number
146
+ * of them.
147
+ *
148
+ * There are five series tokens, and a chart is routinely asked to draw six
149
+ * things. Taking the palette modulo its length hands two of them the identical
150
+ * colour, which in a chart whose whole job is telling parts apart is a bug the
151
+ * theme decides the severity of — on a theme whose first token is green, the
152
+ * sixth wedge is a second green one.
153
+ *
154
+ * So each further lap through the palette is drawn a step further along a tone
155
+ * run, keeping the theme's own hues rather than inventing new ones.
156
+ *
157
+ * The run goes *away* from whichever end the colour already sits near, and
158
+ * always in that one direction. Alternating lighter and darker would be the
159
+ * obvious thing and it reintroduces the bug on the themes that matter: a token
160
+ * of `#fafafa` lightened is `#fafafa` back again, so the second lap of a
161
+ * near-white palette entry is the same colour it was trying not to be.
162
+ */
163
+ export function seriesColorAt(palette: string[], index: number): string {
164
+ if (!palette.length) return FALLBACK_SERIES[0]!;
165
+
166
+ const base = palette[index % palette.length]!;
167
+ const lap = Math.floor(index / palette.length);
168
+ if (lap <= 0) return base;
169
+
170
+ const rgb = channelsOf(base);
171
+ const away = rgb && luminanceOf(rgb) > 0.4 ? -1 : 1;
172
+ // Each lap covers two thirds of what is left, so every one lands somewhere
173
+ // the last did not.
174
+ return tone(base, away * (1 - 0.66 ** lap));
175
+ }
176
+
177
+ export function inkOn(fill: string, behind?: string, opacity = 1): ChartInk {
178
+ const front = channelsOf(fill);
179
+ if (!front) return LIGHT_INK;
180
+
181
+ const alpha = Math.min(1, Math.max(0, opacity));
182
+ const ground = alpha < 1 && behind ? channelsOf(behind) : null;
183
+ const seen: [number, number, number] = ground
184
+ ? [
185
+ alpha * front[0] + (1 - alpha) * ground[0],
186
+ alpha * front[1] + (1 - alpha) * ground[1],
187
+ alpha * front[2] + (1 - alpha) * ground[2],
188
+ ]
189
+ : front;
190
+
191
+ return 1.05 / (luminanceOf(seen) + 0.05) >= INK_FLOOR ? LIGHT_INK : DARK_INK;
192
+ }
193
+
194
+ /** The suffixes, smallest first. Each one is a thousand of the one before it. */
195
+ const MAGNITUDES = ['k', 'M', 'B', 'T'] as const;
196
+
197
+ /** Drop the zeros a fixed decimal count left behind: `12.0` is `12`. */
198
+ function trimZeros(text: string): string {
199
+ return text.includes('.') ? text.replace(/\.?0+$/, '') : text;
200
+ }
201
+
202
+ /**
203
+ * One decimal while the mantissa is a single digit, none after that.
204
+ *
205
+ * `1.2k` and `12k` rather than `1.2k` and `12.4k`: the second decimal place is
206
+ * spurious precision on a tick, and the label has one line to fit in.
207
+ */
208
+ function mantissa(value: number): string {
209
+ return trimZeros(value.toFixed(Math.abs(value) < 10 ? 1 : 0));
210
+ }
211
+
212
+ /** `12k` rather than `12400` — a readout has one line to say it in. */
40
213
  export function compactNumber(value: number): string {
214
+ if (!Number.isFinite(value)) return String(value);
215
+
41
216
  const abs = Math.abs(value);
42
- if (abs >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
43
- if (abs >= 1_000) return `${(value / 1_000).toFixed(1)}k`;
44
- return Number.isInteger(value) ? String(value) : value.toFixed(1);
217
+ if (abs < 1_000) {
218
+ if (Number.isInteger(value)) return String(value);
219
+ // Below one, a single decimal rounds most values to `0`, which reads as
220
+ // nothing rather than as a small number.
221
+ return trimZeros(value.toFixed(abs < 1 ? 2 : 1));
222
+ }
223
+
224
+ let index = 0;
225
+ let scaled = value / 1_000;
226
+ while (Math.abs(scaled) >= 1_000 && index < MAGNITUDES.length - 1) {
227
+ scaled /= 1_000;
228
+ index += 1;
229
+ }
230
+
231
+ /*
232
+ * Rounding can push a mantissa over a thousand — 999,999 is `1000.0k` on the
233
+ * way in, which is a thousand thousands written the long way. Formatting
234
+ * first and promoting after is what makes it `1M`.
235
+ */
236
+ let text = mantissa(scaled);
237
+ if (Math.abs(Number(text)) >= 1_000 && index < MAGNITUDES.length - 1) {
238
+ index += 1;
239
+ text = mantissa(scaled / 1_000);
240
+ }
241
+
242
+ return `${text}${MAGNITUDES[index]}`;
45
243
  }
46
244
 
47
245
  /** The drawable box inside a chart, after its padding is taken off. */