ui-svelte 0.2.4 → 0.2.5
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/assets/country-flags.d.ts +1 -0
- package/dist/assets/country-flags.js +1612 -0
- package/dist/charts/ArcChart.svelte +291 -48
- package/dist/charts/ArcChart.svelte.d.ts +32 -1
- package/dist/charts/Candlestick.svelte +663 -115
- package/dist/charts/Candlestick.svelte.d.ts +40 -0
- package/dist/charts/css/arc-chart.css +76 -6
- package/dist/charts/css/candlestick.css +234 -11
- package/dist/control/Button.svelte +3 -1
- package/dist/control/Button.svelte.d.ts +1 -0
- package/dist/control/IconButton.svelte +3 -1
- package/dist/control/IconButton.svelte.d.ts +1 -0
- package/dist/control/ToggleGroup.svelte +82 -0
- package/dist/control/ToggleGroup.svelte.d.ts +20 -0
- package/dist/control/css/toggle-group.css +85 -0
- package/dist/css/base.css +23 -15
- package/dist/css/utilities.css +45 -0
- package/dist/display/AvatarGroup.svelte +59 -0
- package/dist/display/AvatarGroup.svelte.d.ts +17 -0
- package/dist/display/Code.svelte +9 -2
- package/dist/display/Code.svelte.d.ts +1 -0
- package/dist/display/Section.svelte +1 -1
- package/dist/display/css/avatar-group.css +46 -0
- package/dist/display/css/avatar.css +1 -10
- package/dist/form/ComboBox.svelte.d.ts +1 -1
- package/dist/form/PhoneField.svelte +8 -4
- package/dist/form/Select.svelte.d.ts +1 -1
- package/dist/index.css +43 -21
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/navigation/NavMenu.svelte +1 -0
- package/dist/navigation/css/nav-menu.css +97 -7
- package/package.json +2 -2
- /package/dist/{form/js → assets}/countries.d.ts +0 -0
- /package/dist/{form/js → assets}/countries.js +0 -0
- /package/dist/{form/js → assets}/phone-examples.d.ts +0 -0
- /package/dist/{form/js → assets}/phone-examples.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { cn } from '../utils/class-names.js';
|
|
3
|
-
import { onMount } from 'svelte';
|
|
3
|
+
import { onMount, type Snippet } from 'svelte';
|
|
4
4
|
|
|
5
5
|
type LinearScale = {
|
|
6
6
|
(value: number): number;
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
type Color = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'danger' | 'muted';
|
|
20
|
+
type Size = 'sm' | 'md' | 'lg' | 'xl';
|
|
21
|
+
type Theme = 'default' | 'tradingview' | 'dark' | 'light';
|
|
22
|
+
type CandleStyle = 'filled' | 'hollow' | 'heikinashi' | 'line' | 'area';
|
|
23
|
+
type ScaleType = 'linear' | 'log';
|
|
24
|
+
type GridStyle = 'solid' | 'dashed' | 'dotted' | 'none';
|
|
20
25
|
|
|
21
26
|
type CandleData = {
|
|
22
27
|
date: string | Date;
|
|
@@ -27,6 +32,11 @@
|
|
|
27
32
|
volume?: number;
|
|
28
33
|
};
|
|
29
34
|
|
|
35
|
+
type Indicator =
|
|
36
|
+
| { type: 'sma'; period: number; color?: Color }
|
|
37
|
+
| { type: 'ema'; period: number; color?: Color }
|
|
38
|
+
| { type: 'bollinger'; period: number; stdDev?: number; color?: Color };
|
|
39
|
+
|
|
30
40
|
type Margin = {
|
|
31
41
|
top: number;
|
|
32
42
|
right: number;
|
|
@@ -53,41 +63,91 @@
|
|
|
53
63
|
maxVisibleCandles?: number;
|
|
54
64
|
rootClass?: string;
|
|
55
65
|
chartClass?: string;
|
|
66
|
+
size?: Size;
|
|
67
|
+
theme?: Theme;
|
|
68
|
+
candleStyle?: CandleStyle;
|
|
69
|
+
scaleType?: ScaleType;
|
|
70
|
+
showCrosshair?: boolean;
|
|
71
|
+
showYAxisLabels?: boolean;
|
|
72
|
+
showXAxisLabels?: boolean;
|
|
73
|
+
gridStyle?: GridStyle;
|
|
74
|
+
showLastPrice?: boolean;
|
|
75
|
+
indicators?: Indicator[];
|
|
76
|
+
onClick?: (candle: CandleData, index: number) => void;
|
|
77
|
+
onRangeChange?: (start: number, end: number) => void;
|
|
78
|
+
priceFormatter?: (value: number) => string;
|
|
79
|
+
dateFormatter?: (date: Date | string) => string;
|
|
80
|
+
animated?: boolean;
|
|
81
|
+
tooltipContent?: Snippet<[{ candle: CandleData; change: number; changePercent: number }]>;
|
|
56
82
|
};
|
|
57
83
|
|
|
58
84
|
let {
|
|
59
85
|
data = [],
|
|
60
|
-
margin = { top: 20, right:
|
|
61
|
-
showVolume
|
|
62
|
-
showGrid
|
|
86
|
+
margin = { top: 20, right: 60, bottom: 40, left: 60 },
|
|
87
|
+
showVolume,
|
|
88
|
+
showGrid,
|
|
63
89
|
candleWidth = 8,
|
|
64
90
|
bullishColor = 'success' as Color,
|
|
65
91
|
bearishColor = 'danger' as Color,
|
|
66
92
|
wickWidth = 1,
|
|
67
|
-
loading
|
|
68
|
-
empty
|
|
93
|
+
loading,
|
|
94
|
+
empty,
|
|
69
95
|
emptyText = 'No data available',
|
|
70
|
-
enableZoom
|
|
71
|
-
enableScroll
|
|
96
|
+
enableZoom,
|
|
97
|
+
enableScroll,
|
|
72
98
|
initialVisibleCandles = 50,
|
|
73
99
|
minVisibleCandles = 10,
|
|
74
100
|
maxVisibleCandles = 200,
|
|
75
101
|
rootClass,
|
|
76
|
-
chartClass
|
|
102
|
+
chartClass,
|
|
103
|
+
size = 'md',
|
|
104
|
+
theme = 'default',
|
|
105
|
+
candleStyle = 'filled',
|
|
106
|
+
scaleType = 'linear',
|
|
107
|
+
showCrosshair,
|
|
108
|
+
showYAxisLabels,
|
|
109
|
+
showXAxisLabels,
|
|
110
|
+
gridStyle = 'dashed',
|
|
111
|
+
showLastPrice,
|
|
112
|
+
indicators = [],
|
|
113
|
+
onClick,
|
|
114
|
+
onRangeChange,
|
|
115
|
+
priceFormatter,
|
|
116
|
+
dateFormatter,
|
|
117
|
+
animated,
|
|
118
|
+
tooltipContent
|
|
77
119
|
}: Props = $props();
|
|
78
120
|
|
|
121
|
+
const sizePresets: Record<Size, number> = {
|
|
122
|
+
sm: 200,
|
|
123
|
+
md: 350,
|
|
124
|
+
lg: 500,
|
|
125
|
+
xl: 700
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const themeColors: Record<Theme, { bg: string; grid: string; text: string }> = {
|
|
129
|
+
default: { bg: 'transparent', grid: 'var(--color-muted)', text: 'var(--color-on-muted)' },
|
|
130
|
+
tradingview: { bg: '#131722', grid: '#363c4e', text: '#d1d4dc' },
|
|
131
|
+
dark: { bg: '#1a1a2e', grid: '#2d2d44', text: '#e0e0e0' },
|
|
132
|
+
light: { bg: '#ffffff', grid: '#e0e0e0', text: '#333333' }
|
|
133
|
+
};
|
|
134
|
+
|
|
79
135
|
let containerEl: HTMLDivElement | undefined = $state();
|
|
80
136
|
let svgEl: SVGSVGElement | undefined = $state();
|
|
81
137
|
let containerSize = $state({ width: 0, height: 0 });
|
|
82
138
|
|
|
139
|
+
// svelte-ignore state_referenced_locally
|
|
83
140
|
let visibleCandles = $state(initialVisibleCandles);
|
|
84
141
|
let scrollOffset = $state(0);
|
|
85
142
|
let isDragging = $state(false);
|
|
86
143
|
let dragStartX = $state(0);
|
|
87
144
|
let dragStartOffset = $state(0);
|
|
88
145
|
|
|
146
|
+
let crosshairPosition = $state<{ x: number; y: number } | null>(null);
|
|
147
|
+
let isCrosshairActive = $state(false);
|
|
148
|
+
|
|
89
149
|
let width = $derived(containerSize.width || 800);
|
|
90
|
-
let
|
|
150
|
+
let chartHeight = $derived(sizePresets[size]);
|
|
91
151
|
|
|
92
152
|
let visibleData = $derived.by(() => {
|
|
93
153
|
if (data.length === 0) return [];
|
|
@@ -96,6 +156,123 @@
|
|
|
96
156
|
return data.slice(start, end);
|
|
97
157
|
});
|
|
98
158
|
|
|
159
|
+
let processedData = $derived.by(() => {
|
|
160
|
+
if (candleStyle !== 'heikinashi') return visibleData;
|
|
161
|
+
return calculateHeikinAshi(visibleData);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
function calculateHeikinAshi(candles: CandleData[]): CandleData[] {
|
|
165
|
+
if (candles.length === 0) return [];
|
|
166
|
+
|
|
167
|
+
const result: CandleData[] = [];
|
|
168
|
+
let prevHaOpen = candles[0].open;
|
|
169
|
+
let prevHaClose = (candles[0].open + candles[0].high + candles[0].low + candles[0].close) / 4;
|
|
170
|
+
|
|
171
|
+
for (let i = 0; i < candles.length; i++) {
|
|
172
|
+
const c = candles[i];
|
|
173
|
+
const haClose = (c.open + c.high + c.low + c.close) / 4;
|
|
174
|
+
const haOpen = i === 0 ? c.open : (prevHaOpen + prevHaClose) / 2;
|
|
175
|
+
const haHigh = Math.max(c.high, haOpen, haClose);
|
|
176
|
+
const haLow = Math.min(c.low, haOpen, haClose);
|
|
177
|
+
|
|
178
|
+
result.push({
|
|
179
|
+
date: c.date,
|
|
180
|
+
open: haOpen,
|
|
181
|
+
high: haHigh,
|
|
182
|
+
low: haLow,
|
|
183
|
+
close: haClose,
|
|
184
|
+
volume: c.volume
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
prevHaOpen = haOpen;
|
|
188
|
+
prevHaClose = haClose;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function calculateSMA(candles: CandleData[], period: number): (number | null)[] {
|
|
195
|
+
const result: (number | null)[] = [];
|
|
196
|
+
for (let i = 0; i < candles.length; i++) {
|
|
197
|
+
if (i < period - 1) {
|
|
198
|
+
result.push(null);
|
|
199
|
+
} else {
|
|
200
|
+
let sum = 0;
|
|
201
|
+
for (let j = 0; j < period; j++) {
|
|
202
|
+
sum += candles[i - j].close;
|
|
203
|
+
}
|
|
204
|
+
result.push(sum / period);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function calculateEMA(candles: CandleData[], period: number): (number | null)[] {
|
|
211
|
+
const result: (number | null)[] = [];
|
|
212
|
+
const multiplier = 2 / (period + 1);
|
|
213
|
+
|
|
214
|
+
for (let i = 0; i < candles.length; i++) {
|
|
215
|
+
if (i < period - 1) {
|
|
216
|
+
result.push(null);
|
|
217
|
+
} else if (i === period - 1) {
|
|
218
|
+
let sum = 0;
|
|
219
|
+
for (let j = 0; j < period; j++) {
|
|
220
|
+
sum += candles[i - j].close;
|
|
221
|
+
}
|
|
222
|
+
result.push(sum / period);
|
|
223
|
+
} else {
|
|
224
|
+
const prevEma = result[i - 1];
|
|
225
|
+
if (prevEma !== null) {
|
|
226
|
+
result.push((candles[i].close - prevEma) * multiplier + prevEma);
|
|
227
|
+
} else {
|
|
228
|
+
result.push(null);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function calculateBollingerBands(
|
|
236
|
+
candles: CandleData[],
|
|
237
|
+
period: number,
|
|
238
|
+
stdDev: number = 2
|
|
239
|
+
): { upper: (number | null)[]; middle: (number | null)[]; lower: (number | null)[] } {
|
|
240
|
+
const sma = calculateSMA(candles, period);
|
|
241
|
+
const upper: (number | null)[] = [];
|
|
242
|
+
const lower: (number | null)[] = [];
|
|
243
|
+
|
|
244
|
+
for (let i = 0; i < candles.length; i++) {
|
|
245
|
+
if (sma[i] === null) {
|
|
246
|
+
upper.push(null);
|
|
247
|
+
lower.push(null);
|
|
248
|
+
} else {
|
|
249
|
+
let sumSquaredDiff = 0;
|
|
250
|
+
for (let j = 0; j < period; j++) {
|
|
251
|
+
sumSquaredDiff += Math.pow(candles[i - j].close - sma[i]!, 2);
|
|
252
|
+
}
|
|
253
|
+
const std = Math.sqrt(sumSquaredDiff / period);
|
|
254
|
+
upper.push(sma[i]! + stdDev * std);
|
|
255
|
+
lower.push(sma[i]! - stdDev * std);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return { upper, middle: sma, lower };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let indicatorData = $derived.by(() => {
|
|
263
|
+
return indicators.map((ind) => {
|
|
264
|
+
if (ind.type === 'sma') {
|
|
265
|
+
return { ...ind, values: calculateSMA(processedData, ind.period) };
|
|
266
|
+
} else if (ind.type === 'ema') {
|
|
267
|
+
return { ...ind, values: calculateEMA(processedData, ind.period) };
|
|
268
|
+
} else if (ind.type === 'bollinger') {
|
|
269
|
+
const bands = calculateBollingerBands(processedData, ind.period, ind.stdDev);
|
|
270
|
+
return { ...ind, bands };
|
|
271
|
+
}
|
|
272
|
+
return ind;
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
99
276
|
function createLinearScale(domain: [number, number], range: [number, number]): LinearScale {
|
|
100
277
|
const [d0, d1] = domain;
|
|
101
278
|
const [r0, r1] = range;
|
|
@@ -114,6 +291,28 @@
|
|
|
114
291
|
return scale;
|
|
115
292
|
}
|
|
116
293
|
|
|
294
|
+
function createLogScale(domain: [number, number], range: [number, number]): LinearScale {
|
|
295
|
+
const [d0, d1] = domain;
|
|
296
|
+
const [r0, r1] = range;
|
|
297
|
+
const logD0 = Math.log10(Math.max(d0, 0.001));
|
|
298
|
+
const logD1 = Math.log10(Math.max(d1, 0.001));
|
|
299
|
+
|
|
300
|
+
const scale = (value: number): number => {
|
|
301
|
+
const logValue = Math.log10(Math.max(value, 0.001));
|
|
302
|
+
return r0 + ((logValue - logD0) / (logD1 - logD0)) * (r1 - r0);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
scale.invert = (pixel: number): number => {
|
|
306
|
+
const logValue = logD0 + ((pixel - r0) / (r1 - r0)) * (logD1 - logD0);
|
|
307
|
+
return Math.pow(10, logValue);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
scale.domain = domain;
|
|
311
|
+
scale.range = range;
|
|
312
|
+
|
|
313
|
+
return scale;
|
|
314
|
+
}
|
|
315
|
+
|
|
117
316
|
function createBandScale(
|
|
118
317
|
domain: (string | number | Date)[],
|
|
119
318
|
range: [number, number],
|
|
@@ -148,15 +347,17 @@
|
|
|
148
347
|
if (abs >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
|
|
149
348
|
if (abs >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
|
|
150
349
|
if (abs >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
|
|
151
|
-
if (abs < 1) return value.toFixed(
|
|
152
|
-
return value.toFixed(
|
|
350
|
+
if (abs < 1) return value.toFixed(4);
|
|
351
|
+
return value.toFixed(2);
|
|
153
352
|
}
|
|
154
353
|
|
|
155
354
|
function formatPrice(value: number): string {
|
|
355
|
+
if (priceFormatter) return priceFormatter(value);
|
|
156
356
|
return value.toFixed(2);
|
|
157
357
|
}
|
|
158
358
|
|
|
159
359
|
function formatDate(date: string | Date): string {
|
|
360
|
+
if (dateFormatter) return dateFormatter(date);
|
|
160
361
|
if (typeof date === 'string') {
|
|
161
362
|
const d = new Date(date);
|
|
162
363
|
return `${d.getMonth() + 1}/${d.getDate()}`;
|
|
@@ -164,16 +365,33 @@
|
|
|
164
365
|
return date.toLocaleDateString();
|
|
165
366
|
}
|
|
166
367
|
|
|
368
|
+
function formatDateTime(date: string | Date): string {
|
|
369
|
+
if (dateFormatter) return dateFormatter(date);
|
|
370
|
+
const d = typeof date === 'string' ? new Date(date) : date;
|
|
371
|
+
return d.toLocaleString();
|
|
372
|
+
}
|
|
373
|
+
|
|
167
374
|
let innerWidth = $derived(width - margin.left - margin.right);
|
|
168
|
-
let innerHeight = $derived(
|
|
375
|
+
let innerHeight = $derived(chartHeight - margin.top - margin.bottom);
|
|
169
376
|
|
|
170
377
|
let priceHeight = $derived(showVolume ? innerHeight * 0.75 : innerHeight);
|
|
171
378
|
let volumeHeight = $derived(showVolume ? innerHeight * 0.2 : 0);
|
|
172
379
|
let volumeTop = $derived(showVolume ? priceHeight + 10 : 0);
|
|
173
380
|
|
|
174
381
|
let priceDomain = $derived.by((): [number, number] => {
|
|
175
|
-
if (
|
|
176
|
-
const allPrices =
|
|
382
|
+
if (processedData.length === 0) return [0, 1];
|
|
383
|
+
const allPrices = processedData.flatMap((d) => [d.high, d.low]);
|
|
384
|
+
|
|
385
|
+
indicatorData.forEach((ind: any) => {
|
|
386
|
+
if (ind.values) {
|
|
387
|
+
allPrices.push(...ind.values.filter((v: number | null) => v !== null));
|
|
388
|
+
}
|
|
389
|
+
if (ind.bands) {
|
|
390
|
+
allPrices.push(...ind.bands.upper.filter((v: number | null) => v !== null));
|
|
391
|
+
allPrices.push(...ind.bands.lower.filter((v: number | null) => v !== null));
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
177
395
|
const min = Math.min(...allPrices);
|
|
178
396
|
const max = Math.max(...allPrices);
|
|
179
397
|
const padding = (max - min) * 0.1;
|
|
@@ -181,26 +399,36 @@
|
|
|
181
399
|
});
|
|
182
400
|
|
|
183
401
|
let volumeDomain = $derived.by((): [number, number] => {
|
|
184
|
-
if (!showVolume ||
|
|
185
|
-
const volumes =
|
|
402
|
+
if (!showVolume || processedData.length === 0) return [0, 1];
|
|
403
|
+
const volumes = processedData.map((d) => d.volume || 0);
|
|
186
404
|
return [0, Math.max(...volumes) * 1.2];
|
|
187
405
|
});
|
|
188
406
|
|
|
189
407
|
let xScale = $derived(
|
|
190
408
|
createBandScale(
|
|
191
|
-
|
|
409
|
+
processedData.map((d) => d.date),
|
|
192
410
|
[0, innerWidth],
|
|
193
411
|
0.3
|
|
194
412
|
)
|
|
195
413
|
);
|
|
196
|
-
|
|
414
|
+
|
|
415
|
+
let priceScale = $derived(
|
|
416
|
+
scaleType === 'log'
|
|
417
|
+
? createLogScale(priceDomain, [priceHeight, 0])
|
|
418
|
+
: createLinearScale(priceDomain, [priceHeight, 0])
|
|
419
|
+
);
|
|
420
|
+
|
|
197
421
|
let volumeScale = $derived(
|
|
198
422
|
showVolume ? createLinearScale(volumeDomain, [volumeHeight, 0]) : null
|
|
199
423
|
);
|
|
424
|
+
|
|
200
425
|
let grid = $derived(createGridLines(priceHeight));
|
|
201
426
|
|
|
202
427
|
function createGridLines(priceH: number): Array<{ x: number; y: number; value: number }> {
|
|
203
|
-
const yScale =
|
|
428
|
+
const yScale =
|
|
429
|
+
scaleType === 'log'
|
|
430
|
+
? createLogScale(priceDomain, [priceH, 0])
|
|
431
|
+
: createLinearScale(priceDomain, [priceH, 0]);
|
|
204
432
|
const yTicks = 5;
|
|
205
433
|
|
|
206
434
|
return Array.from({ length: yTicks + 1 }, (_, i) => {
|
|
@@ -209,8 +437,29 @@
|
|
|
209
437
|
});
|
|
210
438
|
}
|
|
211
439
|
|
|
440
|
+
let lastCandle = $derived(
|
|
441
|
+
processedData.length > 0 ? processedData[processedData.length - 1] : null
|
|
442
|
+
);
|
|
443
|
+
let lastPrice = $derived(lastCandle?.close || 0);
|
|
444
|
+
let lastPriceY = $derived(priceScale(lastPrice));
|
|
445
|
+
|
|
446
|
+
function getGridDashArray(): string {
|
|
447
|
+
switch (gridStyle) {
|
|
448
|
+
case 'solid':
|
|
449
|
+
return 'none';
|
|
450
|
+
case 'dotted':
|
|
451
|
+
return '1, 3';
|
|
452
|
+
case 'dashed':
|
|
453
|
+
default:
|
|
454
|
+
return '4, 4';
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
212
458
|
function handleWheel(event: WheelEvent): void {
|
|
213
459
|
if (!enableZoom || data.length === 0) return;
|
|
460
|
+
|
|
461
|
+
if (!event.ctrlKey && !event.metaKey) return;
|
|
462
|
+
|
|
214
463
|
event.preventDefault();
|
|
215
464
|
|
|
216
465
|
const delta = event.deltaY;
|
|
@@ -224,6 +473,10 @@
|
|
|
224
473
|
|
|
225
474
|
const oldEnd = scrollOffset + visibleCandles;
|
|
226
475
|
scrollOffset = Math.max(0, Math.min(oldEnd - visibleCandles, data.length - visibleCandles));
|
|
476
|
+
|
|
477
|
+
if (onRangeChange) {
|
|
478
|
+
onRangeChange(scrollOffset, scrollOffset + visibleCandles);
|
|
479
|
+
}
|
|
227
480
|
}
|
|
228
481
|
|
|
229
482
|
function handleMouseDown(event: MouseEvent): void {
|
|
@@ -234,22 +487,48 @@
|
|
|
234
487
|
}
|
|
235
488
|
|
|
236
489
|
function handleMouseMove(event: MouseEvent): void {
|
|
237
|
-
if (
|
|
490
|
+
if (isDragging && enableScroll) {
|
|
491
|
+
const deltaX = event.clientX - dragStartX;
|
|
492
|
+
const candlesPerPixel = visibleCandles / innerWidth;
|
|
493
|
+
const candlesDelta = Math.round(-deltaX * candlesPerPixel);
|
|
494
|
+
|
|
495
|
+
scrollOffset = Math.max(
|
|
496
|
+
0,
|
|
497
|
+
Math.min(dragStartOffset + candlesDelta, data.length - visibleCandles)
|
|
498
|
+
);
|
|
499
|
+
}
|
|
238
500
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
501
|
+
if (showCrosshair && svgEl && containerEl) {
|
|
502
|
+
const rect = svgEl.getBoundingClientRect();
|
|
503
|
+
const x = event.clientX - rect.left - margin.left;
|
|
504
|
+
const y = event.clientY - rect.top - margin.top;
|
|
242
505
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
506
|
+
if (x >= 0 && x <= innerWidth && y >= 0 && y <= priceHeight) {
|
|
507
|
+
crosshairPosition = { x, y };
|
|
508
|
+
isCrosshairActive = true;
|
|
509
|
+
} else {
|
|
510
|
+
isCrosshairActive;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
247
513
|
}
|
|
248
514
|
|
|
249
515
|
function handleMouseUp(): void {
|
|
250
516
|
isDragging = false;
|
|
251
517
|
}
|
|
252
518
|
|
|
519
|
+
function handleMouseLeave(): void {
|
|
520
|
+
isCrosshairActive = false;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function handleDoubleClick(): void {
|
|
524
|
+
visibleCandles = Math.min(initialVisibleCandles, data.length);
|
|
525
|
+
scrollOffset = Math.max(0, data.length - visibleCandles);
|
|
526
|
+
|
|
527
|
+
if (onRangeChange) {
|
|
528
|
+
onRangeChange(scrollOffset, scrollOffset + visibleCandles);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
253
532
|
let touchStartX = $state(0);
|
|
254
533
|
let touchStartOffset = $state(0);
|
|
255
534
|
|
|
@@ -274,14 +553,19 @@
|
|
|
274
553
|
}
|
|
275
554
|
|
|
276
555
|
let tooltipData = $state<CandleData | null>(null);
|
|
556
|
+
let tooltipChange = $state<{ value: number; percent: number }>({ value: 0, percent: 0 });
|
|
277
557
|
let tooltipPosition = $state<{ x: number; y: number }>({ x: 0, y: 0 });
|
|
278
558
|
let isTooltipActive = $state(false);
|
|
279
559
|
|
|
280
|
-
function handleCandleHover(candle: CandleData, event: MouseEvent): void {
|
|
560
|
+
function handleCandleHover(candle: CandleData, index: number, event: MouseEvent): void {
|
|
281
561
|
const target = event.target as SVGElement;
|
|
282
562
|
const rect = target.getBoundingClientRect();
|
|
283
563
|
|
|
564
|
+
const change = candle.close - candle.open;
|
|
565
|
+
const changePercent = candle.open !== 0 ? (change / candle.open) * 100 : 0;
|
|
566
|
+
|
|
284
567
|
tooltipData = candle;
|
|
568
|
+
tooltipChange = { value: change, percent: changePercent };
|
|
285
569
|
tooltipPosition = {
|
|
286
570
|
x: rect.right + 8,
|
|
287
571
|
y: rect.top + rect.height / 2
|
|
@@ -298,16 +582,30 @@
|
|
|
298
582
|
}, 100);
|
|
299
583
|
}
|
|
300
584
|
|
|
301
|
-
|
|
302
|
-
if (
|
|
303
|
-
|
|
304
|
-
|
|
585
|
+
function handleCandleClick(candle: CandleData, index: number): void {
|
|
586
|
+
if (onClick) {
|
|
587
|
+
onClick(candle, index);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
let crosshairPrice = $derived(crosshairPosition ? priceScale.invert(crosshairPosition.y) : 0);
|
|
305
592
|
|
|
306
|
-
let
|
|
307
|
-
if (
|
|
308
|
-
|
|
593
|
+
let crosshairCandleIndex = $derived.by(() => {
|
|
594
|
+
if (!crosshairPosition || processedData.length === 0) return -1;
|
|
595
|
+
const bandwidth = xScale.bandwidth();
|
|
596
|
+
for (let i = 0; i < processedData.length; i++) {
|
|
597
|
+
const candleX = xScale(processedData[i].date);
|
|
598
|
+
if (crosshairPosition.x >= candleX && crosshairPosition.x < candleX + bandwidth) {
|
|
599
|
+
return i;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return -1;
|
|
309
603
|
});
|
|
310
604
|
|
|
605
|
+
let crosshairCandle = $derived(
|
|
606
|
+
crosshairCandleIndex >= 0 ? processedData[crosshairCandleIndex] : null
|
|
607
|
+
);
|
|
608
|
+
|
|
311
609
|
onMount(() => {
|
|
312
610
|
if (data.length > 0) {
|
|
313
611
|
visibleCandles = Math.min(initialVisibleCandles, data.length);
|
|
@@ -321,6 +619,13 @@
|
|
|
321
619
|
}
|
|
322
620
|
};
|
|
323
621
|
|
|
622
|
+
const handleScroll = () => {
|
|
623
|
+
if (isTooltipActive) {
|
|
624
|
+
isTooltipActive = false;
|
|
625
|
+
tooltipData = null;
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
|
|
324
629
|
updateSize();
|
|
325
630
|
const resizeObserver = new ResizeObserver(updateSize);
|
|
326
631
|
if (containerEl) {
|
|
@@ -329,11 +634,13 @@
|
|
|
329
634
|
|
|
330
635
|
window.addEventListener('mousemove', handleMouseMove);
|
|
331
636
|
window.addEventListener('mouseup', handleMouseUp);
|
|
637
|
+
window.addEventListener('scroll', handleScroll, true);
|
|
332
638
|
|
|
333
639
|
return () => {
|
|
334
640
|
resizeObserver.disconnect();
|
|
335
641
|
window.removeEventListener('mousemove', handleMouseMove);
|
|
336
642
|
window.removeEventListener('mouseup', handleMouseUp);
|
|
643
|
+
window.removeEventListener('scroll', handleScroll, true);
|
|
337
644
|
};
|
|
338
645
|
});
|
|
339
646
|
|
|
@@ -343,11 +650,73 @@
|
|
|
343
650
|
scrollOffset = Math.max(0, Math.min(scrollOffset, data.length - visibleCandles));
|
|
344
651
|
}
|
|
345
652
|
});
|
|
653
|
+
|
|
654
|
+
function getIndicatorColor(ind: Indicator): string {
|
|
655
|
+
const colorMap: Record<Color, string> = {
|
|
656
|
+
primary: 'var(--color-primary)',
|
|
657
|
+
secondary: 'var(--color-secondary)',
|
|
658
|
+
success: 'var(--color-success)',
|
|
659
|
+
info: 'var(--color-info)',
|
|
660
|
+
warning: 'var(--color-warning)',
|
|
661
|
+
danger: 'var(--color-danger)',
|
|
662
|
+
muted: 'var(--color-muted)'
|
|
663
|
+
};
|
|
664
|
+
return colorMap[ind.color || 'primary'];
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function createIndicatorPath(values: (number | null)[]): string {
|
|
668
|
+
const points: string[] = [];
|
|
669
|
+
let started;
|
|
670
|
+
|
|
671
|
+
for (let i = 0; i < values.length; i++) {
|
|
672
|
+
const val = values[i];
|
|
673
|
+
if (val === null) continue;
|
|
674
|
+
|
|
675
|
+
const x = xScale(processedData[i].date) + xScale.bandwidth() / 2;
|
|
676
|
+
const y = priceScale(val);
|
|
677
|
+
|
|
678
|
+
if (!started) {
|
|
679
|
+
points.push(`M ${x} ${y}`);
|
|
680
|
+
started = true;
|
|
681
|
+
} else {
|
|
682
|
+
points.push(`L ${x} ${y}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return points.join(' ');
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function createAreaPath(): string {
|
|
690
|
+
if (processedData.length === 0) return '';
|
|
691
|
+
|
|
692
|
+
const points: string[] = [];
|
|
693
|
+
const firstX = xScale(processedData[0].date) + xScale.bandwidth() / 2;
|
|
694
|
+
|
|
695
|
+
points.push(`M ${firstX} ${priceHeight}`);
|
|
696
|
+
|
|
697
|
+
for (let i = 0; i < processedData.length; i++) {
|
|
698
|
+
const x = xScale(processedData[i].date) + xScale.bandwidth() / 2;
|
|
699
|
+
const y = priceScale(processedData[i].close);
|
|
700
|
+
points.push(`L ${x} ${y}`);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const lastX = xScale(processedData[processedData.length - 1].date) + xScale.bandwidth() / 2;
|
|
704
|
+
points.push(`L ${lastX} ${priceHeight}`);
|
|
705
|
+
points.push('Z');
|
|
706
|
+
|
|
707
|
+
return points.join(' ');
|
|
708
|
+
}
|
|
346
709
|
</script>
|
|
347
710
|
|
|
348
|
-
<div
|
|
711
|
+
<div
|
|
712
|
+
bind:this={containerEl}
|
|
713
|
+
class={cn('candlestick-chart-container', `is-${size}`, `theme-${theme}`, rootClass)}
|
|
714
|
+
style={theme !== 'default' ? `background: ${themeColors[theme].bg};` : ''}
|
|
715
|
+
>
|
|
349
716
|
{#if loading}
|
|
350
|
-
<div class="candlestick-chart-loading"
|
|
717
|
+
<div class="candlestick-chart-loading">
|
|
718
|
+
<div class="candlestick-chart-loading-skeleton"></div>
|
|
719
|
+
</div>
|
|
351
720
|
{:else if empty || data.length === 0}
|
|
352
721
|
<div class="candlestick-chart-empty">
|
|
353
722
|
<svg
|
|
@@ -366,21 +735,23 @@
|
|
|
366
735
|
<span>{emptyText}</span>
|
|
367
736
|
</div>
|
|
368
737
|
{:else}
|
|
369
|
-
<div class={cn('candlestick-chart', chartClass)}>
|
|
738
|
+
<div class={cn('candlestick-chart', `is-${size}`, chartClass)}>
|
|
370
739
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
371
740
|
<svg
|
|
372
741
|
bind:this={svgEl}
|
|
373
742
|
class="candlestick-chart-svg"
|
|
374
743
|
class:candlestick-chart-dragging={isDragging}
|
|
375
744
|
{width}
|
|
376
|
-
{
|
|
745
|
+
height={chartHeight}
|
|
377
746
|
onwheel={handleWheel}
|
|
378
747
|
onmousedown={handleMouseDown}
|
|
379
748
|
ontouchstart={handleTouchStart}
|
|
380
749
|
ontouchmove={handleTouchMove}
|
|
750
|
+
onmouseleave={handleMouseLeave}
|
|
751
|
+
ondblclick={handleDoubleClick}
|
|
381
752
|
>
|
|
382
753
|
<g transform="translate({margin.left}, {margin.top})">
|
|
383
|
-
{#if showGrid}
|
|
754
|
+
{#if showGrid && gridStyle !== 'none'}
|
|
384
755
|
<g class="candlestick-chart-grid">
|
|
385
756
|
{#each grid as line}
|
|
386
757
|
<line
|
|
@@ -389,6 +760,7 @@
|
|
|
389
760
|
x2={line.x}
|
|
390
761
|
y2={line.y}
|
|
391
762
|
class="candlestick-chart-grid-line"
|
|
763
|
+
stroke-dasharray={getGridDashArray()}
|
|
392
764
|
/>
|
|
393
765
|
{/each}
|
|
394
766
|
</g>
|
|
@@ -404,7 +776,7 @@
|
|
|
404
776
|
/>
|
|
405
777
|
<line x1={0} y1={0} x2={0} y2={priceHeight} class="candlestick-chart-axis-line" />
|
|
406
778
|
|
|
407
|
-
{#if showGrid}
|
|
779
|
+
{#if showYAxisLabels && showGrid}
|
|
408
780
|
{#each grid as line}
|
|
409
781
|
<text
|
|
410
782
|
x={-10}
|
|
@@ -418,17 +790,19 @@
|
|
|
418
790
|
{/each}
|
|
419
791
|
{/if}
|
|
420
792
|
|
|
421
|
-
{#
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
793
|
+
{#if showXAxisLabels}
|
|
794
|
+
{#each processedData.filter((_, i) => i % Math.ceil(processedData.length / 6) === 0) as d}
|
|
795
|
+
<text
|
|
796
|
+
x={xScale(d.date) + xScale.bandwidth() / 2}
|
|
797
|
+
y={priceHeight + 20}
|
|
798
|
+
class="candlestick-chart-axis-label"
|
|
799
|
+
text-anchor="middle"
|
|
800
|
+
font-size="10"
|
|
801
|
+
>
|
|
802
|
+
{formatDate(d.date)}
|
|
803
|
+
</text>
|
|
804
|
+
{/each}
|
|
805
|
+
{/if}
|
|
432
806
|
|
|
433
807
|
{#if showVolume}
|
|
434
808
|
<line
|
|
@@ -441,48 +815,199 @@
|
|
|
441
815
|
{/if}
|
|
442
816
|
</g>
|
|
443
817
|
|
|
444
|
-
{#
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
818
|
+
{#if candleStyle === 'area'}
|
|
819
|
+
<path d={createAreaPath()} class="candlestick-chart-area is-{bullishColor}" />
|
|
820
|
+
<path
|
|
821
|
+
d={createIndicatorPath(processedData.map((d) => d.close))}
|
|
822
|
+
class="candlestick-chart-line is-{bullishColor}"
|
|
823
|
+
fill="none"
|
|
824
|
+
stroke-width="2"
|
|
825
|
+
/>
|
|
826
|
+
{:else if candleStyle === 'line'}
|
|
827
|
+
<path
|
|
828
|
+
d={createIndicatorPath(processedData.map((d) => d.close))}
|
|
829
|
+
class="candlestick-chart-line is-{bullishColor}"
|
|
830
|
+
fill="none"
|
|
831
|
+
stroke-width="2"
|
|
832
|
+
/>
|
|
833
|
+
{:else}
|
|
834
|
+
{#each processedData as d, i}
|
|
835
|
+
{@const x = xScale(d.date) + xScale.bandwidth() / 2}
|
|
836
|
+
{@const open = priceScale(d.open)}
|
|
837
|
+
{@const close = priceScale(d.close)}
|
|
838
|
+
{@const high = priceScale(d.high)}
|
|
839
|
+
{@const low = priceScale(d.low)}
|
|
840
|
+
{@const isBullish = d.close >= d.open}
|
|
841
|
+
{@const bodyTop = Math.min(open, close)}
|
|
842
|
+
{@const bodyHeight = Math.abs(open - close)}
|
|
843
|
+
{@const colorClass = isBullish ? bullishColor : bearishColor}
|
|
844
|
+
{@const isHollow = candleStyle === 'hollow' && isBullish}
|
|
845
|
+
|
|
846
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
847
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
848
|
+
<g
|
|
849
|
+
onmouseenter={(e) => handleCandleHover(d, i, e)}
|
|
850
|
+
onmouseleave={handleCandleLeave}
|
|
851
|
+
onclick={() => handleCandleClick(d, i)}
|
|
852
|
+
>
|
|
853
|
+
<line
|
|
854
|
+
x1={x}
|
|
855
|
+
y1={high}
|
|
856
|
+
x2={x}
|
|
857
|
+
y2={low}
|
|
858
|
+
class="candlestick-chart-wick is-{colorClass}"
|
|
859
|
+
style="stroke-width: {wickWidth};"
|
|
860
|
+
/>
|
|
465
861
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
862
|
+
<rect
|
|
863
|
+
x={x - candleWidth / 2}
|
|
864
|
+
y={bodyTop}
|
|
865
|
+
width={candleWidth}
|
|
866
|
+
height={Math.max(bodyHeight, 1)}
|
|
867
|
+
class={cn(
|
|
868
|
+
'candlestick-chart-candle',
|
|
869
|
+
`is-${colorClass}`,
|
|
870
|
+
isHollow && 'is-hollow'
|
|
871
|
+
)}
|
|
872
|
+
/>
|
|
873
|
+
</g>
|
|
874
|
+
|
|
875
|
+
{#if showVolume && d.volume && volumeScale}
|
|
876
|
+
{@const volumeBarHeight = volumeScale(volumeDomain[1] - d.volume)}
|
|
877
|
+
<rect
|
|
878
|
+
x={xScale(d.date)}
|
|
879
|
+
y={volumeTop + volumeBarHeight}
|
|
880
|
+
width={xScale.bandwidth()}
|
|
881
|
+
height={volumeHeight - volumeBarHeight}
|
|
882
|
+
class="candlestick-chart-volume is-{colorClass}"
|
|
883
|
+
/>
|
|
884
|
+
{/if}
|
|
885
|
+
{/each}
|
|
886
|
+
{/if}
|
|
887
|
+
|
|
888
|
+
{#each indicatorData as ind}
|
|
889
|
+
{#if 'values' in ind && ind.values}
|
|
890
|
+
<path
|
|
891
|
+
d={createIndicatorPath(ind.values)}
|
|
892
|
+
class="candlestick-chart-indicator"
|
|
893
|
+
fill="none"
|
|
894
|
+
stroke={getIndicatorColor(ind)}
|
|
895
|
+
stroke-width="1.5"
|
|
472
896
|
/>
|
|
473
|
-
|
|
897
|
+
{/if}
|
|
898
|
+
{#if 'bands' in ind && ind.bands}
|
|
899
|
+
<path
|
|
900
|
+
d={createIndicatorPath(ind.bands.upper)}
|
|
901
|
+
class="candlestick-chart-indicator candlestick-chart-bollinger-band"
|
|
902
|
+
fill="none"
|
|
903
|
+
stroke={getIndicatorColor(ind)}
|
|
904
|
+
stroke-width="1"
|
|
905
|
+
opacity="0.5"
|
|
906
|
+
/>
|
|
907
|
+
<path
|
|
908
|
+
d={createIndicatorPath(ind.bands.middle)}
|
|
909
|
+
class="candlestick-chart-indicator"
|
|
910
|
+
fill="none"
|
|
911
|
+
stroke={getIndicatorColor(ind)}
|
|
912
|
+
stroke-width="1.5"
|
|
913
|
+
/>
|
|
914
|
+
<path
|
|
915
|
+
d={createIndicatorPath(ind.bands.lower)}
|
|
916
|
+
class="candlestick-chart-indicator candlestick-chart-bollinger-band"
|
|
917
|
+
fill="none"
|
|
918
|
+
stroke={getIndicatorColor(ind)}
|
|
919
|
+
stroke-width="1"
|
|
920
|
+
opacity="0.5"
|
|
921
|
+
/>
|
|
922
|
+
{/if}
|
|
923
|
+
{/each}
|
|
474
924
|
|
|
475
|
-
|
|
476
|
-
|
|
925
|
+
{#if showLastPrice && lastCandle}
|
|
926
|
+
<line
|
|
927
|
+
x1={0}
|
|
928
|
+
y1={lastPriceY}
|
|
929
|
+
x2={innerWidth}
|
|
930
|
+
y2={lastPriceY}
|
|
931
|
+
class="candlestick-chart-last-price is-{lastCandle.close >= lastCandle.open
|
|
932
|
+
? bullishColor
|
|
933
|
+
: bearishColor}"
|
|
934
|
+
stroke-dasharray="4, 2"
|
|
935
|
+
/>
|
|
936
|
+
<rect
|
|
937
|
+
x={innerWidth + 2}
|
|
938
|
+
y={lastPriceY - 10}
|
|
939
|
+
width={50}
|
|
940
|
+
height={20}
|
|
941
|
+
rx="3"
|
|
942
|
+
class="candlestick-chart-last-price-label-bg is-{lastCandle.close >= lastCandle.open
|
|
943
|
+
? bullishColor
|
|
944
|
+
: bearishColor}"
|
|
945
|
+
/>
|
|
946
|
+
<text
|
|
947
|
+
x={innerWidth + 27}
|
|
948
|
+
y={lastPriceY + 4}
|
|
949
|
+
class="candlestick-chart-last-price-label"
|
|
950
|
+
text-anchor="middle"
|
|
951
|
+
font-size="10"
|
|
952
|
+
>
|
|
953
|
+
{formatPrice(lastPrice)}
|
|
954
|
+
</text>
|
|
955
|
+
{/if}
|
|
956
|
+
|
|
957
|
+
{#if showCrosshair && isCrosshairActive && crosshairPosition}
|
|
958
|
+
<line
|
|
959
|
+
x1={0}
|
|
960
|
+
y1={crosshairPosition.y}
|
|
961
|
+
x2={innerWidth}
|
|
962
|
+
y2={crosshairPosition.y}
|
|
963
|
+
class="candlestick-chart-crosshair"
|
|
964
|
+
/>
|
|
965
|
+
<line
|
|
966
|
+
x1={crosshairPosition.x}
|
|
967
|
+
y1={0}
|
|
968
|
+
x2={crosshairPosition.x}
|
|
969
|
+
y2={priceHeight}
|
|
970
|
+
class="candlestick-chart-crosshair"
|
|
971
|
+
/>
|
|
972
|
+
|
|
973
|
+
<rect
|
|
974
|
+
x={-55}
|
|
975
|
+
y={crosshairPosition.y - 10}
|
|
976
|
+
width={50}
|
|
977
|
+
height={20}
|
|
978
|
+
rx="3"
|
|
979
|
+
class="candlestick-chart-crosshair-label-bg"
|
|
980
|
+
/>
|
|
981
|
+
<text
|
|
982
|
+
x={-30}
|
|
983
|
+
y={crosshairPosition.y + 4}
|
|
984
|
+
class="candlestick-chart-crosshair-label"
|
|
985
|
+
text-anchor="middle"
|
|
986
|
+
font-size="10"
|
|
987
|
+
>
|
|
988
|
+
{formatPrice(crosshairPrice)}
|
|
989
|
+
</text>
|
|
990
|
+
|
|
991
|
+
{#if crosshairCandle}
|
|
477
992
|
<rect
|
|
478
|
-
x={xScale(
|
|
479
|
-
y={
|
|
480
|
-
width={
|
|
481
|
-
height={
|
|
482
|
-
|
|
993
|
+
x={xScale(crosshairCandle.date) + xScale.bandwidth() / 2 - 30}
|
|
994
|
+
y={priceHeight + 5}
|
|
995
|
+
width={60}
|
|
996
|
+
height={16}
|
|
997
|
+
rx="3"
|
|
998
|
+
class="candlestick-chart-crosshair-label-bg"
|
|
483
999
|
/>
|
|
1000
|
+
<text
|
|
1001
|
+
x={xScale(crosshairCandle.date) + xScale.bandwidth() / 2}
|
|
1002
|
+
y={priceHeight + 16}
|
|
1003
|
+
class="candlestick-chart-crosshair-label"
|
|
1004
|
+
text-anchor="middle"
|
|
1005
|
+
font-size="9"
|
|
1006
|
+
>
|
|
1007
|
+
{formatDate(crosshairCandle.date)}
|
|
1008
|
+
</text>
|
|
484
1009
|
{/if}
|
|
485
|
-
{/
|
|
1010
|
+
{/if}
|
|
486
1011
|
</g>
|
|
487
1012
|
</svg>
|
|
488
1013
|
</div>
|
|
@@ -492,35 +1017,58 @@
|
|
|
492
1017
|
class="candlestick-chart-tooltip"
|
|
493
1018
|
style="top: {tooltipPosition.y}px; left: {tooltipPosition.x}px;"
|
|
494
1019
|
>
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
1020
|
+
{#if tooltipContent}
|
|
1021
|
+
{@render tooltipContent({
|
|
1022
|
+
candle: tooltipData,
|
|
1023
|
+
change: tooltipChange.value,
|
|
1024
|
+
changePercent: tooltipChange.percent
|
|
1025
|
+
})}
|
|
1026
|
+
{:else}
|
|
1027
|
+
<div class="candlestick-chart-tooltip-content">
|
|
1028
|
+
<div class="candlestick-chart-tooltip-title">{formatDateTime(tooltipData.date)}</div>
|
|
1029
|
+
<div class="candlestick-chart-tooltip-rows">
|
|
1030
|
+
<div class="candlestick-chart-tooltip-row">
|
|
1031
|
+
<span class="candlestick-chart-tooltip-label">Open:</span>
|
|
1032
|
+
<span class="candlestick-chart-tooltip-value">{formatPrice(tooltipData.open)}</span>
|
|
1033
|
+
</div>
|
|
1034
|
+
<div class="candlestick-chart-tooltip-row">
|
|
1035
|
+
<span class="candlestick-chart-tooltip-label">High:</span>
|
|
1036
|
+
<span class="candlestick-chart-tooltip-value">{formatPrice(tooltipData.high)}</span>
|
|
1037
|
+
</div>
|
|
1038
|
+
<div class="candlestick-chart-tooltip-row">
|
|
1039
|
+
<span class="candlestick-chart-tooltip-label">Low:</span>
|
|
1040
|
+
<span class="candlestick-chart-tooltip-value">{formatPrice(tooltipData.low)}</span>
|
|
1041
|
+
</div>
|
|
1042
|
+
<div class="candlestick-chart-tooltip-row">
|
|
1043
|
+
<span class="candlestick-chart-tooltip-label">Close:</span>
|
|
1044
|
+
<span class="candlestick-chart-tooltip-value">{formatPrice(tooltipData.close)}</span
|
|
519
1045
|
>
|
|
520
1046
|
</div>
|
|
521
|
-
|
|
1047
|
+
<div class="candlestick-chart-tooltip-row candlestick-chart-tooltip-change">
|
|
1048
|
+
<span class="candlestick-chart-tooltip-label">Change:</span>
|
|
1049
|
+
<span
|
|
1050
|
+
class={cn(
|
|
1051
|
+
'candlestick-chart-tooltip-value',
|
|
1052
|
+
tooltipChange.value >= 0 ? 'is-bullish' : 'is-bearish'
|
|
1053
|
+
)}
|
|
1054
|
+
>
|
|
1055
|
+
{tooltipChange.value >= 0 ? '+' : ''}{formatPrice(tooltipChange.value)} ({tooltipChange.percent >=
|
|
1056
|
+
0
|
|
1057
|
+
? '+'
|
|
1058
|
+
: ''}{tooltipChange.percent.toFixed(2)}%)
|
|
1059
|
+
</span>
|
|
1060
|
+
</div>
|
|
1061
|
+
{#if showVolume && tooltipData.volume}
|
|
1062
|
+
<div class="candlestick-chart-tooltip-row candlestick-chart-tooltip-volume">
|
|
1063
|
+
<span class="candlestick-chart-tooltip-label">Volume:</span>
|
|
1064
|
+
<span class="candlestick-chart-tooltip-value"
|
|
1065
|
+
>{formatNumber(tooltipData.volume)}</span
|
|
1066
|
+
>
|
|
1067
|
+
</div>
|
|
1068
|
+
{/if}
|
|
1069
|
+
</div>
|
|
522
1070
|
</div>
|
|
523
|
-
|
|
1071
|
+
{/if}
|
|
524
1072
|
</div>
|
|
525
1073
|
{/if}
|
|
526
1074
|
{/if}
|