@siyahi/ui 0.1.9-next.24 → 0.1.9-next.25

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 (45) hide show
  1. package/agents/examples/change-grid-money.tsx +36 -0
  2. package/agents/examples/change-grid.tsx +42 -0
  3. package/agents/examples/drawdown-chart.tsx +35 -0
  4. package/agents/examples/heatmap-calendar-signed.tsx +67 -0
  5. package/agents/examples/heatmap-status.tsx +6 -4
  6. package/agents/examples/histogram-chart.tsx +28 -0
  7. package/agents/examples/scatter-chart-quadrants.tsx +63 -0
  8. package/agents/examples/scatter-chart.tsx +75 -0
  9. package/agents/examples/slope-chart-market.tsx +33 -0
  10. package/agents/examples/slope-chart.tsx +26 -0
  11. package/agents/examples/waterfall-chart-stores.tsx +27 -0
  12. package/agents/examples/waterfall-chart.tsx +37 -0
  13. package/agents/manifest.json +160 -0
  14. package/dist/components/registry/heatmap/calendar-heatmap.d.ts +8 -2
  15. package/dist/components/registry/heatmap/calendar-heatmap.js +96 -18
  16. package/dist/components/registry/heatmap/status-heatmap.js +86 -47
  17. package/dist/components/siyahi/change-grid.d.ts +31 -0
  18. package/dist/components/siyahi/change-grid.js +74 -0
  19. package/dist/components/siyahi/drawdown-chart.d.ts +34 -0
  20. package/dist/components/siyahi/drawdown-chart.js +102 -0
  21. package/dist/components/siyahi/histogram-chart.d.ts +57 -0
  22. package/dist/components/siyahi/histogram-chart.js +134 -0
  23. package/dist/components/siyahi/scatter-chart.d.ts +59 -0
  24. package/dist/components/siyahi/scatter-chart.js +367 -0
  25. package/dist/components/siyahi/slope-chart.d.ts +35 -0
  26. package/dist/components/siyahi/slope-chart.js +159 -0
  27. package/dist/components/siyahi/treemap.js +3 -7
  28. package/dist/components/siyahi/waterfall-chart.d.ts +51 -0
  29. package/dist/components/siyahi/waterfall-chart.js +147 -0
  30. package/dist/index.d.ts +9 -2
  31. package/dist/index.js +7 -1
  32. package/dist/lib/chart-frame.d.ts +7 -2
  33. package/dist/lib/chart-frame.js +44 -10
  34. package/dist/lib/chart-series.d.ts +2 -3
  35. package/dist/lib/fit-text.d.ts +6 -0
  36. package/dist/lib/fit-text.js +11 -0
  37. package/dist/lib/heat.d.ts +7 -0
  38. package/dist/lib/heat.js +14 -0
  39. package/dist/lib/scatter.d.ts +98 -0
  40. package/dist/lib/scatter.js +218 -0
  41. package/dist/lib/treemap.d.ts +0 -5
  42. package/dist/lib/treemap.js +1 -16
  43. package/package.json +1 -1
  44. package/report.css +9 -0
  45. package/styles/siyahi.css +16 -0
