@siyahi/ui 0.1.9-next.23 → 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/delta.tsx +11 -6
- 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/delta.js +2 -2
- 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 +23 -5
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/lib/fit-text.d.ts
|
|
2
|
+
/** About how wide a text-xs label draws, in pixels. */
|
|
3
|
+
export declare const textWidth: (text: string) => number;
|
|
4
|
+
/** The label, cut with an ellipsis when it would draw wider than `width` pixels at text-xs. */
|
|
5
|
+
export declare function fit(text: string, width: number): string;
|
|
6
|
+
//#endregion
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/lib/fit-text.ts
|
|
2
|
+
const CHAR_PX = 6.6;
|
|
3
|
+
/** About how wide a text-xs label draws, in pixels. */
|
|
4
|
+
const textWidth = (text) => text.length * CHAR_PX;
|
|
5
|
+
/** The label, cut with an ellipsis when it would draw wider than `width` pixels at text-xs. */
|
|
6
|
+
function fit(text, width) {
|
|
7
|
+
const room = Math.floor(width / CHAR_PX);
|
|
8
|
+
return text.length <= room ? text : `${text.slice(0, Math.max(room - 1, 1)).trimEnd()}…`;
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { fit, textWidth };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/lib/heat.d.ts
|
|
2
|
+
export declare const HEAT_STEPS = 5;
|
|
3
|
+
/** The range the heat scale uses: one that is not a positive number falls back to ±5%. */
|
|
4
|
+
export declare const heatRange: (range?: number) => number;
|
|
5
|
+
export declare const heatFill: (side: "positive" | "negative", step: number) => string;
|
|
6
|
+
export declare function heat(change: number | null, range?: number): string;
|
|
7
|
+
//#endregion
|
package/dist/lib/heat.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/lib/heat.ts
|
|
2
|
+
/** The signed heat scale a Treemap tile and a ChangeGrid cell share: five steps of gain or loss mixed into the card. */
|
|
3
|
+
const DEFAULT_RANGE = .05;
|
|
4
|
+
const HEAT_STEPS = 5;
|
|
5
|
+
/** The range the heat scale uses: one that is not a positive number falls back to ±5%. */
|
|
6
|
+
const heatRange = (range = DEFAULT_RANGE) => Number.isFinite(range) && range > 0 ? range : DEFAULT_RANGE;
|
|
7
|
+
const heatFill = (side, step) => `color-mix(in oklab, var(--${side}) var(--heat-${step}), var(--card))`;
|
|
8
|
+
function heat(change, range) {
|
|
9
|
+
if (!change || !Number.isFinite(change)) return "var(--muted)";
|
|
10
|
+
const step = Math.min(5, Math.ceil(Math.abs(change) / heatRange(range) * 5));
|
|
11
|
+
return heatFill(change > 0 ? "positive" : "negative", step);
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { HEAT_STEPS, heat, heatFill, heatRange };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
//#region src/lib/scatter.d.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
|
+
export declare function logTicks(min: number, max: number): number[];
|
|
7
|
+
/**
|
|
8
|
+
* 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
|
|
9
|
+
* of ten: −50% to +75% by 25s, never −50% to +150% because the step had to be 50. Values that differ only by float
|
|
10
|
+
* noise, such as 0.3 and 0.30000000000000004, count as one value and get that value as their only tick.
|
|
11
|
+
*/
|
|
12
|
+
export declare function linearTicks(min: number, max: number, count?: number): number[];
|
|
13
|
+
export interface Box {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
}
|
|
19
|
+
export interface LabelCandidate {
|
|
20
|
+
key: string;
|
|
21
|
+
text: string;
|
|
22
|
+
/** The dot's centre, in pixels. */
|
|
23
|
+
cx: number;
|
|
24
|
+
cy: number;
|
|
25
|
+
/** The dot's radius, its edge included: a sized dot keeps names off all of itself. */
|
|
26
|
+
r: number;
|
|
27
|
+
}
|
|
28
|
+
export interface PlacedLabel {
|
|
29
|
+
key: string;
|
|
30
|
+
text: string;
|
|
31
|
+
/** Where the text starts and its baseline, with textAnchor="start". */
|
|
32
|
+
x: number;
|
|
33
|
+
y: number;
|
|
34
|
+
}
|
|
35
|
+
/** How labels are set: their type size in pixels and a name's width in that type. */
|
|
36
|
+
export interface LabelType {
|
|
37
|
+
size: number;
|
|
38
|
+
measure: (text: string) => number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Names beside dots, in the candidates' order, up to `limit`: each tries the right of its dot, then the left, above,
|
|
42
|
+
* below and the four corners, and takes the first spot inside the plot that covers no other label, no other dot and
|
|
43
|
+
* nothing `reserved` (such as a quadrant's name). A name that finds no spot is left to the tooltip, so labels never sit
|
|
44
|
+
* on top of each other.
|
|
45
|
+
*/
|
|
46
|
+
export declare function placeLabels(candidates: readonly LabelCandidate[], plot: Box, limit: number, { size, measure }: LabelType, reserved?: readonly Box[]): PlacedLabel[];
|
|
47
|
+
/**
|
|
48
|
+
* Splits the plot into four at a value on each axis, such as zero, with an optional name in each corner. On a log
|
|
49
|
+
* axis x must be above zero, or no split is drawn. A name that does not fit its quadrant in two lines is left out.
|
|
50
|
+
*/
|
|
51
|
+
export interface ScatterQuadrants {
|
|
52
|
+
x: number;
|
|
53
|
+
y: number;
|
|
54
|
+
labels?: {
|
|
55
|
+
topLeft?: string;
|
|
56
|
+
topRight?: string;
|
|
57
|
+
bottomLeft?: string;
|
|
58
|
+
bottomRight?: string;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export type QuadrantLabels = ScatterQuadrants["labels"];
|
|
62
|
+
/** A split a log axis cannot place, at or below zero, or one that is not a number, is no split at all. */
|
|
63
|
+
export declare function quadrantSplit(quadrants: ScatterQuadrants | undefined, xScale: "linear" | "log"): ScatterQuadrants | undefined;
|
|
64
|
+
/** The largest sized dot; the smallest stays big enough to see and to tap. */
|
|
65
|
+
export declare const MAX_RADIUS = 20;
|
|
66
|
+
/**
|
|
67
|
+
* Each dot's radius. Area, not radius, follows size, so a holding twice as large has twice the ink. A size that weighs
|
|
68
|
+
* nothing draws the smallest dot, so it cannot shrink or hide the others. Points without sizes all take `plain`.
|
|
69
|
+
*/
|
|
70
|
+
export declare function dotSizes(points: readonly {
|
|
71
|
+
size?: number;
|
|
72
|
+
}[], plain: number): {
|
|
73
|
+
radiusOf: (p: {
|
|
74
|
+
size?: number;
|
|
75
|
+
}) => number;
|
|
76
|
+
sized: boolean;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* 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
|
|
80
|
+
* own quadrant's width so two names on one edge never meet: where its lines are drawn, and the box they take.
|
|
81
|
+
*/
|
|
82
|
+
export declare function corners(plot: Box, names: QuadrantLabels, split: {
|
|
83
|
+
x: number;
|
|
84
|
+
y: number;
|
|
85
|
+
} | null, type: LabelType): {
|
|
86
|
+
key: string;
|
|
87
|
+
lines: string[];
|
|
88
|
+
x: number;
|
|
89
|
+
y: number;
|
|
90
|
+
line: number;
|
|
91
|
+
box: {
|
|
92
|
+
x: number;
|
|
93
|
+
y: number;
|
|
94
|
+
width: number;
|
|
95
|
+
height: number;
|
|
96
|
+
};
|
|
97
|
+
}[];
|
|
98
|
+
//#endregion
|
|
@@ -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);
|
|
@@ -237,9 +253,11 @@
|
|
|
237
253
|
* --input is the edge of a field, where the reader types or picks. A button,
|
|
238
254
|
* a toggle and a code block are framed by their label or their content, so
|
|
239
255
|
* their outline stays the faint divider colour; scoping the variable keeps
|
|
240
|
-
* the fill each derives from it faint too.
|
|
256
|
+
* the fill each derives from it faint too. A button is matched by its own
|
|
257
|
+
* group/button class: rendered as a popover or menu trigger, it takes that
|
|
258
|
+
* trigger's data-slot instead of "button".
|
|
241
259
|
*/
|
|
242
|
-
:is([
|
|
260
|
+
:is([class~="group/button"], [data-slot="toggle"], [data-slot="toggle-group-item"], [data-slot="code-block"]) {
|
|
243
261
|
--input: var(--border);
|
|
244
262
|
}
|
|
245
263
|
|
|
@@ -263,7 +281,7 @@
|
|
|
263
281
|
* hidden range input, so it shows the input's focus.
|
|
264
282
|
*/
|
|
265
283
|
:is([data-slot="toggle"], [data-slot="toggle-group-item"], [data-slot="scroll-area-viewport"]):focus-visible,
|
|
266
|
-
[
|
|
284
|
+
[class~="group/button"][data-day]:focus-visible {
|
|
267
285
|
--tw-ring-color: var(--ring);
|
|
268
286
|
}
|
|
269
287
|
[data-slot="slider-thumb"]:has(input:focus-visible) {
|
|
@@ -274,13 +292,13 @@
|
|
|
274
292
|
* A destructive control focuses, and an invalid field is edged, in the full
|
|
275
293
|
* destructive colour; stock fades both to 40-50%, under 3:1.
|
|
276
294
|
*/
|
|
277
|
-
[
|
|
295
|
+
[class~="group/button"][class~="text-destructive"]:focus-visible,
|
|
278
296
|
[data-slot="code-block"][data-variant="destructive"]:has(> [data-slot="code-block-content"]:focus-visible),
|
|
279
297
|
:is(
|
|
280
298
|
[data-slot="input"],
|
|
281
299
|
[data-slot="textarea"],
|
|
282
300
|
[data-slot="select-trigger"],
|
|
283
|
-
[
|
|
301
|
+
[class~="group/button"]
|
|
284
302
|
)[aria-invalid="true"],
|
|
285
303
|
[data-slot="input-group"]:has([data-slot][aria-invalid="true"]) {
|
|
286
304
|
border-color: var(--destructive);
|