@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.
- package/agents/examples/change-grid-money.tsx +36 -0
- package/agents/examples/change-grid.tsx +42 -0
- package/agents/examples/drawdown-chart.tsx +35 -0
- package/agents/examples/heatmap-calendar-signed.tsx +67 -0
- package/agents/examples/heatmap-status.tsx +6 -4
- package/agents/examples/histogram-chart.tsx +28 -0
- package/agents/examples/scatter-chart-quadrants.tsx +63 -0
- package/agents/examples/scatter-chart.tsx +75 -0
- package/agents/examples/slope-chart-market.tsx +33 -0
- package/agents/examples/slope-chart.tsx +26 -0
- package/agents/examples/waterfall-chart-stores.tsx +27 -0
- package/agents/examples/waterfall-chart.tsx +37 -0
- package/agents/manifest.json +160 -0
- package/dist/components/registry/heatmap/calendar-heatmap.d.ts +8 -2
- package/dist/components/registry/heatmap/calendar-heatmap.js +96 -18
- package/dist/components/registry/heatmap/status-heatmap.js +86 -47
- package/dist/components/siyahi/change-grid.d.ts +31 -0
- package/dist/components/siyahi/change-grid.js +74 -0
- package/dist/components/siyahi/drawdown-chart.d.ts +34 -0
- package/dist/components/siyahi/drawdown-chart.js +102 -0
- package/dist/components/siyahi/histogram-chart.d.ts +57 -0
- package/dist/components/siyahi/histogram-chart.js +134 -0
- package/dist/components/siyahi/scatter-chart.d.ts +59 -0
- package/dist/components/siyahi/scatter-chart.js +367 -0
- package/dist/components/siyahi/slope-chart.d.ts +35 -0
- package/dist/components/siyahi/slope-chart.js +159 -0
- package/dist/components/siyahi/treemap.js +3 -7
- package/dist/components/siyahi/waterfall-chart.d.ts +51 -0
- package/dist/components/siyahi/waterfall-chart.js +147 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +7 -1
- package/dist/lib/chart-frame.d.ts +7 -2
- package/dist/lib/chart-frame.js +44 -10
- package/dist/lib/chart-series.d.ts +2 -3
- package/dist/lib/fit-text.d.ts +6 -0
- package/dist/lib/fit-text.js +11 -0
- package/dist/lib/heat.d.ts +7 -0
- package/dist/lib/heat.js +14 -0
- package/dist/lib/scatter.d.ts +98 -0
- package/dist/lib/scatter.js +218 -0
- package/dist/lib/treemap.d.ts +0 -5
- package/dist/lib/treemap.js +1 -16
- package/package.json +1 -1
- package/report.css +9 -0
- 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 };
|
package/dist/lib/treemap.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/lib/treemap.js
CHANGED
|
@@ -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 {
|
|
35
|
+
export { layout };
|
package/package.json
CHANGED
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);
|