@@ -0,0 +1,218 @@
1
+ //#region src/lib/scatter.ts
2
+ /**
3
+ * Ticks for a log axis at 1, 2 and 5 times a power of ten, from the largest at or under min to the smallest at or
4
+ * over max, so the axis ends on a tick and every label reads as a round number.
5
+ */
6
+ function logTicks(min, max) {
7
+ if (!(min > 0) || !Number.isFinite(max) || !(max >= min)) return [];
8
+ const ticks = [];
9
+ for (let decade = Math.floor(Math.log10(min));; decade++) for (const step of [
10
+ 1,
11
+ 2,
12
+ 5
13
+ ]) {
14
+ const tick = Number((step * 10 ** decade).toPrecision(12));
15
+ if (Number(((step === 5 ? 10 : step === 2 ? 5 : 2) * 10 ** decade).toPrecision(12)) <= min) continue;
16
+ ticks.push(tick);
17
+ if (tick >= max) return ticks;
18
+ }
19
+ }
20
+ const clean = (value) => Number(value.toPrecision(12)) || 0;
21
+ /**
22
+ * Ticks for a linear axis that just covers min to max, about `count` of them, a step of 1, 2, 2.5 or 5 times a power
23
+ * of ten: −50% to +75% by 25s, never −50% to +150% because the step had to be 50. Values that differ only by float
24
+ * noise, such as 0.3 and 0.30000000000000004, count as one value and get that value as their only tick.
25
+ */
26
+ function linearTicks(min, max, count = 5) {
27
+ if (!Number.isFinite(min) || !Number.isFinite(max) || max < min) return [];
28
+ if (max - min <= Math.max(Math.abs(min), Math.abs(max)) * 1e-9) return [clean(min)];
29
+ const rough = (max - min) / (count - 1);
30
+ const power = 10 ** Math.floor(Math.log10(rough));
31
+ const step = ([
32
+ 1,
33
+ 2,
34
+ 2.5,
35
+ 5,
36
+ 10
37
+ ].find((m) => m * power >= rough * (1 - 1e-9)) ?? 10) * power;
38
+ const first = Math.floor(min / step + 1e-9);
39
+ const last = Math.ceil(max / step - 1e-9);
40
+ return Array.from({ length: last - first + 1 }, (_, i) => clean((first + i) * step));
41
+ }
42
+ const HALO = 2;
43
+ const GAP = 4;
44
+ const overlaps = (a, b) => a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
45
+ /**
46
+ * Names beside dots, in the candidates' order, up to `limit`: each tries the right of its dot, then the left, above,
47
+ * below and the four corners, and takes the first spot inside the plot that covers no other label, no other dot and
48
+ * nothing `reserved` (such as a quadrant's name). A name that finds no spot is left to the tooltip, so labels never sit
49
+ * on top of each other.
50
+ */
51
+ function placeLabels(candidates, plot, limit, { size, measure }, reserved = []) {
52
+ const line = size * 4 / 3;
53
+ const dots = candidates.map(({ key, cx, cy, r }) => ({
54
+ key,
55
+ box: {
56
+ x: cx - r,
57
+ y: cy - r,
58
+ width: r * 2,
59
+ height: r * 2
60
+ }
61
+ }));
62
+ const taken = [...reserved];
63
+ const placed = [];
64
+ for (const { key, text, cx, cy, r: radius } of candidates) {
65
+ if (placed.length >= limit) break;
66
+ const width = measure(text) + 4;
67
+ const spot = [
68
+ {
69
+ x: cx + radius + GAP,
70
+ y: cy - line / 2,
71
+ width,
72
+ height: line
73
+ },
74
+ {
75
+ x: cx - radius - GAP - width,
76
+ y: cy - line / 2,
77
+ width,
78
+ height: line
79
+ },
80
+ {
81
+ x: cx - width / 2,
82
+ y: cy - radius - GAP - line,
83
+ width,
84
+ height: line
85
+ },
86
+ {
87
+ x: cx - width / 2,
88
+ y: cy + radius + GAP,
89
+ width,
90
+ height: line
91
+ },
92
+ {
93
+ x: cx + radius,
94
+ y: cy - radius - line,
95
+ width,
96
+ height: line
97
+ },
98
+ {
99
+ x: cx - radius - width,
100
+ y: cy - radius - line,
101
+ width,
102
+ height: line
103
+ },
104
+ {
105
+ x: cx + radius,
106
+ y: cy + radius,
107
+ width,
108
+ height: line
109
+ },
110
+ {
111
+ x: cx - radius - width,
112
+ y: cy + radius,
113
+ width,
114
+ height: line
115
+ }
116
+ ].find((box) => box.x >= plot.x && box.y >= plot.y && box.x + box.width <= plot.x + plot.width && box.y + box.height <= plot.y + plot.height && !taken.some((other) => overlaps(box, other)) && !dots.some((dot) => dot.key !== key && overlaps(box, dot.box)));
117
+ if (!spot) continue;
118
+ taken.push(spot);
119
+ placed.push({
120
+ key,
121
+ text,
122
+ x: spot.x + HALO,
123
+ y: spot.y + size
124
+ });
125
+ }
126
+ return placed;
127
+ }
128
+ /** A split a log axis cannot place, at or below zero, or one that is not a number, is no split at all. */
129
+ function quadrantSplit(quadrants, xScale) {
130
+ if (!quadrants || !Number.isFinite(quadrants.x) || !Number.isFinite(quadrants.y)) return void 0;
131
+ return xScale === "linear" || quadrants.x > 0 ? quadrants : void 0;
132
+ }
133
+ /** The largest sized dot; the smallest stays big enough to see and to tap. */
134
+ const MAX_RADIUS = 20;
135
+ const MIN_RADIUS = 3;
136
+ /** A size that is not a positive number (NaN, Infinity, a negative) weighs nothing. */
137
+ const weight = (p) => p.size !== void 0 && Number.isFinite(p.size) && p.size > 0 ? p.size : 0;
138
+ /**
139
+ * Each dot's radius. Area, not radius, follows size, so a holding twice as large has twice the ink. A size that weighs
140
+ * nothing draws the smallest dot, so it cannot shrink or hide the others. Points without sizes all take `plain`.
141
+ */
142
+ function dotSizes(points, plain) {
143
+ const largest = points.reduce((most, p) => Math.max(most, weight(p)), 0);
144
+ const sized = points.some((p) => p.size !== void 0);
145
+ const radiusOf = (p) => {
146
+ if (!sized || p.size === void 0) return plain;
147
+ return largest > 0 ? Math.max(MIN_RADIUS, 20 * Math.sqrt(weight(p) / largest)) : MIN_RADIUS;
148
+ };
149
+ return {
150
+ radiusOf,
151
+ sized
152
+ };
153
+ }
154
+ /** Words broken into lines no wider than `width`; a word wider than that keeps a line of its own. */
155
+ function wrap(text, width, measure) {
156
+ const lines = [];
157
+ for (const word of text.split(" ")) {
158
+ const last = lines.at(-1);
159
+ if (last !== void 0 && measure(`${last} ${word}`) <= width) lines[lines.length - 1] = `${last} ${word}`;
160
+ else lines.push(word);
161
+ }
162
+ return lines;
163
+ }
164
+ /**
165
+ * Each corner's name in the plot's corner, in the band the y axis keeps clear above or below the data, wrapped to its
166
+ * own quadrant's width so two names on one edge never meet: where its lines are drawn, and the box they take.
167
+ */
168
+ function corners(plot, names, split, type) {
169
+ if (!names || !split) return [];
170
+ const line = type.size * 4 / 3;
171
+ const inset = 6;
172
+ return [
173
+ [
174
+ names.topLeft,
175
+ "left",
176
+ "top"
177
+ ],
178
+ [
179
+ names.topRight,
180
+ "right",
181
+ "top"
182
+ ],
183
+ [
184
+ names.bottomLeft,
185
+ "left",
186
+ "bottom"
187
+ ],
188
+ [
189
+ names.bottomRight,
190
+ "right",
191
+ "bottom"
192
+ ]
193
+ ].flatMap(([text, side, edge]) => {
194
+ if (!text) return [];
195
+ const room = (side === "left" ? split.x - plot.x : plot.x + plot.width - split.x) - 12;
196
+ const lines = wrap(text, Math.max(room, 0), type.measure);
197
+ const width = Math.max(...lines.map(type.measure));
198
+ if (lines.length > 2 || width > room) return [];
199
+ const height = lines.length * line;
200
+ const x = side === "left" ? plot.x + inset : plot.x + plot.width - inset - width;
201
+ const top = edge === "top" ? plot.y + inset : plot.y + plot.height - inset - height;
202
+ return [{
203
+ key: `${side}-${edge}`,
204
+ lines,
205
+ x,
206
+ y: top + type.size,
207
+ line,
208
+ box: {
209
+ x,
210
+ y: top,
211
+ width,
212
+ height
213
+ }
214
+ }];
215
+ });
216
+ }
217
+ //#endregion
218
+ export { MAX_RADIUS, corners, dotSizes, linearTicks, logTicks, placeLabels, quadrantSplit };
@@ -18,10 +18,5 @@ export declare function layout<T extends {
18
18
  weight: number;
19
19
  group?: string;
20
20
  }>(items: T[], width: number, height: number): Layout<T>;
