@sentropic/design-system-svelte 0.34.39 → 0.34.42
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/dist/ContourChart.svelte +398 -0
- package/dist/ContourChart.svelte.d.ts +44 -0
- package/dist/ContourChart.svelte.d.ts.map +1 -0
- package/dist/ContourChart.test.d.ts +2 -0
- package/dist/ContourChart.test.d.ts.map +1 -0
- package/dist/ContourChart.test.js +59 -0
- package/dist/PointAndFigureChart.svelte +425 -0
- package/dist/PointAndFigureChart.svelte.d.ts +44 -0
- package/dist/PointAndFigureChart.svelte.d.ts.map +1 -0
- package/dist/PointAndFigureChart.test.d.ts +2 -0
- package/dist/PointAndFigureChart.test.d.ts.map +1 -0
- package/dist/PointAndFigureChart.test.js +73 -0
- package/dist/RenkoChart.svelte +356 -0
- package/dist/RenkoChart.svelte.d.ts +43 -0
- package/dist/RenkoChart.svelte.d.ts.map +1 -0
- package/dist/RenkoChart.test.d.ts +2 -0
- package/dist/RenkoChart.test.d.ts.map +1 -0
- package/dist/RenkoChart.test.js +66 -0
- package/dist/WindBarbChart.svelte +367 -0
- package/dist/WindBarbChart.svelte.d.ts +45 -0
- package/dist/WindBarbChart.svelte.d.ts.map +1 -0
- package/dist/WindBarbChart.test.d.ts +2 -0
- package/dist/WindBarbChart.test.d.ts.map +1 -0
- package/dist/WindBarbChart.test.js +65 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/**
|
|
3
|
+
* PointAndFigureChart — Point & Figure à partir d'une série de prix (façon
|
|
4
|
+
* Highcharts Stock « pointandfigure »). Des colonnes de X (hausse) ou de O
|
|
5
|
+
* (baisse) selon le mouvement de prix, calé sur une grille de `boxSize`. On
|
|
6
|
+
* empile des X tant que le prix monte d'au moins un `box` ; on ne change de
|
|
7
|
+
* colonne (vers des O) que lorsque le prix recule d'au moins `reversal`×`box`
|
|
8
|
+
* en sens inverse — et réciproquement. X = ton success, O = ton error. Axe Y
|
|
9
|
+
* prix gradué (niceTicks), PAS d'axe temps régulier (colonnes équidistantes).
|
|
10
|
+
* a11y : `role="img"` + `data-chart-key` + liste accessible des colonnes.
|
|
11
|
+
* API canonique (référence Svelte, React/Vue/Angular doivent s'aligner).
|
|
12
|
+
*
|
|
13
|
+
* Props obligatoires :
|
|
14
|
+
* data PointAndFigureChartDatum[] - série de prix {date, close}
|
|
15
|
+
*
|
|
16
|
+
* Props optionnelles :
|
|
17
|
+
* boxSize number (taille d'une case ; défaut auto ~ (max-min)/20)
|
|
18
|
+
* reversal number (nombre de cases pour inverser ; défaut 3)
|
|
19
|
+
* label string
|
|
20
|
+
* width number (défaut 640)
|
|
21
|
+
* height number (défaut 320)
|
|
22
|
+
* size number (non utilisé pour le rendu ; réservé parité d'API)
|
|
23
|
+
* class string
|
|
24
|
+
*/
|
|
25
|
+
export type PointAndFigureChartMark = "x" | "o";
|
|
26
|
+
|
|
27
|
+
export type PointAndFigureChartDatum = {
|
|
28
|
+
/** Position temporelle (timestamp ou index) — ignorée pour l'empilement. */
|
|
29
|
+
date: number;
|
|
30
|
+
/** Prix de clôture : pilote la formation des colonnes. */
|
|
31
|
+
close: number;
|
|
32
|
+
};
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<script lang="ts">
|
|
36
|
+
import ChartDataList from "./ChartDataList.svelte";
|
|
37
|
+
|
|
38
|
+
type PointAndFigureChartProps = {
|
|
39
|
+
data: PointAndFigureChartDatum[];
|
|
40
|
+
boxSize?: number;
|
|
41
|
+
reversal?: number;
|
|
42
|
+
label?: string;
|
|
43
|
+
width?: number;
|
|
44
|
+
height?: number;
|
|
45
|
+
size?: number;
|
|
46
|
+
class?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
let {
|
|
50
|
+
data = [],
|
|
51
|
+
boxSize,
|
|
52
|
+
reversal = 3,
|
|
53
|
+
label,
|
|
54
|
+
width = 640,
|
|
55
|
+
height = 320,
|
|
56
|
+
size,
|
|
57
|
+
class: className
|
|
58
|
+
}: PointAndFigureChartProps = $props();
|
|
59
|
+
|
|
60
|
+
const MARGIN = { top: 16, right: 18, bottom: 36, left: 52 };
|
|
61
|
+
|
|
62
|
+
function niceTicks(min: number, max: number, target = 5): number[] {
|
|
63
|
+
if (!Number.isFinite(min) || !Number.isFinite(max) || min === max) {
|
|
64
|
+
return [Number.isFinite(max) ? max : 0];
|
|
65
|
+
}
|
|
66
|
+
const range = max - min;
|
|
67
|
+
const rough = range / Math.max(target - 1, 1);
|
|
68
|
+
const pow = Math.pow(10, Math.floor(Math.log10(rough)));
|
|
69
|
+
const norm = rough / pow;
|
|
70
|
+
let step: number;
|
|
71
|
+
if (norm < 1.5) step = pow;
|
|
72
|
+
else if (norm < 3) step = 2 * pow;
|
|
73
|
+
else if (norm < 7) step = 5 * pow;
|
|
74
|
+
else step = 10 * pow;
|
|
75
|
+
const start = Math.floor(min / step) * step;
|
|
76
|
+
const end = Math.ceil(max / step) * step;
|
|
77
|
+
const ticks: number[] = [];
|
|
78
|
+
for (let v = start; v <= end + step / 2; v += step) ticks.push(Number(v.toFixed(10)));
|
|
79
|
+
return ticks;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function scaleLinear(v: number, d0: number, d1: number, r0: number, r1: number) {
|
|
83
|
+
if (d1 === d0) return r0;
|
|
84
|
+
return r0 + ((v - d0) * (r1 - r0)) / (d1 - d0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fmt(v: number): string {
|
|
88
|
+
if (Math.abs(v) >= 1000) return `${(v / 1000).toFixed(v % 1000 === 0 ? 0 : 1)}k`;
|
|
89
|
+
return Number.isInteger(v) ? String(v) : v.toFixed(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let hoveredKey: string | null = $state(null);
|
|
93
|
+
|
|
94
|
+
// Points valides : date et close finis.
|
|
95
|
+
const validData = $derived(
|
|
96
|
+
data.filter((d) => d && Number.isFinite(d.date) && Number.isFinite(d.close))
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Taille de case effective : `boxSize` fini > 0, sinon auto ~ (max-min)/20.
|
|
100
|
+
const effectiveBox = $derived.by(() => {
|
|
101
|
+
if (Number.isFinite(boxSize) && (boxSize as number) > 0) return boxSize as number;
|
|
102
|
+
const closes = validData.map((d) => d.close);
|
|
103
|
+
if (closes.length === 0) return 1;
|
|
104
|
+
const min = Math.min(...closes);
|
|
105
|
+
const max = Math.max(...closes);
|
|
106
|
+
const span = max - min;
|
|
107
|
+
return span > 0 ? span / 20 : 1;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Nombre de cases pour inverser : entier ≥ 1 (défaut 3).
|
|
111
|
+
const reversalBoxes = $derived(
|
|
112
|
+
Math.max(1, Math.floor(Number.isFinite(reversal) ? reversal : 3))
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Une colonne = une suite de X (hausse) OU de O (baisse). Une colonne porte
|
|
116
|
+
// ses cases occupées sous forme d'indices de case [low..high]. On change de
|
|
117
|
+
// colonne quand le prix recule d'au moins `reversal` cases en sens inverse.
|
|
118
|
+
const pnfColumns = $derived.by(() => {
|
|
119
|
+
const box = effectiveBox;
|
|
120
|
+
const cols: { mark: PointAndFigureChartMark; low: number; high: number }[] = [];
|
|
121
|
+
if (validData.length === 0 || box <= 0)
|
|
122
|
+
return [] as { mark: PointAndFigureChartMark; low: number; high: number; priceLow: number; priceHigh: number }[];
|
|
123
|
+
|
|
124
|
+
const closes = validData.map((d) => d.close);
|
|
125
|
+
const baseMin = Math.min(...closes);
|
|
126
|
+
// Indice de case (entier) d'un prix : quantifié sur la grille de `box`.
|
|
127
|
+
const boxIndex = (price: number) => Math.floor((price - baseMin) / box + 1e-9);
|
|
128
|
+
|
|
129
|
+
let mark: PointAndFigureChartMark | null = null;
|
|
130
|
+
let low = 0;
|
|
131
|
+
let high = 0;
|
|
132
|
+
|
|
133
|
+
for (let i = 0; i < closes.length; i++) {
|
|
134
|
+
const idx = boxIndex(closes[i]);
|
|
135
|
+
if (mark === null) {
|
|
136
|
+
mark = "x";
|
|
137
|
+
low = idx;
|
|
138
|
+
high = idx;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (mark === "x") {
|
|
142
|
+
if (idx > high) {
|
|
143
|
+
high = idx; // poursuite de la hausse
|
|
144
|
+
} else if (idx <= high - reversalBoxes) {
|
|
145
|
+
cols.push({ mark, low, high }); // fige la colonne X
|
|
146
|
+
mark = "o";
|
|
147
|
+
high = high - 1; // une colonne O repart un cran sous le sommet
|
|
148
|
+
low = idx;
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
if (idx < low) {
|
|
152
|
+
low = idx; // poursuite de la baisse
|
|
153
|
+
} else if (idx >= low + reversalBoxes) {
|
|
154
|
+
cols.push({ mark, low, high }); // fige la colonne O
|
|
155
|
+
mark = "x";
|
|
156
|
+
low = low + 1; // une colonne X repart un cran au-dessus du creux
|
|
157
|
+
high = idx;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (mark !== null && high >= low) cols.push({ mark, low, high });
|
|
162
|
+
// Annote chaque colonne avec les prix de ses cases [low..high].
|
|
163
|
+
return cols.map((c) => ({
|
|
164
|
+
mark: c.mark,
|
|
165
|
+
low: c.low,
|
|
166
|
+
high: c.high,
|
|
167
|
+
priceLow: baseMin + c.low * box,
|
|
168
|
+
priceHigh: baseMin + (c.high + 1) * box
|
|
169
|
+
}));
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const priceRange = $derived.by(() => {
|
|
173
|
+
if (pnfColumns.length === 0) {
|
|
174
|
+
const closes = validData.map((d) => d.close);
|
|
175
|
+
const min = closes.length ? Math.min(...closes) : 0;
|
|
176
|
+
const max = closes.length ? Math.max(...closes) : 0;
|
|
177
|
+
return { min, max };
|
|
178
|
+
}
|
|
179
|
+
let min = Infinity;
|
|
180
|
+
let max = -Infinity;
|
|
181
|
+
for (const c of pnfColumns) {
|
|
182
|
+
if (c.priceLow < min) min = c.priceLow;
|
|
183
|
+
if (c.priceHigh > max) max = c.priceHigh;
|
|
184
|
+
}
|
|
185
|
+
return { min, max };
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const scales = $derived.by(() => {
|
|
189
|
+
const { min, max } = priceRange;
|
|
190
|
+
const yTicks = niceTicks(min, max);
|
|
191
|
+
const plotW = Math.max(width - MARGIN.left - MARGIN.right, 1);
|
|
192
|
+
const plotH = Math.max(height - MARGIN.top - MARGIN.bottom, 1);
|
|
193
|
+
return {
|
|
194
|
+
yTicks,
|
|
195
|
+
yMin: yTicks[0],
|
|
196
|
+
yMax: yTicks[yTicks.length - 1],
|
|
197
|
+
plotW,
|
|
198
|
+
plotH
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Grille prix × colonnes : chaque colonne pose un glyphe par case [low..high].
|
|
203
|
+
const marks = $derived.by(() => {
|
|
204
|
+
const { yMin, yMax, plotW, plotH } = scales;
|
|
205
|
+
const box = effectiveBox;
|
|
206
|
+
const n = pnfColumns.length;
|
|
207
|
+
if (n === 0) return [];
|
|
208
|
+
const colW = plotW / n;
|
|
209
|
+
const glyph = Math.min(colW, scaleLinear(box, 0, Math.max(yMax - yMin, box), 0, plotH)) * 0.7;
|
|
210
|
+
const out: {
|
|
211
|
+
key: string;
|
|
212
|
+
mark: PointAndFigureChartMark;
|
|
213
|
+
cx: number;
|
|
214
|
+
cy: number;
|
|
215
|
+
r: number;
|
|
216
|
+
priceLow: number;
|
|
217
|
+
priceHigh: number;
|
|
218
|
+
}[] = [];
|
|
219
|
+
pnfColumns.forEach((c, ci) => {
|
|
220
|
+
const cx = MARGIN.left + colW * ci + colW / 2;
|
|
221
|
+
for (let b = c.low; b <= c.high; b++) {
|
|
222
|
+
const priceMid = priceRangeBase(b, box);
|
|
223
|
+
const cy = MARGIN.top + scaleLinear(priceMid, yMin, yMax, plotH, 0);
|
|
224
|
+
out.push({
|
|
225
|
+
key: `${ci}-${b}`,
|
|
226
|
+
mark: c.mark,
|
|
227
|
+
cx,
|
|
228
|
+
cy,
|
|
229
|
+
r: Math.max(glyph / 2, 2),
|
|
230
|
+
priceLow: c.priceLow,
|
|
231
|
+
priceHigh: c.priceHigh
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
return out;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Prix du centre d'une case (index de case → prix milieu), repère commun.
|
|
239
|
+
function priceRangeBase(boxIdx: number, box: number): number {
|
|
240
|
+
const closes = validData.map((d) => d.close);
|
|
241
|
+
const baseMin = closes.length ? Math.min(...closes) : 0;
|
|
242
|
+
return baseMin + (boxIdx + 0.5) * box;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const dataValueItems = $derived(
|
|
246
|
+
pnfColumns.map(
|
|
247
|
+
(c, i) => `${c.mark === "x" ? "X" : "O"} ${fmt(c.priceLow)} → ${fmt(c.priceHigh)}`
|
|
248
|
+
)
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
function handlePointerMove(event: PointerEvent) {
|
|
252
|
+
const target = event.target;
|
|
253
|
+
if (!(target instanceof Element)) {
|
|
254
|
+
hoveredKey = null;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
hoveredKey = target.getAttribute("data-chart-key");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const hoveredMark = $derived.by(() => {
|
|
261
|
+
if (hoveredKey === null) return null;
|
|
262
|
+
return marks.find((m) => m.key === hoveredKey) ?? null;
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const classes = () => ["st-pointAndFigureChart", className].filter(Boolean).join(" ");
|
|
266
|
+
</script>
|
|
267
|
+
|
|
268
|
+
<div class={classes()}>
|
|
269
|
+
<div
|
|
270
|
+
class="st-pointAndFigureChart__visual"
|
|
271
|
+
role="img"
|
|
272
|
+
aria-label={label}
|
|
273
|
+
onpointermove={handlePointerMove}
|
|
274
|
+
onpointerleave={() => (hoveredKey = null)}
|
|
275
|
+
>
|
|
276
|
+
<svg
|
|
277
|
+
viewBox="0 0 {width} {height}"
|
|
278
|
+
preserveAspectRatio="xMidYMid meet"
|
|
279
|
+
width="100%"
|
|
280
|
+
height="100%"
|
|
281
|
+
focusable="false"
|
|
282
|
+
aria-hidden="true"
|
|
283
|
+
>
|
|
284
|
+
<!-- gridlines + ticks Y (prix) -->
|
|
285
|
+
{#each scales.yTicks as t (t)}
|
|
286
|
+
{@const y = MARGIN.top + scaleLinear(t, scales.yMin, scales.yMax, scales.plotH, 0)}
|
|
287
|
+
<line class="st-pointAndFigureChart__grid" x1={MARGIN.left} x2={width - MARGIN.right} y1={y} y2={y} />
|
|
288
|
+
<text class="st-pointAndFigureChart__tick" x={MARGIN.left - 6} y={y} text-anchor="end" dominant-baseline="middle">{fmt(t)}</text>
|
|
289
|
+
{/each}
|
|
290
|
+
|
|
291
|
+
<!-- axes -->
|
|
292
|
+
<line class="st-pointAndFigureChart__axis" x1={MARGIN.left} x2={MARGIN.left} y1={MARGIN.top} y2={height - MARGIN.bottom} />
|
|
293
|
+
<line class="st-pointAndFigureChart__axis" x1={MARGIN.left} x2={width - MARGIN.right} y1={height - MARGIN.bottom} y2={height - MARGIN.bottom} />
|
|
294
|
+
|
|
295
|
+
<!-- glyphes X / O par colonne et par case -->
|
|
296
|
+
{#each marks as m (m.key)}
|
|
297
|
+
{#if m.mark === "x"}
|
|
298
|
+
<g
|
|
299
|
+
class="st-pointAndFigureChart__mark st-pointAndFigureChart__mark--x"
|
|
300
|
+
class:st-pointAndFigureChart__mark--dim={hoveredKey !== null && hoveredKey !== m.key}
|
|
301
|
+
data-chart-key={m.key}
|
|
302
|
+
>
|
|
303
|
+
<line class="st-pointAndFigureChart__glyph" x1={m.cx - m.r} y1={m.cy - m.r} x2={m.cx + m.r} y2={m.cy + m.r} data-chart-key={m.key} />
|
|
304
|
+
<line class="st-pointAndFigureChart__glyph" x1={m.cx - m.r} y1={m.cy + m.r} x2={m.cx + m.r} y2={m.cy - m.r} data-chart-key={m.key} />
|
|
305
|
+
</g>
|
|
306
|
+
{:else}
|
|
307
|
+
<circle
|
|
308
|
+
class="st-pointAndFigureChart__mark st-pointAndFigureChart__mark--o st-pointAndFigureChart__glyph"
|
|
309
|
+
class:st-pointAndFigureChart__mark--dim={hoveredKey !== null && hoveredKey !== m.key}
|
|
310
|
+
cx={m.cx}
|
|
311
|
+
cy={m.cy}
|
|
312
|
+
r={m.r}
|
|
313
|
+
data-chart-key={m.key}
|
|
314
|
+
/>
|
|
315
|
+
{/if}
|
|
316
|
+
{/each}
|
|
317
|
+
</svg>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<ChartDataList label={label ?? "point and figure"} items={dataValueItems} />
|
|
321
|
+
|
|
322
|
+
{#if hoveredMark}
|
|
323
|
+
{@const m = hoveredMark}
|
|
324
|
+
<div
|
|
325
|
+
class="st-pointAndFigureChart__tooltip"
|
|
326
|
+
role="presentation"
|
|
327
|
+
style="left: {(m.cx / width) * 100}%; top: {(m.cy / height) * 100}%"
|
|
328
|
+
>
|
|
329
|
+
<span class="st-pointAndFigureChart__tooltipLabel">{m.mark === "x" ? "X" : "O"}</span>
|
|
330
|
+
<span class="st-pointAndFigureChart__tooltipValue">{fmt(m.priceLow)} → {fmt(m.priceHigh)}</span>
|
|
331
|
+
</div>
|
|
332
|
+
{/if}
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
<style>
|
|
336
|
+
.st-pointAndFigureChart {
|
|
337
|
+
color: var(--st-semantic-text-secondary);
|
|
338
|
+
display: block;
|
|
339
|
+
font-family: inherit;
|
|
340
|
+
position: relative;
|
|
341
|
+
width: 100%;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.st-pointAndFigureChart svg {
|
|
345
|
+
display: block;
|
|
346
|
+
overflow: visible;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.st-pointAndFigureChart__visual {
|
|
350
|
+
display: block;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.st-pointAndFigureChart__grid {
|
|
354
|
+
opacity: 0.5;
|
|
355
|
+
stroke: var(--st-semantic-border-subtle);
|
|
356
|
+
stroke-dasharray: 2 3;
|
|
357
|
+
stroke-width: 1;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.st-pointAndFigureChart__axis {
|
|
361
|
+
stroke: var(--st-semantic-border-subtle);
|
|
362
|
+
stroke-width: 1;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.st-pointAndFigureChart__tick {
|
|
366
|
+
fill: var(--st-semantic-text-secondary);
|
|
367
|
+
font-size: 0.6875rem;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.st-pointAndFigureChart__mark {
|
|
371
|
+
transition: opacity 120ms ease;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.st-pointAndFigureChart__mark--dim {
|
|
375
|
+
opacity: 0.35;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.st-pointAndFigureChart__glyph {
|
|
379
|
+
cursor: pointer;
|
|
380
|
+
fill: none;
|
|
381
|
+
stroke-linecap: round;
|
|
382
|
+
stroke-width: 2;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.st-pointAndFigureChart__mark--x .st-pointAndFigureChart__glyph,
|
|
386
|
+
.st-pointAndFigureChart__mark--x.st-pointAndFigureChart__glyph {
|
|
387
|
+
stroke: var(--st-semantic-feedback-success);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.st-pointAndFigureChart__mark--o .st-pointAndFigureChart__glyph,
|
|
391
|
+
.st-pointAndFigureChart__mark--o.st-pointAndFigureChart__glyph {
|
|
392
|
+
stroke: var(--st-semantic-feedback-error);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.st-pointAndFigureChart__tooltip {
|
|
396
|
+
background: var(--st-semantic-surface-inverse);
|
|
397
|
+
border-radius: var(--st-radius-sm, 0.25rem);
|
|
398
|
+
color: var(--st-semantic-text-inverse);
|
|
399
|
+
display: inline-flex;
|
|
400
|
+
flex-direction: column;
|
|
401
|
+
font-size: 0.75rem;
|
|
402
|
+
gap: 0.125rem;
|
|
403
|
+
line-height: 1.2;
|
|
404
|
+
padding: 0.375rem 0.5rem;
|
|
405
|
+
pointer-events: none;
|
|
406
|
+
position: absolute;
|
|
407
|
+
transform: translate(-50%, calc(-100% - 8px));
|
|
408
|
+
white-space: nowrap;
|
|
409
|
+
z-index: 1;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.st-pointAndFigureChart__tooltipLabel {
|
|
413
|
+
font-weight: 600;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.st-pointAndFigureChart__tooltipValue {
|
|
417
|
+
opacity: 0.85;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
@media (prefers-reduced-motion: reduce) {
|
|
421
|
+
.st-pointAndFigureChart__mark {
|
|
422
|
+
transition: none;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
</style>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PointAndFigureChart — Point & Figure à partir d'une série de prix (façon
|
|
3
|
+
* Highcharts Stock « pointandfigure »). Des colonnes de X (hausse) ou de O
|
|
4
|
+
* (baisse) selon le mouvement de prix, calé sur une grille de `boxSize`. On
|
|
5
|
+
* empile des X tant que le prix monte d'au moins un `box` ; on ne change de
|
|
6
|
+
* colonne (vers des O) que lorsque le prix recule d'au moins `reversal`×`box`
|
|
7
|
+
* en sens inverse — et réciproquement. X = ton success, O = ton error. Axe Y
|
|
8
|
+
* prix gradué (niceTicks), PAS d'axe temps régulier (colonnes équidistantes).
|
|
9
|
+
* a11y : `role="img"` + `data-chart-key` + liste accessible des colonnes.
|
|
10
|
+
* API canonique (référence Svelte, React/Vue/Angular doivent s'aligner).
|
|
11
|
+
*
|
|
12
|
+
* Props obligatoires :
|
|
13
|
+
* data PointAndFigureChartDatum[] - série de prix {date, close}
|
|
14
|
+
*
|
|
15
|
+
* Props optionnelles :
|
|
16
|
+
* boxSize number (taille d'une case ; défaut auto ~ (max-min)/20)
|
|
17
|
+
* reversal number (nombre de cases pour inverser ; défaut 3)
|
|
18
|
+
* label string
|
|
19
|
+
* width number (défaut 640)
|
|
20
|
+
* height number (défaut 320)
|
|
21
|
+
* size number (non utilisé pour le rendu ; réservé parité d'API)
|
|
22
|
+
* class string
|
|
23
|
+
*/
|
|
24
|
+
export type PointAndFigureChartMark = "x" | "o";
|
|
25
|
+
export type PointAndFigureChartDatum = {
|
|
26
|
+
/** Position temporelle (timestamp ou index) — ignorée pour l'empilement. */
|
|
27
|
+
date: number;
|
|
28
|
+
/** Prix de clôture : pilote la formation des colonnes. */
|
|
29
|
+
close: number;
|
|
30
|
+
};
|
|
31
|
+
type PointAndFigureChartProps = {
|
|
32
|
+
data: PointAndFigureChartDatum[];
|
|
33
|
+
boxSize?: number;
|
|
34
|
+
reversal?: number;
|
|
35
|
+
label?: string;
|
|
36
|
+
width?: number;
|
|
37
|
+
height?: number;
|
|
38
|
+
size?: number;
|
|
39
|
+
class?: string;
|
|
40
|
+
};
|
|
41
|
+
declare const PointAndFigureChart: import("svelte").Component<PointAndFigureChartProps, {}, "">;
|
|
42
|
+
type PointAndFigureChart = ReturnType<typeof PointAndFigureChart>;
|
|
43
|
+
export default PointAndFigureChart;
|
|
44
|
+
//# sourceMappingURL=PointAndFigureChart.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PointAndFigureChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/PointAndFigureChart.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,MAAM,uBAAuB,GAAG,GAAG,GAAG,GAAG,CAAC;AAEhD,MAAM,MAAM,wBAAwB,GAAG;IACrC,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAMF,KAAK,wBAAwB,GAAG;IAC9B,IAAI,EAAE,wBAAwB,EAAE,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA6QJ,QAAA,MAAM,mBAAmB,8DAAwC,CAAC;AAClE,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAClE,eAAe,mBAAmB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PointAndFigureChart.test.d.ts","sourceRoot":"","sources":["../src/lib/PointAndFigureChart.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { render } from "@testing-library/svelte";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import PointAndFigureChart from "./PointAndFigureChart.svelte";
|
|
4
|
+
// Série de prix : montée puis repli marqué (≥ reversal cases) — produit une
|
|
5
|
+
// colonne de X puis une colonne de O.
|
|
6
|
+
const series = [
|
|
7
|
+
{ date: 0, close: 100 },
|
|
8
|
+
{ date: 1, close: 110 },
|
|
9
|
+
{ date: 2, close: 120 },
|
|
10
|
+
{ date: 3, close: 130 },
|
|
11
|
+
{ date: 4, close: 100 },
|
|
12
|
+
{ date: 5, close: 90 }
|
|
13
|
+
];
|
|
14
|
+
const marks = (container) => Array.from(container.querySelectorAll(".st-pointAndFigureChart__mark"));
|
|
15
|
+
const listItems = (container) => Array.from(container.querySelectorAll(".st-chartDataList li")).map((n) => n.textContent?.trim());
|
|
16
|
+
const structuralClass = (el) => el.className.split(/\s+/)[0];
|
|
17
|
+
describe("PointAndFigureChart", () => {
|
|
18
|
+
it("renders an img role and X/O marks from the price series", () => {
|
|
19
|
+
const { container } = render(PointAndFigureChart, {
|
|
20
|
+
props: { data: series, boxSize: 10, reversal: 3, label: "P&F" }
|
|
21
|
+
});
|
|
22
|
+
expect(container.querySelector('[role="img"]')).toBeTruthy();
|
|
23
|
+
expect(marks(container).length).toBeGreaterThan(0);
|
|
24
|
+
});
|
|
25
|
+
it("draws X marks for the rising column", () => {
|
|
26
|
+
const { container } = render(PointAndFigureChart, {
|
|
27
|
+
props: { data: [{ date: 0, close: 100 }, { date: 1, close: 130 }], boxSize: 10, reversal: 3, label: "P" }
|
|
28
|
+
});
|
|
29
|
+
expect(container.querySelectorAll(".st-pointAndFigureChart__mark--x").length).toBeGreaterThan(0);
|
|
30
|
+
});
|
|
31
|
+
it("switches to a column of O marks after a reversal", () => {
|
|
32
|
+
const { container } = render(PointAndFigureChart, {
|
|
33
|
+
props: { data: series, boxSize: 10, reversal: 3, label: "P" }
|
|
34
|
+
});
|
|
35
|
+
expect(container.querySelectorAll(".st-pointAndFigureChart__mark--o").length).toBeGreaterThan(0);
|
|
36
|
+
});
|
|
37
|
+
it("renders a graduated price (Y) axis with nice ticks", () => {
|
|
38
|
+
const { container } = render(PointAndFigureChart, {
|
|
39
|
+
props: { data: series, boxSize: 10, reversal: 3, label: "P" }
|
|
40
|
+
});
|
|
41
|
+
expect(container.querySelectorAll(".st-pointAndFigureChart__axis").length).toBe(2);
|
|
42
|
+
expect(container.querySelectorAll(".st-pointAndFigureChart__tick").length).toBeGreaterThan(0);
|
|
43
|
+
});
|
|
44
|
+
it("lists every column in the accessible data list", () => {
|
|
45
|
+
const { container } = render(PointAndFigureChart, {
|
|
46
|
+
props: { data: [{ date: 0, close: 100 }, { date: 1, close: 130 }], boxSize: 10, reversal: 3, label: "P" }
|
|
47
|
+
});
|
|
48
|
+
expect(listItems(container).length).toBeGreaterThan(0);
|
|
49
|
+
expect(listItems(container)[0]?.startsWith("X")).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
it("drops non-finite points before building columns", () => {
|
|
52
|
+
const { container } = render(PointAndFigureChart, {
|
|
53
|
+
props: {
|
|
54
|
+
data: [
|
|
55
|
+
{ date: Number.NaN, close: 100 },
|
|
56
|
+
{ date: 0, close: Number.NaN },
|
|
57
|
+
{ date: 1, close: 100 },
|
|
58
|
+
{ date: 2, close: 130 }
|
|
59
|
+
],
|
|
60
|
+
boxSize: 10,
|
|
61
|
+
reversal: 3,
|
|
62
|
+
label: "P"
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
expect(marks(container).length).toBeGreaterThan(0);
|
|
66
|
+
});
|
|
67
|
+
it("merges a custom class onto the root", () => {
|
|
68
|
+
const { container } = render(PointAndFigureChart, { props: { data: series, class: "mine" } });
|
|
69
|
+
const root = container.querySelector(".st-pointAndFigureChart");
|
|
70
|
+
expect(structuralClass(root)).toBe("st-pointAndFigureChart");
|
|
71
|
+
expect(root.classList.contains("mine")).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
});
|