21
- export declare const HEAT_STEPS = 5;
22
- /** The range the heat scale uses: one that is not a positive number falls back to ±5%. */
23
- export declare const heatRange: (range?: number) => number;
24
- export declare const heatFill: (side: "positive" | "negative", step: number) => string;
25
- export declare function heat(change: number | null, range?: number): string;
26
21
  //#endregion
27
22
  export {};
@@ -31,20 +31,5 @@ function layout(items, width, height) {
31
31
  }])
32
32
  };
33
33
  }
34
- const DEFAULT_RANGE = .05;
35
- const HEAT_STEPS = 5;
36
- /** The range the heat scale uses: one that is not a positive number falls back to ±5%. */
37
- const heatRange = (range = DEFAULT_RANGE) => Number.isFinite(range) && range > 0 ? range : DEFAULT_RANGE;
38
- const DARK_FLOOR = 25;
39
- const heatFill = (side, step) => {
40
- const mix = (share) => `color-mix(in oklab, var(--${side}) ${share}%, var(--card))`;
41
- const dark = DARK_FLOOR + (step - 1) * 35 / 4;
42
- return `light-dark(${mix(step * 12)}, ${mix(dark)})`;
43
- };
44
- function heat(change, range) {
45
- if (!change) return "var(--muted)";
46
- const step = Math.min(5, Math.ceil(Math.abs(change) / heatRange(range) * 5));
47
- return heatFill(change > 0 ? "positive" : "negative", step);
48
- }
49
34
  //#endregion
50
- export { HEAT_STEPS, heat, heatFill, heatRange, layout };
35
+ export { layout };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siyahi/ui",
3
- "version": "0.1.9-next.24",
3
+ "version": "0.1.9-next.25",
4
4
  "description": "Stock shadcn/ui on Base UI and Tailwind v4, a finance theme, a few compositions and formatters.",
5
5
  "keywords": [
6
6
  "base-ui",
package/report.css CHANGED
@@ -30,6 +30,11 @@
30
30
  --negative-foreground: oklch(0.985 0 0);
31
31
  --warning: oklch(0.555 0.149 48.998);
32
32
  --warning-foreground: oklch(0.985 0 0);
33
+ --heat-1: 12%;
34
+ --heat-2: 24%;
35
+ --heat-3: 36%;
36
+ --heat-4: 48%;
37
+ --heat-5: 60%;
33
38
  --link: oklch(0.51 0.2 275);
34
39
  --chart-1: oklch(0.4 0.17 275);
35
40
  --chart-2: oklch(0.66 0.13 70);
@@ -72,6 +77,10 @@
72
77
  --negative-foreground: oklch(0.145 0 0);
73
78
  --warning: oklch(0.828 0.169 84.429);
74
79
  --warning-foreground: oklch(0.145 0 0);
80
+ --heat-1: 25%;
81
+ --heat-2: 33.75%;
82
+ --heat-3: 42.5%;
83
+ --heat-4: 51.25%;
75
84
  --link: oklch(0.75 0.128 275);
76
85
  --chart-1: oklch(0.58 0.2 275);
77
86
  --chart-2: oklch(0.88 0.08 75);
package/styles/siyahi.css CHANGED
@@ -113,6 +113,16 @@
113
113
  --negative-foreground: oklch(0.985 0 0);
114
114
  --warning: oklch(0.555 0.149 48.998);
115
115
  --warning-foreground: oklch(0.985 0 0);
116
+ /*
117
+ * Heat steps: how much of --positive or --negative each of five steps mixes
118
+ * into the card, for Treemap, ChangeGrid and a signed CalendarHeatmap
119
+ * (lib/heat.ts). The contrast check reads them from here.
120
+ */
121
+ --heat-1: 12%;
122
+ --heat-2: 24%;
123
+ --heat-3: 36%;
124
+ --heat-4: 48%;
125
+ --heat-5: 60%;
116
126
  --link: oklch(0.51 0.2 275);
117
127
  /*
118
128
  * Indigo, amber, plum, sky, teal: no green or red, so no series reads as a
@@ -179,6 +189,12 @@
179
189
  --negative-foreground: oklch(0.145 0 0);
180
190
  --warning: oklch(0.828 0.169 84.429);
181
191
  --warning-foreground: oklch(0.145 0 0);
192
+ /* A 12% tint of a dark card reads as the card itself, so dark starts at 25%. */
193
+ --heat-1: 25%;
194
+ --heat-2: 33.75%;
195
+ --heat-3: 42.5%;
196
+ --heat-4: 51.25%;
197
+ --heat-5: 60%;
182
198
  --link: oklch(0.75 0.128 275);
183
199
  --chart-1: oklch(0.58 0.2 275);
184
200
  --chart-2: oklch(0.88 0.08 75);