hyperprop-charting-library 0.1.142 → 0.1.144
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/hyperprop-charting-library.cjs +1382 -106
- package/dist/hyperprop-charting-library.d.ts +332 -2
- package/dist/hyperprop-charting-library.js +1380 -106
- package/dist/index.cjs +1382 -106
- package/dist/index.d.cts +332 -2
- package/dist/index.d.ts +332 -2
- package/dist/index.js +1380 -106
- package/docs/API.md +229 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -335,26 +335,45 @@ var getSeriesFingerprint = (data) => {
|
|
|
335
335
|
var withCachedSeries = (key, data, compute) => {
|
|
336
336
|
const fingerprint = getSeriesFingerprint(data);
|
|
337
337
|
const existing = builtInSeriesCache.get(key);
|
|
338
|
-
|
|
339
|
-
|
|
338
|
+
const shape = getSeriesShape(data);
|
|
339
|
+
if (existing) {
|
|
340
|
+
const unchanged = existing.fingerprint === fingerprint;
|
|
341
|
+
const liveTickWithinThrottle = !unchanged && indicatorLiveThrottleMs > 0 && existing.shape === shape && Date.now() - existing.computedAt < indicatorLiveThrottleMs;
|
|
342
|
+
if (unchanged || liveTickWithinThrottle) {
|
|
343
|
+
return existing.values;
|
|
344
|
+
}
|
|
340
345
|
}
|
|
341
346
|
const values = compute();
|
|
342
|
-
builtInSeriesCache.set(key, { fingerprint, values });
|
|
347
|
+
builtInSeriesCache.set(key, { fingerprint, values, shape, computedAt: Date.now() });
|
|
343
348
|
return values;
|
|
344
349
|
};
|
|
345
350
|
var builtInComputationCache = /* @__PURE__ */ new Map();
|
|
351
|
+
var indicatorLiveThrottleMs = 80;
|
|
352
|
+
var setIndicatorLiveThrottleMs = (ms) => {
|
|
353
|
+
indicatorLiveThrottleMs = Math.max(0, ms);
|
|
354
|
+
};
|
|
355
|
+
var getSeriesShape = (data) => {
|
|
356
|
+
const length = data.length;
|
|
357
|
+
const last = length > 0 ? data[length - 1] : void 0;
|
|
358
|
+
return last ? `${length}|${last.time.getTime()}` : "empty";
|
|
359
|
+
};
|
|
346
360
|
var COMPUTATION_CACHE_MAX_ENTRIES = 128;
|
|
347
361
|
var withCachedComputation = (key, data, compute) => {
|
|
348
362
|
const fingerprint = getSeriesFingerprint(data);
|
|
349
363
|
const existing = builtInComputationCache.get(key);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
364
|
+
const shape = getSeriesShape(data);
|
|
365
|
+
if (existing) {
|
|
366
|
+
const unchanged = existing.fingerprint === fingerprint;
|
|
367
|
+
const liveTickWithinThrottle = !unchanged && indicatorLiveThrottleMs > 0 && existing.shape === shape && Date.now() - existing.computedAt < indicatorLiveThrottleMs;
|
|
368
|
+
if (unchanged || liveTickWithinThrottle) {
|
|
369
|
+
builtInComputationCache.delete(key);
|
|
370
|
+
builtInComputationCache.set(key, existing);
|
|
371
|
+
return existing.value;
|
|
372
|
+
}
|
|
354
373
|
}
|
|
355
374
|
const value = compute();
|
|
356
375
|
builtInComputationCache.delete(key);
|
|
357
|
-
builtInComputationCache.set(key, { fingerprint, value });
|
|
376
|
+
builtInComputationCache.set(key, { fingerprint, value, shape, computedAt: Date.now() });
|
|
358
377
|
while (builtInComputationCache.size > COMPUTATION_CACHE_MAX_ENTRIES) {
|
|
359
378
|
const oldest = builtInComputationCache.keys().next().value;
|
|
360
379
|
if (oldest === void 0) break;
|
|
@@ -2724,6 +2743,237 @@ var CHART_THEMES = {
|
|
|
2724
2743
|
var CHART_THEME_NAMES = Object.keys(CHART_THEMES);
|
|
2725
2744
|
var resolveTheme = (theme) => typeof theme === "string" ? CHART_THEMES[theme] ?? CHART_THEMES.midnight : theme;
|
|
2726
2745
|
|
|
2746
|
+
// src/time.ts
|
|
2747
|
+
var DAY_MS = 864e5;
|
|
2748
|
+
var HOUR_MS = 36e5;
|
|
2749
|
+
var OFFSET_FORMATTER_CACHE = /* @__PURE__ */ new Map();
|
|
2750
|
+
var LABEL_FORMATTER_CACHE = /* @__PURE__ */ new Map();
|
|
2751
|
+
var getOffsetFormatter = (timeZone) => {
|
|
2752
|
+
let formatter = OFFSET_FORMATTER_CACHE.get(timeZone);
|
|
2753
|
+
if (!formatter) {
|
|
2754
|
+
formatter = new Intl.DateTimeFormat("en-US", {
|
|
2755
|
+
timeZone,
|
|
2756
|
+
hourCycle: "h23",
|
|
2757
|
+
year: "numeric",
|
|
2758
|
+
month: "2-digit",
|
|
2759
|
+
day: "2-digit",
|
|
2760
|
+
hour: "2-digit",
|
|
2761
|
+
minute: "2-digit",
|
|
2762
|
+
second: "2-digit"
|
|
2763
|
+
});
|
|
2764
|
+
OFFSET_FORMATTER_CACHE.set(timeZone, formatter);
|
|
2765
|
+
}
|
|
2766
|
+
return formatter;
|
|
2767
|
+
};
|
|
2768
|
+
var getLabelFormatter = (timeZone, options, key) => {
|
|
2769
|
+
const cacheKey = `${timeZone ?? "local"}|${key}`;
|
|
2770
|
+
let formatter = LABEL_FORMATTER_CACHE.get(cacheKey);
|
|
2771
|
+
if (!formatter) {
|
|
2772
|
+
formatter = new Intl.DateTimeFormat(void 0, timeZone ? { ...options, timeZone } : options);
|
|
2773
|
+
LABEL_FORMATTER_CACHE.set(cacheKey, formatter);
|
|
2774
|
+
}
|
|
2775
|
+
return formatter;
|
|
2776
|
+
};
|
|
2777
|
+
var isValidTimeZone = (timeZone) => {
|
|
2778
|
+
try {
|
|
2779
|
+
new Intl.DateTimeFormat("en-US", { timeZone });
|
|
2780
|
+
return true;
|
|
2781
|
+
} catch {
|
|
2782
|
+
return false;
|
|
2783
|
+
}
|
|
2784
|
+
};
|
|
2785
|
+
var floorDiv = (value, divisor) => Math.floor(value / divisor);
|
|
2786
|
+
var positiveMod = (value, divisor) => (value % divisor + divisor) % divisor;
|
|
2787
|
+
var computeOffsetMs = (timeZone, ms) => {
|
|
2788
|
+
const parts = getOffsetFormatter(timeZone).formatToParts(new Date(ms));
|
|
2789
|
+
let year = 0;
|
|
2790
|
+
let month = 1;
|
|
2791
|
+
let day = 1;
|
|
2792
|
+
let hour = 0;
|
|
2793
|
+
let minute = 0;
|
|
2794
|
+
let second = 0;
|
|
2795
|
+
for (const part of parts) {
|
|
2796
|
+
const value = Number(part.value);
|
|
2797
|
+
if (part.type === "year") year = value;
|
|
2798
|
+
else if (part.type === "month") month = value;
|
|
2799
|
+
else if (part.type === "day") day = value;
|
|
2800
|
+
else if (part.type === "hour") hour = value === 24 ? 0 : value;
|
|
2801
|
+
else if (part.type === "minute") minute = value;
|
|
2802
|
+
else if (part.type === "second") second = value;
|
|
2803
|
+
}
|
|
2804
|
+
const asUtc = Date.UTC(year, month - 1, day, hour, minute, second);
|
|
2805
|
+
return asUtc - Math.floor(ms / 1e3) * 1e3;
|
|
2806
|
+
};
|
|
2807
|
+
var createZoneClock = (timeZone) => {
|
|
2808
|
+
const normalized = !timeZone || timeZone === "local" ? null : timeZone.toLowerCase() === "utc" ? "UTC" : isValidTimeZone(timeZone) ? timeZone : null;
|
|
2809
|
+
if (normalized === null) {
|
|
2810
|
+
const scratch = /* @__PURE__ */ new Date();
|
|
2811
|
+
const partsOf2 = (ms) => {
|
|
2812
|
+
scratch.setTime(ms);
|
|
2813
|
+
return {
|
|
2814
|
+
year: scratch.getFullYear(),
|
|
2815
|
+
month: scratch.getMonth() + 1,
|
|
2816
|
+
day: scratch.getDate(),
|
|
2817
|
+
hour: scratch.getHours(),
|
|
2818
|
+
minute: scratch.getMinutes()
|
|
2819
|
+
};
|
|
2820
|
+
};
|
|
2821
|
+
return {
|
|
2822
|
+
timeZone: null,
|
|
2823
|
+
offsetMs: (ms) => {
|
|
2824
|
+
scratch.setTime(ms);
|
|
2825
|
+
return -scratch.getTimezoneOffset() * 6e4;
|
|
2826
|
+
},
|
|
2827
|
+
dayKey: (ms) => {
|
|
2828
|
+
scratch.setTime(ms);
|
|
2829
|
+
return floorDiv(Date.UTC(scratch.getFullYear(), scratch.getMonth(), scratch.getDate()), DAY_MS);
|
|
2830
|
+
},
|
|
2831
|
+
minutesOfDay: (ms) => {
|
|
2832
|
+
scratch.setTime(ms);
|
|
2833
|
+
return scratch.getHours() * 60 + scratch.getMinutes();
|
|
2834
|
+
},
|
|
2835
|
+
weekday: (ms) => {
|
|
2836
|
+
scratch.setTime(ms);
|
|
2837
|
+
return scratch.getDay();
|
|
2838
|
+
},
|
|
2839
|
+
parts: partsOf2,
|
|
2840
|
+
formatTime: (ms, hour12) => formatClockTime(partsOf2(ms), hour12),
|
|
2841
|
+
formatDayMonth: (ms) => getLabelFormatter(null, { month: "short", day: "numeric" }, "md").format(ms),
|
|
2842
|
+
formatMonth: (ms) => getLabelFormatter(null, { month: "short" }, "m").format(ms),
|
|
2843
|
+
formatYear: (ms) => String(partsOf2(ms).year)
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
2846
|
+
const zone = normalized;
|
|
2847
|
+
const offsetCache = /* @__PURE__ */ new Map();
|
|
2848
|
+
const offsetMs = (ms) => {
|
|
2849
|
+
const bucket = floorDiv(ms, HOUR_MS);
|
|
2850
|
+
const cached = offsetCache.get(bucket);
|
|
2851
|
+
if (cached !== void 0) return cached;
|
|
2852
|
+
const offset = computeOffsetMs(zone, ms);
|
|
2853
|
+
if (offsetCache.size > 2e4) offsetCache.clear();
|
|
2854
|
+
offsetCache.set(bucket, offset);
|
|
2855
|
+
return offset;
|
|
2856
|
+
};
|
|
2857
|
+
const localMs = (ms) => ms + offsetMs(ms);
|
|
2858
|
+
const partsOf = (ms) => {
|
|
2859
|
+
const local = new Date(localMs(ms));
|
|
2860
|
+
return {
|
|
2861
|
+
year: local.getUTCFullYear(),
|
|
2862
|
+
month: local.getUTCMonth() + 1,
|
|
2863
|
+
day: local.getUTCDate(),
|
|
2864
|
+
hour: local.getUTCHours(),
|
|
2865
|
+
minute: local.getUTCMinutes()
|
|
2866
|
+
};
|
|
2867
|
+
};
|
|
2868
|
+
return {
|
|
2869
|
+
timeZone: zone,
|
|
2870
|
+
offsetMs,
|
|
2871
|
+
dayKey: (ms) => floorDiv(localMs(ms), DAY_MS),
|
|
2872
|
+
minutesOfDay: (ms) => floorDiv(positiveMod(localMs(ms), DAY_MS), 6e4),
|
|
2873
|
+
// 1970-01-01 was a Thursday (index 4).
|
|
2874
|
+
weekday: (ms) => positiveMod(floorDiv(localMs(ms), DAY_MS) + 4, 7),
|
|
2875
|
+
parts: partsOf,
|
|
2876
|
+
formatTime: (ms, hour12) => formatClockTime(partsOf(ms), hour12),
|
|
2877
|
+
formatDayMonth: (ms) => getLabelFormatter(zone, { month: "short", day: "numeric" }, "md").format(ms),
|
|
2878
|
+
formatMonth: (ms) => getLabelFormatter(zone, { month: "short" }, "m").format(ms),
|
|
2879
|
+
formatYear: (ms) => String(partsOf(ms).year)
|
|
2880
|
+
};
|
|
2881
|
+
};
|
|
2882
|
+
var pad2 = (value) => value < 10 ? `0${value}` : String(value);
|
|
2883
|
+
var formatClockTime = (parts, hour12) => {
|
|
2884
|
+
if (!hour12) return `${pad2(parts.hour)}:${pad2(parts.minute)}`;
|
|
2885
|
+
const suffix = parts.hour < 12 ? "AM" : "PM";
|
|
2886
|
+
const hour = parts.hour % 12 === 0 ? 12 : parts.hour % 12;
|
|
2887
|
+
return `${hour}:${pad2(parts.minute)} ${suffix}`;
|
|
2888
|
+
};
|
|
2889
|
+
var parseHhMm = (value) => {
|
|
2890
|
+
const match = /^(\d{1,2}):?(\d{2})$/.exec(value.trim());
|
|
2891
|
+
if (!match) return null;
|
|
2892
|
+
const hour = Number(match[1]);
|
|
2893
|
+
const minute = Number(match[2]);
|
|
2894
|
+
if (!Number.isFinite(hour) || !Number.isFinite(minute)) return null;
|
|
2895
|
+
if (hour < 0 || hour > 24 || minute < 0 || minute > 59) return null;
|
|
2896
|
+
return hour * 60 + minute;
|
|
2897
|
+
};
|
|
2898
|
+
var SESSION_PRESETS = {
|
|
2899
|
+
"cme-futures": {
|
|
2900
|
+
timezone: "America/New_York",
|
|
2901
|
+
// Opening days: the session opens Sunday–Thursday evening and runs into
|
|
2902
|
+
// the following morning, so Friday evening (weekend) is correctly closed.
|
|
2903
|
+
days: [0, 1, 2, 3, 4],
|
|
2904
|
+
segments: [{ start: "18:00", end: "17:00" }]
|
|
2905
|
+
},
|
|
2906
|
+
"cme-rth": {
|
|
2907
|
+
timezone: "America/New_York",
|
|
2908
|
+
days: [1, 2, 3, 4, 5],
|
|
2909
|
+
segments: [{ start: "09:30", end: "16:00" }]
|
|
2910
|
+
},
|
|
2911
|
+
"us-equities": {
|
|
2912
|
+
timezone: "America/New_York",
|
|
2913
|
+
days: [1, 2, 3, 4, 5],
|
|
2914
|
+
segments: [{ start: "09:30", end: "16:00" }]
|
|
2915
|
+
},
|
|
2916
|
+
"us-equities-extended": {
|
|
2917
|
+
timezone: "America/New_York",
|
|
2918
|
+
days: [1, 2, 3, 4, 5],
|
|
2919
|
+
segments: [{ start: "04:00", end: "20:00" }]
|
|
2920
|
+
},
|
|
2921
|
+
"24x7": {
|
|
2922
|
+
days: [0, 1, 2, 3, 4, 5, 6],
|
|
2923
|
+
segments: [{ start: "00:00", end: "24:00" }]
|
|
2924
|
+
}
|
|
2925
|
+
};
|
|
2926
|
+
var resolveSessionSpec = (spec) => {
|
|
2927
|
+
if (!spec) return null;
|
|
2928
|
+
if (typeof spec === "string") return SESSION_PRESETS[spec] ?? null;
|
|
2929
|
+
return spec;
|
|
2930
|
+
};
|
|
2931
|
+
var compileSession = (spec) => {
|
|
2932
|
+
const resolved = resolveSessionSpec(spec);
|
|
2933
|
+
if (!resolved) return null;
|
|
2934
|
+
const rawSegments = resolved.segments?.length ? resolved.segments : [{ start: "00:00", end: "24:00" }];
|
|
2935
|
+
const segments = [];
|
|
2936
|
+
for (const segment of rawSegments) {
|
|
2937
|
+
const startMinutes = parseHhMm(segment.start);
|
|
2938
|
+
const endMinutesRaw = parseHhMm(segment.end);
|
|
2939
|
+
if (startMinutes === null || endMinutesRaw === null) continue;
|
|
2940
|
+
const endMinutes = endMinutesRaw === 0 ? 1440 : endMinutesRaw;
|
|
2941
|
+
segments.push({ startMinutes, endMinutes, wraps: endMinutes <= startMinutes });
|
|
2942
|
+
}
|
|
2943
|
+
if (segments.length === 0) return null;
|
|
2944
|
+
return {
|
|
2945
|
+
clock: createZoneClock(resolved.timezone ?? null),
|
|
2946
|
+
days: new Set(resolved.days ?? [1, 2, 3, 4, 5]),
|
|
2947
|
+
segments,
|
|
2948
|
+
overnight: segments.some((segment) => segment.wraps)
|
|
2949
|
+
};
|
|
2950
|
+
};
|
|
2951
|
+
var sessionInfoAt = (session, ms) => {
|
|
2952
|
+
const { clock } = session;
|
|
2953
|
+
const dayKey = clock.dayKey(ms);
|
|
2954
|
+
const minutes = clock.minutesOfDay(ms);
|
|
2955
|
+
const weekday = clock.weekday(ms);
|
|
2956
|
+
let sessionDay = dayKey;
|
|
2957
|
+
let inSession = false;
|
|
2958
|
+
for (const segment of session.segments) {
|
|
2959
|
+
if (segment.wraps) {
|
|
2960
|
+
if (minutes >= segment.startMinutes) {
|
|
2961
|
+
if (session.days.has(weekday)) {
|
|
2962
|
+
inSession = true;
|
|
2963
|
+
sessionDay = dayKey + 1;
|
|
2964
|
+
}
|
|
2965
|
+
} else if (minutes < segment.endMinutes) {
|
|
2966
|
+
const openedWeekday = positiveMod(weekday - 1, 7);
|
|
2967
|
+
if (session.days.has(openedWeekday)) inSession = true;
|
|
2968
|
+
sessionDay = dayKey;
|
|
2969
|
+
}
|
|
2970
|
+
} else if (minutes >= segment.startMinutes && minutes < segment.endMinutes && session.days.has(weekday)) {
|
|
2971
|
+
inSession = true;
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
return { inSession, sessionDay };
|
|
2975
|
+
};
|
|
2976
|
+
|
|
2727
2977
|
// src/options.ts
|
|
2728
2978
|
var DEFAULT_GRID_OPTIONS = {
|
|
2729
2979
|
color: "#2b2f38",
|
|
@@ -2924,10 +3174,36 @@ var DEFAULT_OPTIONS = {
|
|
|
2924
3174
|
accessibility: { label: "Price chart", description: "", focusable: true },
|
|
2925
3175
|
downsampling: { enabled: true, thresholdPx: 1.5 },
|
|
2926
3176
|
touch: { hitToleranceScale: 2.2, longPressTooltip: true, longPressMs: 400 },
|
|
3177
|
+
dataLine: {
|
|
3178
|
+
visible: false,
|
|
3179
|
+
symbol: "",
|
|
3180
|
+
interval: "",
|
|
3181
|
+
exchange: "",
|
|
3182
|
+
showOhlc: true,
|
|
3183
|
+
showChange: true,
|
|
3184
|
+
showVolume: false,
|
|
3185
|
+
details: [],
|
|
3186
|
+
statusColor: "",
|
|
3187
|
+
fontSize: 0,
|
|
3188
|
+
symbolColor: "",
|
|
3189
|
+
textColor: ""
|
|
3190
|
+
},
|
|
2927
3191
|
datafeedOptions: { prefetchThresholdBars: 150, cooldownMs: 1200, chunkBars: 1500 },
|
|
3192
|
+
animation: { viewportTransitions: true, durationMs: 260 },
|
|
3193
|
+
indicatorUpdate: { liveThrottleMs: 80 },
|
|
3194
|
+
timezone: "local",
|
|
3195
|
+
timeFormat: "24h",
|
|
3196
|
+
session: {
|
|
3197
|
+
spec: null,
|
|
3198
|
+
separators: true,
|
|
3199
|
+
highlightOutOfSession: false,
|
|
3200
|
+
outOfSessionColor: "rgba(120,132,158,0.07)"
|
|
3201
|
+
},
|
|
3202
|
+
history: { enabled: true, limit: 100 },
|
|
2928
3203
|
crosshair: DEFAULT_CROSSHAIR_OPTIONS,
|
|
2929
3204
|
grid: DEFAULT_GRID_OPTIONS,
|
|
2930
3205
|
watermark: DEFAULT_WATERMARK_OPTIONS,
|
|
3206
|
+
alerts: [],
|
|
2931
3207
|
priceLines: [],
|
|
2932
3208
|
orderLines: [],
|
|
2933
3209
|
tickerLine: {
|
|
@@ -2983,6 +3259,26 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
|
|
|
2983
3259
|
...baseOptions.touch,
|
|
2984
3260
|
...options.touch ?? {}
|
|
2985
3261
|
},
|
|
3262
|
+
dataLine: {
|
|
3263
|
+
...baseOptions.dataLine,
|
|
3264
|
+
...options.dataLine ?? {}
|
|
3265
|
+
},
|
|
3266
|
+
animation: {
|
|
3267
|
+
...baseOptions.animation,
|
|
3268
|
+
...options.animation ?? {}
|
|
3269
|
+
},
|
|
3270
|
+
indicatorUpdate: {
|
|
3271
|
+
...baseOptions.indicatorUpdate,
|
|
3272
|
+
...options.indicatorUpdate ?? {}
|
|
3273
|
+
},
|
|
3274
|
+
session: {
|
|
3275
|
+
...baseOptions.session,
|
|
3276
|
+
...options.session ?? {}
|
|
3277
|
+
},
|
|
3278
|
+
history: {
|
|
3279
|
+
...baseOptions.history,
|
|
3280
|
+
...options.history ?? {}
|
|
3281
|
+
},
|
|
2986
3282
|
datafeedOptions: {
|
|
2987
3283
|
...baseOptions.datafeedOptions,
|
|
2988
3284
|
...options.datafeedOptions ?? {}
|
|
@@ -3146,6 +3442,45 @@ function createChart(element, options = {}) {
|
|
|
3146
3442
|
...drawing.label === void 0 ? {} : { label: drawing.label }
|
|
3147
3443
|
});
|
|
3148
3444
|
let tradeMarkers = [];
|
|
3445
|
+
let timezoneSetting = mergedOptions.timezone ?? "local";
|
|
3446
|
+
let zoneClock = createZoneClock(timezoneSetting);
|
|
3447
|
+
let timeFormat = mergedOptions.timeFormat === "12h" ? "12h" : "24h";
|
|
3448
|
+
const DEFAULT_SESSION_OPTIONS = {
|
|
3449
|
+
spec: null,
|
|
3450
|
+
separators: true,
|
|
3451
|
+
highlightOutOfSession: false,
|
|
3452
|
+
outOfSessionColor: "rgba(120,132,158,0.07)"
|
|
3453
|
+
};
|
|
3454
|
+
let sessionOptions = {
|
|
3455
|
+
...DEFAULT_SESSION_OPTIONS,
|
|
3456
|
+
...mergedOptions.session ?? {}
|
|
3457
|
+
};
|
|
3458
|
+
let compiledSession = compileSession(sessionOptions.spec);
|
|
3459
|
+
const isHour12 = () => timeFormat === "12h";
|
|
3460
|
+
const formatClock = (time) => zoneClock.formatTime(time.getTime(), isHour12());
|
|
3461
|
+
let barMarks = [];
|
|
3462
|
+
let timescaleMarks = [];
|
|
3463
|
+
let markRegions = [];
|
|
3464
|
+
let hoveredMarkKey = null;
|
|
3465
|
+
let markClickHandler = null;
|
|
3466
|
+
let markHoverHandler = null;
|
|
3467
|
+
let alerts = [];
|
|
3468
|
+
let generatedAlertId = 1;
|
|
3469
|
+
let alertTriggerHandler = null;
|
|
3470
|
+
let alertActionHandler = null;
|
|
3471
|
+
let alertRegions = [];
|
|
3472
|
+
let alertDragState = null;
|
|
3473
|
+
let hoveredAlertId = null;
|
|
3474
|
+
let replay = null;
|
|
3475
|
+
let replayStateHandler = null;
|
|
3476
|
+
const historyEnabled = mergedOptions.history?.enabled !== false;
|
|
3477
|
+
const historyLimit = Math.max(1, Math.floor(Number(mergedOptions.history?.limit ?? 100)) || 100);
|
|
3478
|
+
let historyPast = [];
|
|
3479
|
+
let historyFuture = [];
|
|
3480
|
+
let historyBaseline = "";
|
|
3481
|
+
let historySuspended = 0;
|
|
3482
|
+
let undoRedoStateHandler = null;
|
|
3483
|
+
let lastUndoRedoSignature = "";
|
|
3149
3484
|
let indicators = (options.indicators ?? []).map((indicator) => normalizeIndicatorState(indicator));
|
|
3150
3485
|
let drawings = dedupeDrawingIds(
|
|
3151
3486
|
(options.drawings ?? []).map((drawing) => normalizeDrawingState(drawing))
|
|
@@ -3194,9 +3529,168 @@ function createChart(element, options = {}) {
|
|
|
3194
3529
|
let selectedDrawingId = null;
|
|
3195
3530
|
const drawingToolDefaults = /* @__PURE__ */ new Map();
|
|
3196
3531
|
const getDrawingToolDefaults = (tool) => drawingToolDefaults.get(tool) ?? {};
|
|
3532
|
+
const historyDoc = () => JSON.stringify(drawings.map((drawing) => serializeDrawing(drawing)));
|
|
3533
|
+
const describeHistoryChange = (beforeDoc, afterDoc) => {
|
|
3534
|
+
let before = [];
|
|
3535
|
+
let after = [];
|
|
3536
|
+
try {
|
|
3537
|
+
before = JSON.parse(beforeDoc);
|
|
3538
|
+
after = JSON.parse(afterDoc);
|
|
3539
|
+
} catch {
|
|
3540
|
+
return "Edit drawings";
|
|
3541
|
+
}
|
|
3542
|
+
const label = (type) => type ? type.replace(/-/g, " ") : "drawing";
|
|
3543
|
+
if (after.length > before.length) {
|
|
3544
|
+
const beforeIds = new Set(before.map((drawing) => drawing.id));
|
|
3545
|
+
const added = after.find((drawing) => !beforeIds.has(drawing.id));
|
|
3546
|
+
return `Add ${label(added?.type)}`;
|
|
3547
|
+
}
|
|
3548
|
+
if (after.length < before.length) {
|
|
3549
|
+
if (after.length === 0 && before.length > 1) return "Clear drawings";
|
|
3550
|
+
const afterIds = new Set(after.map((drawing) => drawing.id));
|
|
3551
|
+
const removed = before.find((drawing) => !afterIds.has(drawing.id));
|
|
3552
|
+
return `Delete ${label(removed?.type)}`;
|
|
3553
|
+
}
|
|
3554
|
+
const changed = after.find((drawing, index) => JSON.stringify(drawing) !== JSON.stringify(before[index]));
|
|
3555
|
+
return `Edit ${label(changed?.type)}`;
|
|
3556
|
+
};
|
|
3557
|
+
const getUndoRedoState = () => ({
|
|
3558
|
+
canUndo: historyPast.length > 0,
|
|
3559
|
+
canRedo: historyFuture.length > 0,
|
|
3560
|
+
undoLabel: historyPast.length > 0 ? historyPast[historyPast.length - 1].label : null,
|
|
3561
|
+
redoLabel: historyFuture.length > 0 ? historyFuture[historyFuture.length - 1].label : null
|
|
3562
|
+
});
|
|
3563
|
+
const emitUndoRedoState = () => {
|
|
3564
|
+
const state = getUndoRedoState();
|
|
3565
|
+
const signature = `${state.canUndo}|${state.canRedo}|${state.undoLabel}|${state.redoLabel}`;
|
|
3566
|
+
if (signature === lastUndoRedoSignature) return;
|
|
3567
|
+
lastUndoRedoSignature = signature;
|
|
3568
|
+
undoRedoStateHandler?.(state);
|
|
3569
|
+
};
|
|
3570
|
+
const commitHistory = () => {
|
|
3571
|
+
if (!historyEnabled || historySuspended > 0) return;
|
|
3572
|
+
if (drawingDragState !== null) return;
|
|
3573
|
+
const next = historyDoc();
|
|
3574
|
+
if (next === historyBaseline) return;
|
|
3575
|
+
historyPast.push({ label: describeHistoryChange(historyBaseline, next), doc: historyBaseline });
|
|
3576
|
+
if (historyPast.length > historyLimit) historyPast.shift();
|
|
3577
|
+
historyFuture = [];
|
|
3578
|
+
historyBaseline = next;
|
|
3579
|
+
emitUndoRedoState();
|
|
3580
|
+
};
|
|
3581
|
+
const resetHistoryBaseline = () => {
|
|
3582
|
+
historyBaseline = historyDoc();
|
|
3583
|
+
};
|
|
3584
|
+
const applyHistoryDoc = (doc) => {
|
|
3585
|
+
let parsed;
|
|
3586
|
+
try {
|
|
3587
|
+
parsed = JSON.parse(doc);
|
|
3588
|
+
} catch {
|
|
3589
|
+
return;
|
|
3590
|
+
}
|
|
3591
|
+
historySuspended += 1;
|
|
3592
|
+
try {
|
|
3593
|
+
drawings = dedupeDrawingIds(parsed.map((drawing) => normalizeDrawingState(drawing)));
|
|
3594
|
+
if (selectedDrawingId !== null && !drawings.some((drawing) => drawing.id === selectedDrawingId)) {
|
|
3595
|
+
selectedDrawingId = null;
|
|
3596
|
+
}
|
|
3597
|
+
draftDrawing = null;
|
|
3598
|
+
drawingDragState = null;
|
|
3599
|
+
historyBaseline = doc;
|
|
3600
|
+
emitDrawingsChange();
|
|
3601
|
+
scheduleDraw({ updateAutoScale: true });
|
|
3602
|
+
} finally {
|
|
3603
|
+
historySuspended -= 1;
|
|
3604
|
+
}
|
|
3605
|
+
};
|
|
3606
|
+
const undo = () => {
|
|
3607
|
+
if (!historyEnabled || historyPast.length === 0) return false;
|
|
3608
|
+
const entry = historyPast.pop();
|
|
3609
|
+
historyFuture.push({ label: entry.label, doc: historyBaseline });
|
|
3610
|
+
applyHistoryDoc(entry.doc);
|
|
3611
|
+
emitUndoRedoState();
|
|
3612
|
+
return true;
|
|
3613
|
+
};
|
|
3614
|
+
const redo = () => {
|
|
3615
|
+
if (!historyEnabled || historyFuture.length === 0) return false;
|
|
3616
|
+
const entry = historyFuture.pop();
|
|
3617
|
+
historyPast.push({ label: entry.label, doc: historyBaseline });
|
|
3618
|
+
applyHistoryDoc(entry.doc);
|
|
3619
|
+
emitUndoRedoState();
|
|
3620
|
+
return true;
|
|
3621
|
+
};
|
|
3622
|
+
const clearHistory = () => {
|
|
3623
|
+
historyPast = [];
|
|
3624
|
+
historyFuture = [];
|
|
3625
|
+
resetHistoryBaseline();
|
|
3626
|
+
emitUndoRedoState();
|
|
3627
|
+
};
|
|
3197
3628
|
const emitDrawingsChange = () => {
|
|
3629
|
+
commitHistory();
|
|
3198
3630
|
drawingsChangeHandler?.(drawings.map((drawing) => serializeDrawing(drawing)));
|
|
3199
3631
|
};
|
|
3632
|
+
const normalizeAlert = (alert) => ({
|
|
3633
|
+
id: alert.id ?? `alert-${generatedAlertId++}`,
|
|
3634
|
+
price: Number(alert.price),
|
|
3635
|
+
condition: alert.condition ?? "crossing",
|
|
3636
|
+
label: alert.label ?? "",
|
|
3637
|
+
color: alert.color ?? "#f7a600",
|
|
3638
|
+
active: alert.active !== false,
|
|
3639
|
+
triggered: alert.triggered === true,
|
|
3640
|
+
once: alert.once !== false,
|
|
3641
|
+
draggable: alert.draggable !== false,
|
|
3642
|
+
lastPrice: null
|
|
3643
|
+
});
|
|
3644
|
+
const serializeAlert = (alert) => ({
|
|
3645
|
+
id: alert.id,
|
|
3646
|
+
price: alert.price,
|
|
3647
|
+
condition: alert.condition,
|
|
3648
|
+
label: alert.label,
|
|
3649
|
+
color: alert.color,
|
|
3650
|
+
active: alert.active,
|
|
3651
|
+
triggered: alert.triggered,
|
|
3652
|
+
once: alert.once,
|
|
3653
|
+
draggable: alert.draggable
|
|
3654
|
+
});
|
|
3655
|
+
const evaluateAlerts = (price, timeMs, index) => {
|
|
3656
|
+
if (alerts.length === 0 || !Number.isFinite(price)) return;
|
|
3657
|
+
for (const alert of alerts) {
|
|
3658
|
+
if (!alert.active || alert.triggered || !Number.isFinite(alert.price)) {
|
|
3659
|
+
alert.lastPrice = price;
|
|
3660
|
+
continue;
|
|
3661
|
+
}
|
|
3662
|
+
const previous = alert.lastPrice;
|
|
3663
|
+
let hit = false;
|
|
3664
|
+
switch (alert.condition) {
|
|
3665
|
+
case "greater":
|
|
3666
|
+
hit = price > alert.price;
|
|
3667
|
+
break;
|
|
3668
|
+
case "less":
|
|
3669
|
+
hit = price < alert.price;
|
|
3670
|
+
break;
|
|
3671
|
+
case "crossing-up":
|
|
3672
|
+
hit = previous !== null && previous < alert.price && price >= alert.price;
|
|
3673
|
+
break;
|
|
3674
|
+
case "crossing-down":
|
|
3675
|
+
hit = previous !== null && previous > alert.price && price <= alert.price;
|
|
3676
|
+
break;
|
|
3677
|
+
default:
|
|
3678
|
+
hit = previous !== null && (previous < alert.price && price >= alert.price || previous > alert.price && price <= alert.price);
|
|
3679
|
+
break;
|
|
3680
|
+
}
|
|
3681
|
+
alert.lastPrice = price;
|
|
3682
|
+
if (!hit) continue;
|
|
3683
|
+
alert.triggered = true;
|
|
3684
|
+
if (alert.once) alert.active = false;
|
|
3685
|
+
alertTriggerHandler?.({ alert: serializeAlert(alert), price, timeMs, index });
|
|
3686
|
+
scheduleDraw({ overlayOnly: false });
|
|
3687
|
+
}
|
|
3688
|
+
};
|
|
3689
|
+
const evaluateAlertsFromLatestBar = () => {
|
|
3690
|
+
if (alerts.length === 0 || data.length === 0) return;
|
|
3691
|
+
const last = data[data.length - 1];
|
|
3692
|
+
evaluateAlerts(last.c, last.time.getTime(), data.length - 1);
|
|
3693
|
+
};
|
|
3200
3694
|
const orderWidgetWidthById = /* @__PURE__ */ new Map();
|
|
3201
3695
|
const orderPriceTagWidthById = /* @__PURE__ */ new Map();
|
|
3202
3696
|
let xCenter = 0;
|
|
@@ -3242,6 +3736,7 @@ function createChart(element, options = {}) {
|
|
|
3242
3736
|
let crosshairPoint = null;
|
|
3243
3737
|
let doubleClickEnabled = mergedOptions.doubleClickEnabled;
|
|
3244
3738
|
let doubleClickAction = mergedOptions.doubleClickAction;
|
|
3739
|
+
setIndicatorLiveThrottleMs(mergedOptions.indicatorUpdate?.liveThrottleMs ?? 80);
|
|
3245
3740
|
let noOverlappingLineLabels = DEFAULT_LABELS_OPTIONS.noOverlapping;
|
|
3246
3741
|
let rightAxisLabelSlots = [];
|
|
3247
3742
|
let plotLabelSlots = [];
|
|
@@ -3368,6 +3863,9 @@ function createChart(element, options = {}) {
|
|
|
3368
3863
|
let overlayLayout = null;
|
|
3369
3864
|
const margin = { top: 16, right: 72, bottom: 26, left: 12 };
|
|
3370
3865
|
let cachedRightMargin = margin.right;
|
|
3866
|
+
let rightMarginShrinkPendingSince = null;
|
|
3867
|
+
const RIGHT_MARGIN_SHRINK_DELAY_MS = 600;
|
|
3868
|
+
const RIGHT_MARGIN_SHRINK_EPSILON_PX = 2;
|
|
3371
3869
|
const minVisibleBars = Math.max(1, Math.floor(mergedOptions.minVisibleBars));
|
|
3372
3870
|
const maxVisibleBars = Math.max(minVisibleBars, Math.floor(mergedOptions.maxVisibleBars));
|
|
3373
3871
|
const maxPanBars = Math.max(0, Math.floor(mergedOptions.maxPanBars));
|
|
@@ -3483,21 +3981,69 @@ function createChart(element, options = {}) {
|
|
|
3483
3981
|
Math.min(highCenter, count + maxPanBars)
|
|
3484
3982
|
);
|
|
3485
3983
|
};
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
if (
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3984
|
+
let viewportTweenRaf = null;
|
|
3985
|
+
const cancelViewportTween = () => {
|
|
3986
|
+
if (viewportTweenRaf !== null) {
|
|
3987
|
+
cancelAnimationFrame(viewportTweenRaf);
|
|
3988
|
+
viewportTweenRaf = null;
|
|
3989
|
+
}
|
|
3990
|
+
};
|
|
3991
|
+
const prefersReducedMotion = () => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
3992
|
+
const animateViewportTo = (targetCenter, targetSpan, onComplete) => {
|
|
3993
|
+
const animation = mergedOptions.animation ?? DEFAULT_OPTIONS.animation;
|
|
3994
|
+
const duration = Math.max(0, animation?.durationMs ?? 260);
|
|
3995
|
+
const startCenter = xCenter;
|
|
3996
|
+
const startSpan = xSpan;
|
|
3997
|
+
const centerDelta = targetCenter - startCenter;
|
|
3998
|
+
const spanRatio = startSpan > 0 ? targetSpan / startSpan : 1;
|
|
3999
|
+
const negligible = Math.abs(centerDelta) < 0.5 && Math.abs(spanRatio - 1) < 0.01;
|
|
4000
|
+
if (animation?.viewportTransitions === false || duration === 0 || negligible || prefersReducedMotion() || typeof requestAnimationFrame !== "function") {
|
|
4001
|
+
xCenter = targetCenter;
|
|
4002
|
+
xSpan = targetSpan;
|
|
4003
|
+
clampXViewport();
|
|
4004
|
+
onComplete?.();
|
|
4005
|
+
emitViewportChange();
|
|
4006
|
+
scheduleDraw();
|
|
4007
|
+
return false;
|
|
3492
4008
|
}
|
|
4009
|
+
cancelViewportTween();
|
|
4010
|
+
const startedAt = performance.now();
|
|
4011
|
+
const step = (now) => {
|
|
4012
|
+
const t = clamp((now - startedAt) / duration, 0, 1);
|
|
4013
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
4014
|
+
xCenter = startCenter + centerDelta * eased;
|
|
4015
|
+
xSpan = startSpan * Math.pow(spanRatio, eased);
|
|
4016
|
+
clampXViewport();
|
|
4017
|
+
scheduleDraw();
|
|
4018
|
+
if (t < 1) {
|
|
4019
|
+
viewportTweenRaf = requestAnimationFrame(step);
|
|
4020
|
+
return;
|
|
4021
|
+
}
|
|
4022
|
+
viewportTweenRaf = null;
|
|
4023
|
+
xCenter = targetCenter;
|
|
4024
|
+
xSpan = targetSpan;
|
|
4025
|
+
clampXViewport();
|
|
4026
|
+
onComplete?.();
|
|
4027
|
+
emitViewportChange();
|
|
4028
|
+
scheduleDraw();
|
|
4029
|
+
};
|
|
4030
|
+
viewportTweenRaf = requestAnimationFrame(step);
|
|
4031
|
+
return true;
|
|
4032
|
+
};
|
|
4033
|
+
const getFitXTarget = () => {
|
|
4034
|
+
const count = data.length;
|
|
4035
|
+
if (count === 0) return { center: 0, span: 60 };
|
|
3493
4036
|
const maxSpan = Math.min(maxVisibleBars, Math.max(minVisibleBars, count + maxPanBars * 2));
|
|
3494
|
-
const
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
}
|
|
3499
|
-
|
|
3500
|
-
|
|
4037
|
+
const span = clamp(Math.floor(mergedOptions.initialVisibleBars), minVisibleBars, maxSpan);
|
|
4038
|
+
return {
|
|
4039
|
+
center: mergedOptions.initialViewport === "center" ? count / 2 : count - span / 2 + rightEdgePaddingBars,
|
|
4040
|
+
span
|
|
4041
|
+
};
|
|
4042
|
+
};
|
|
4043
|
+
const fitXViewport = () => {
|
|
4044
|
+
const target = getFitXTarget();
|
|
4045
|
+
xSpan = target.span;
|
|
4046
|
+
xCenter = target.center;
|
|
3501
4047
|
clampXViewport();
|
|
3502
4048
|
};
|
|
3503
4049
|
const getYBounds = () => {
|
|
@@ -3647,6 +4193,7 @@ function createChart(element, options = {}) {
|
|
|
3647
4193
|
};
|
|
3648
4194
|
const resetRightMarginCache = () => {
|
|
3649
4195
|
cachedRightMargin = margin.right;
|
|
4196
|
+
rightMarginShrinkPendingSince = null;
|
|
3650
4197
|
};
|
|
3651
4198
|
const parseData = (nextData) => {
|
|
3652
4199
|
const dedupedByTime = /* @__PURE__ */ new Map();
|
|
@@ -4009,40 +4556,13 @@ function createChart(element, options = {}) {
|
|
|
4009
4556
|
datafeedUnsubscribe = typeof unsubscribe === "function" ? unsubscribe : null;
|
|
4010
4557
|
};
|
|
4011
4558
|
const formatHoverTimeLabel = (time, mode) => {
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
hour12: false
|
|
4017
|
-
});
|
|
4018
|
-
}
|
|
4019
|
-
if (mode === "datetime") {
|
|
4020
|
-
return time.toLocaleString(void 0, {
|
|
4021
|
-
month: "short",
|
|
4022
|
-
day: "numeric",
|
|
4023
|
-
hour: "2-digit",
|
|
4024
|
-
minute: "2-digit",
|
|
4025
|
-
hour12: false
|
|
4026
|
-
});
|
|
4027
|
-
}
|
|
4028
|
-
if (mode === "date") {
|
|
4029
|
-
return time.toLocaleDateString(void 0, {
|
|
4030
|
-
month: "short",
|
|
4031
|
-
day: "numeric"
|
|
4032
|
-
});
|
|
4033
|
-
}
|
|
4559
|
+
const ms = time.getTime();
|
|
4560
|
+
if (mode === "time") return formatClock(time);
|
|
4561
|
+
if (mode === "datetime") return `${zoneClock.formatDayMonth(ms)}, ${formatClock(time)}`;
|
|
4562
|
+
if (mode === "date") return zoneClock.formatDayMonth(ms);
|
|
4034
4563
|
const stepMs = getTimeStepMs();
|
|
4035
|
-
if (stepMs < 24 * 60 * 60 * 1e3)
|
|
4036
|
-
|
|
4037
|
-
hour: "2-digit",
|
|
4038
|
-
minute: "2-digit",
|
|
4039
|
-
hour12: false
|
|
4040
|
-
});
|
|
4041
|
-
}
|
|
4042
|
-
return time.toLocaleDateString(void 0, {
|
|
4043
|
-
month: "short",
|
|
4044
|
-
day: "numeric"
|
|
4045
|
-
});
|
|
4564
|
+
if (stepMs < 24 * 60 * 60 * 1e3) return formatClock(time);
|
|
4565
|
+
return zoneClock.formatDayMonth(ms);
|
|
4046
4566
|
};
|
|
4047
4567
|
const drawText = (text, x, y, align = "left", baseline = "alphabetic", color = mergedOptions.axis?.textColor ?? mergedOptions.axisColor) => {
|
|
4048
4568
|
ctx.fillStyle = color;
|
|
@@ -4139,6 +4659,86 @@ function createChart(element, options = {}) {
|
|
|
4139
4659
|
labelTextColorOn(mergedLine.labelBackgroundColor, mergedLine.labelTextColor)
|
|
4140
4660
|
);
|
|
4141
4661
|
};
|
|
4662
|
+
const drawBellGlyph = (x, y, size, color) => {
|
|
4663
|
+
const w = size;
|
|
4664
|
+
const h = size;
|
|
4665
|
+
ctx.save();
|
|
4666
|
+
ctx.strokeStyle = color;
|
|
4667
|
+
ctx.fillStyle = color;
|
|
4668
|
+
ctx.lineWidth = 1.2;
|
|
4669
|
+
ctx.lineJoin = "round";
|
|
4670
|
+
ctx.beginPath();
|
|
4671
|
+
ctx.moveTo(x - w * 0.42, y + h * 0.28);
|
|
4672
|
+
ctx.quadraticCurveTo(x - w * 0.3, y + h * 0.2, x - w * 0.3, y - h * 0.05);
|
|
4673
|
+
ctx.quadraticCurveTo(x - w * 0.3, y - h * 0.46, x, y - h * 0.46);
|
|
4674
|
+
ctx.quadraticCurveTo(x + w * 0.3, y - h * 0.46, x + w * 0.3, y - h * 0.05);
|
|
4675
|
+
ctx.quadraticCurveTo(x + w * 0.3, y + h * 0.2, x + w * 0.42, y + h * 0.28);
|
|
4676
|
+
ctx.closePath();
|
|
4677
|
+
ctx.stroke();
|
|
4678
|
+
ctx.beginPath();
|
|
4679
|
+
ctx.arc(x, y + h * 0.42, Math.max(1, w * 0.1), 0, Math.PI * 2);
|
|
4680
|
+
ctx.fill();
|
|
4681
|
+
ctx.restore();
|
|
4682
|
+
};
|
|
4683
|
+
const drawAlertLine = (alert, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
|
|
4684
|
+
const lineY = getLineY(alert.price, yFromPrice, chartTop, chartBottom, false);
|
|
4685
|
+
if (lineY === null) return;
|
|
4686
|
+
const dim = alert.triggered || !alert.active;
|
|
4687
|
+
const color = alert.color || "#f7a600";
|
|
4688
|
+
const axis = resolvedAxis;
|
|
4689
|
+
ctx.save();
|
|
4690
|
+
ctx.globalAlpha = dim ? 0.5 : 1;
|
|
4691
|
+
ctx.strokeStyle = color;
|
|
4692
|
+
ctx.lineWidth = 1;
|
|
4693
|
+
applyDashPattern("dashed", dashPatterns.dotted, dashPatterns.dashed);
|
|
4694
|
+
ctx.beginPath();
|
|
4695
|
+
ctx.moveTo(crisp(chartLeft), crisp(lineY));
|
|
4696
|
+
ctx.lineTo(crisp(chartRight), crisp(lineY));
|
|
4697
|
+
ctx.stroke();
|
|
4698
|
+
ctx.restore();
|
|
4699
|
+
const fontSize = Math.max(8, axis.fontSize);
|
|
4700
|
+
ctx.font = `${fontSize}px ${mergedOptions.fontFamily}`;
|
|
4701
|
+
const text = alert.label ? `${formatPrice(alert.price)} ${alert.label}` : formatPrice(alert.price);
|
|
4702
|
+
const paddingX = 6;
|
|
4703
|
+
const bellWidth = fontSize + 2;
|
|
4704
|
+
const labelHeight = 20;
|
|
4705
|
+
const hovered = hoveredAlertId === alert.id;
|
|
4706
|
+
const removeWidth = hovered ? labelHeight : 0;
|
|
4707
|
+
const contentWidth = Math.ceil(measureTextWidth(text)) + bellWidth + paddingX * 3 + removeWidth;
|
|
4708
|
+
const labelWidth = getRightAxisLabelWidth(chartRight, contentWidth);
|
|
4709
|
+
const labelX = getRightAxisLabelX(chartRight);
|
|
4710
|
+
const labelY = placeRightAxisLabel(lineY - labelHeight / 2, labelHeight, chartTop, chartBottom - labelHeight);
|
|
4711
|
+
ctx.save();
|
|
4712
|
+
ctx.globalAlpha = dim ? 0.6 : 1;
|
|
4713
|
+
ctx.fillStyle = color;
|
|
4714
|
+
fillRoundedRect(Math.round(labelX), Math.round(labelY), labelWidth, labelHeight, 3);
|
|
4715
|
+
const inkColor = labelTextColorOn(color, "#101114");
|
|
4716
|
+
drawBellGlyph(labelX + paddingX + bellWidth / 2, labelY + labelHeight / 2, fontSize, inkColor);
|
|
4717
|
+
drawText(text, labelX + paddingX * 2 + bellWidth, labelY + labelHeight / 2, "left", "middle", inkColor);
|
|
4718
|
+
if (hovered) {
|
|
4719
|
+
const removeX = labelX + labelWidth - removeWidth;
|
|
4720
|
+
ctx.strokeStyle = inkColor;
|
|
4721
|
+
ctx.lineWidth = 1.4;
|
|
4722
|
+
const inset = 6;
|
|
4723
|
+
ctx.beginPath();
|
|
4724
|
+
ctx.moveTo(removeX + inset, labelY + inset);
|
|
4725
|
+
ctx.lineTo(removeX + removeWidth - inset, labelY + labelHeight - inset);
|
|
4726
|
+
ctx.moveTo(removeX + removeWidth - inset, labelY + inset);
|
|
4727
|
+
ctx.lineTo(removeX + inset, labelY + labelHeight - inset);
|
|
4728
|
+
ctx.stroke();
|
|
4729
|
+
alertRegions.push({
|
|
4730
|
+
id: alert.id,
|
|
4731
|
+
x: removeX,
|
|
4732
|
+
y: labelY,
|
|
4733
|
+
width: removeWidth,
|
|
4734
|
+
height: labelHeight,
|
|
4735
|
+
part: "remove"
|
|
4736
|
+
});
|
|
4737
|
+
}
|
|
4738
|
+
ctx.restore();
|
|
4739
|
+
alertRegions.push({ id: alert.id, x: labelX, y: labelY, width: labelWidth - removeWidth, height: labelHeight, part: "line" });
|
|
4740
|
+
alertRegions.push({ id: alert.id, x: chartLeft, y: lineY - 5, width: chartRight - chartLeft, height: 10, part: "line" });
|
|
4741
|
+
};
|
|
4142
4742
|
const drawOrderLine = (line, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
|
|
4143
4743
|
const mergedLine = { ...DEFAULT_ORDER_LINE_OPTIONS, ...line };
|
|
4144
4744
|
const renderPrice = mergedLine.behavior === "follow" && Number.isFinite(mergedLine.followPrice) ? mergedLine.followPrice : mergedLine.price;
|
|
@@ -4423,6 +5023,37 @@ function createChart(element, options = {}) {
|
|
|
4423
5023
|
ctx.closePath();
|
|
4424
5024
|
ctx.stroke();
|
|
4425
5025
|
};
|
|
5026
|
+
const drawMarkBadge = (x, y, radius, color, shape, label, textColor) => {
|
|
5027
|
+
ctx.save();
|
|
5028
|
+
ctx.fillStyle = color;
|
|
5029
|
+
ctx.beginPath();
|
|
5030
|
+
if (shape === "square") {
|
|
5031
|
+
ctx.rect(x - radius, y - radius, radius * 2, radius * 2);
|
|
5032
|
+
} else if (shape === "diamond") {
|
|
5033
|
+
ctx.moveTo(x, y - radius);
|
|
5034
|
+
ctx.lineTo(x + radius, y);
|
|
5035
|
+
ctx.lineTo(x, y + radius);
|
|
5036
|
+
ctx.lineTo(x - radius, y);
|
|
5037
|
+
ctx.closePath();
|
|
5038
|
+
} else if (shape === "flag") {
|
|
5039
|
+
ctx.rect(x - radius * 0.16, y - radius, radius * 0.32, radius * 2);
|
|
5040
|
+
ctx.moveTo(x + radius * 0.16, y - radius);
|
|
5041
|
+
ctx.lineTo(x + radius * 1.35, y - radius * 0.45);
|
|
5042
|
+
ctx.lineTo(x + radius * 0.16, y + radius * 0.1);
|
|
5043
|
+
ctx.closePath();
|
|
5044
|
+
} else {
|
|
5045
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
5046
|
+
}
|
|
5047
|
+
ctx.fill();
|
|
5048
|
+
if (label && shape !== "flag") {
|
|
5049
|
+
ctx.fillStyle = textColor;
|
|
5050
|
+
ctx.font = `600 ${Math.max(8, Math.round(radius * 1.2))}px ${mergedOptions.fontFamily}`;
|
|
5051
|
+
ctx.textAlign = "center";
|
|
5052
|
+
ctx.textBaseline = "middle";
|
|
5053
|
+
ctx.fillText(label.slice(0, 2), x, y + 0.5);
|
|
5054
|
+
}
|
|
5055
|
+
ctx.restore();
|
|
5056
|
+
};
|
|
4426
5057
|
const setCrosshairPoint = (point) => {
|
|
4427
5058
|
const current = crosshairPoint;
|
|
4428
5059
|
if (point === null && current === null) {
|
|
@@ -4470,6 +5101,8 @@ function createChart(element, options = {}) {
|
|
|
4470
5101
|
const shouldUpdateAutoScale = options2.updateAutoScale ?? true;
|
|
4471
5102
|
orderActionRegions = [];
|
|
4472
5103
|
orderDragRegions = [];
|
|
5104
|
+
alertRegions = [];
|
|
5105
|
+
markRegions = [];
|
|
4473
5106
|
crosshairPriceActionRegion = null;
|
|
4474
5107
|
const pixelRatio = getPixelRatio();
|
|
4475
5108
|
canvas.style.width = `${width}px`;
|
|
@@ -4552,7 +5185,23 @@ function createChart(element, options = {}) {
|
|
|
4552
5185
|
}
|
|
4553
5186
|
const maxRightMargin = Math.max(margin.right, width - margin.left - 160);
|
|
4554
5187
|
const nextRightMargin = Math.min(maxRightMargin, Math.max(margin.right, Math.ceil(required + 1)));
|
|
4555
|
-
|
|
5188
|
+
if (nextRightMargin >= cachedRightMargin) {
|
|
5189
|
+
cachedRightMargin = nextRightMargin;
|
|
5190
|
+
rightMarginShrinkPendingSince = null;
|
|
5191
|
+
} else if (cachedRightMargin - nextRightMargin > RIGHT_MARGIN_SHRINK_EPSILON_PX) {
|
|
5192
|
+
const now = performance.now();
|
|
5193
|
+
if (rightMarginShrinkPendingSince === null) {
|
|
5194
|
+
rightMarginShrinkPendingSince = now;
|
|
5195
|
+
}
|
|
5196
|
+
if (now - rightMarginShrinkPendingSince >= RIGHT_MARGIN_SHRINK_DELAY_MS) {
|
|
5197
|
+
cachedRightMargin = nextRightMargin;
|
|
5198
|
+
rightMarginShrinkPendingSince = null;
|
|
5199
|
+
} else {
|
|
5200
|
+
scheduleDraw({ updateAutoScale: false });
|
|
5201
|
+
}
|
|
5202
|
+
} else {
|
|
5203
|
+
rightMarginShrinkPendingSince = null;
|
|
5204
|
+
}
|
|
4556
5205
|
return Math.min(maxRightMargin, cachedRightMargin);
|
|
4557
5206
|
};
|
|
4558
5207
|
const rightMargin = estimateRightMargin();
|
|
@@ -5725,12 +6374,14 @@ function createChart(element, options = {}) {
|
|
|
5725
6374
|
if (!prevTime) {
|
|
5726
6375
|
weight = 7;
|
|
5727
6376
|
} else {
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
6377
|
+
const prev = zoneClock.parts(prevTime.getTime());
|
|
6378
|
+
const current = zoneClock.parts(time.getTime());
|
|
6379
|
+
if (prev.year !== current.year) weight = 9;
|
|
6380
|
+
else if (prev.month !== current.month) weight = 8;
|
|
6381
|
+
else if (prev.day !== current.day) weight = 7;
|
|
5731
6382
|
else {
|
|
5732
|
-
const prevMinutes =
|
|
5733
|
-
const curMinutes =
|
|
6383
|
+
const prevMinutes = prev.hour * 60 + prev.minute;
|
|
6384
|
+
const curMinutes = current.hour * 60 + current.minute;
|
|
5734
6385
|
const crossed = (span) => Math.floor(prevMinutes / span) !== Math.floor(curMinutes / span);
|
|
5735
6386
|
if (crossed(360)) weight = 6;
|
|
5736
6387
|
else if (crossed(60)) weight = 5;
|
|
@@ -5776,31 +6427,64 @@ function createChart(element, options = {}) {
|
|
|
5776
6427
|
ctx.stroke();
|
|
5777
6428
|
ctx.restore();
|
|
5778
6429
|
}
|
|
5779
|
-
|
|
6430
|
+
const sessionSeparatorsOn = grid.sessionSeparators || compiledSession !== null && sessionOptions.separators !== false;
|
|
6431
|
+
const intradaySeries = (() => {
|
|
6432
|
+
if (data.length < 2) return false;
|
|
6433
|
+
const probe = Math.min(Math.max(1, startIndex), data.length - 1);
|
|
6434
|
+
const delta = Math.abs(data[probe].time.getTime() - data[probe - 1].time.getTime());
|
|
6435
|
+
return delta > 0 && delta < 0.9 * 864e5;
|
|
6436
|
+
})();
|
|
6437
|
+
if (compiledSession && sessionOptions.highlightOutOfSession && intradaySeries && data.length > 0) {
|
|
6438
|
+
const session = compiledSession;
|
|
6439
|
+
ctx.save();
|
|
6440
|
+
ctx.fillStyle = sessionOptions.outOfSessionColor;
|
|
6441
|
+
let runStart = null;
|
|
6442
|
+
const flushRun = (endExclusive) => {
|
|
6443
|
+
if (runStart === null) return;
|
|
6444
|
+
const left = chartLeft + (runStart - xStart) / xSpan * chartWidth;
|
|
6445
|
+
const right = chartLeft + (endExclusive - xStart) / xSpan * chartWidth;
|
|
6446
|
+
const clampedLeft = Math.max(chartLeft, left);
|
|
6447
|
+
const clampedRight = Math.min(chartRight, right);
|
|
6448
|
+
if (clampedRight > clampedLeft) {
|
|
6449
|
+
ctx.fillRect(clampedLeft, chartTop, clampedRight - clampedLeft, fullChartBottom - chartTop);
|
|
6450
|
+
}
|
|
6451
|
+
runStart = null;
|
|
6452
|
+
};
|
|
6453
|
+
for (let index = Math.max(0, startIndex); index <= endIndex; index += 1) {
|
|
6454
|
+
const point = data[index];
|
|
6455
|
+
if (!point) continue;
|
|
6456
|
+
const inSession = sessionInfoAt(session, point.time.getTime()).inSession;
|
|
6457
|
+
if (!inSession) {
|
|
6458
|
+
if (runStart === null) runStart = index;
|
|
6459
|
+
} else {
|
|
6460
|
+
flushRun(index);
|
|
6461
|
+
}
|
|
6462
|
+
}
|
|
6463
|
+
flushRun(endIndex + 1);
|
|
6464
|
+
ctx.restore();
|
|
6465
|
+
}
|
|
6466
|
+
if (sessionSeparatorsOn && data.length > 1 && intradaySeries) {
|
|
5780
6467
|
const first = Math.max(1, startIndex);
|
|
5781
|
-
const
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
if (sessionDayOf(point.time.getTime()) === sessionDayOf(prev.time.getTime())) {
|
|
5794
|
-
continue;
|
|
5795
|
-
}
|
|
5796
|
-
const x = chartLeft + (index - xStart) / xSpan * chartWidth;
|
|
5797
|
-
ctx.beginPath();
|
|
5798
|
-
ctx.moveTo(crisp(x), crisp(chartTop));
|
|
5799
|
-
ctx.lineTo(crisp(x), crisp(fullChartBottom));
|
|
5800
|
-
ctx.stroke();
|
|
6468
|
+
const session = compiledSession;
|
|
6469
|
+
const dayOf = (ms) => session ? sessionInfoAt(session, ms).sessionDay : zoneClock.dayKey(ms);
|
|
6470
|
+
ctx.save();
|
|
6471
|
+
ctx.globalAlpha = clamp(grid.sessionSeparatorOpacity, 0, 1);
|
|
6472
|
+
ctx.strokeStyle = grid.sessionSeparatorColor;
|
|
6473
|
+
ctx.lineWidth = 1;
|
|
6474
|
+
for (let index = first; index <= endIndex; index += 1) {
|
|
6475
|
+
const point = data[index];
|
|
6476
|
+
const prev = data[index - 1];
|
|
6477
|
+
if (!point || !prev) continue;
|
|
6478
|
+
if (dayOf(point.time.getTime()) === dayOf(prev.time.getTime())) {
|
|
6479
|
+
continue;
|
|
5801
6480
|
}
|
|
5802
|
-
|
|
6481
|
+
const x = chartLeft + (index - xStart) / xSpan * chartWidth;
|
|
6482
|
+
ctx.beginPath();
|
|
6483
|
+
ctx.moveTo(crisp(x), crisp(chartTop));
|
|
6484
|
+
ctx.lineTo(crisp(x), crisp(fullChartBottom));
|
|
6485
|
+
ctx.stroke();
|
|
5803
6486
|
}
|
|
6487
|
+
ctx.restore();
|
|
5804
6488
|
}
|
|
5805
6489
|
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
5806
6490
|
const useSmoothedCandle = tickerOpts.smoothing && smoothedTickerPrice !== null;
|
|
@@ -6381,6 +7065,35 @@ function createChart(element, options = {}) {
|
|
|
6381
7065
|
ctx.textAlign = prevAlign;
|
|
6382
7066
|
ctx.textBaseline = prevBaseline;
|
|
6383
7067
|
}
|
|
7068
|
+
if (barMarks.length > 0 && data.length > 0) {
|
|
7069
|
+
const prevFont = ctx.font;
|
|
7070
|
+
const prevAlign = ctx.textAlign;
|
|
7071
|
+
const prevBaseline = ctx.textBaseline;
|
|
7072
|
+
ctx.textAlign = "center";
|
|
7073
|
+
ctx.textBaseline = "middle";
|
|
7074
|
+
for (const mark of barMarks) {
|
|
7075
|
+
const ms = typeof mark.time === "number" ? mark.time : Date.parse(String(mark.time));
|
|
7076
|
+
if (!Number.isFinite(ms)) continue;
|
|
7077
|
+
const index = findNearestIndexForTimeMs(ms);
|
|
7078
|
+
if (index === null) continue;
|
|
7079
|
+
const bar = data[index];
|
|
7080
|
+
if (!bar) continue;
|
|
7081
|
+
const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
7082
|
+
if (x < chartLeft - 20 || x > chartRight + 20) continue;
|
|
7083
|
+
const radius = Math.max(4, Number(mark.size) || 7);
|
|
7084
|
+
const above = mark.position === "above";
|
|
7085
|
+
const y = Number.isFinite(mark.price) ? yFromPrice(Number(mark.price)) : above ? yFromPrice(bar.h) - radius - 8 : yFromPrice(bar.l) + radius + 8;
|
|
7086
|
+
if (y < chartTop - radius || y > chartBottom + radius) continue;
|
|
7087
|
+
const key = mark.id ?? `bar:${index}:${mark.label ?? ""}`;
|
|
7088
|
+
const hovered = hoveredMarkKey === `bar|${key}`;
|
|
7089
|
+
const color = mark.color ?? "#f7a600";
|
|
7090
|
+
drawMarkBadge(x, y, hovered ? radius + 1.5 : radius, color, mark.shape ?? "circle", mark.label ?? "", mark.textColor ?? "#101114");
|
|
7091
|
+
markRegions.push({ kind: "bar", mark, id: key, x, y, radius: radius + 3 });
|
|
7092
|
+
}
|
|
7093
|
+
ctx.font = prevFont;
|
|
7094
|
+
ctx.textAlign = prevAlign;
|
|
7095
|
+
ctx.textBaseline = prevBaseline;
|
|
7096
|
+
}
|
|
6384
7097
|
ctx.restore();
|
|
6385
7098
|
const positionAxisGutter = Math.max(0, width - chartRight);
|
|
6386
7099
|
if (positionAxisGutter > 0) {
|
|
@@ -6891,9 +7604,79 @@ function createChart(element, options = {}) {
|
|
|
6891
7604
|
for (const priceLine of priceLines) {
|
|
6892
7605
|
drawPriceLine(priceLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
|
|
6893
7606
|
}
|
|
7607
|
+
for (const alert of alerts) {
|
|
7608
|
+
drawAlertLine(alert, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
|
|
7609
|
+
}
|
|
6894
7610
|
for (const orderLine of orderLines) {
|
|
6895
7611
|
drawOrderLine(orderLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
|
|
6896
7612
|
}
|
|
7613
|
+
let dataLineBottom = chartTop;
|
|
7614
|
+
const dataLine = mergedOptions.dataLine;
|
|
7615
|
+
if (dataLine?.visible && data.length > 0) {
|
|
7616
|
+
const hoverIndex = crosshairPoint && getHitRegion(crosshairPoint.x, crosshairPoint.y) === "plot" ? indexFromCanvasX(crosshairPoint.x) : null;
|
|
7617
|
+
const barIndex = hoverIndex !== null && hoverIndex >= 0 && hoverIndex < data.length ? hoverIndex : data.length - 1;
|
|
7618
|
+
const bar = data[barIndex];
|
|
7619
|
+
if (bar) {
|
|
7620
|
+
const prevFont = ctx.font;
|
|
7621
|
+
const fontSize = Math.max(9, dataLine.fontSize || axis.fontSize);
|
|
7622
|
+
ctx.font = `${fontSize}px ${mergedOptions.fontFamily}`;
|
|
7623
|
+
ctx.textBaseline = "top";
|
|
7624
|
+
ctx.textAlign = "left";
|
|
7625
|
+
const symbolColor = dataLine.symbolColor || axis.textColor;
|
|
7626
|
+
const textColor = dataLine.textColor || labels.indicatorTextColor;
|
|
7627
|
+
const changeAbs = bar.c - bar.o;
|
|
7628
|
+
const changeColor = changeAbs >= 0 ? mergedOptions.upColor : mergedOptions.downColor;
|
|
7629
|
+
const rowY = chartTop + 6;
|
|
7630
|
+
let cursorX = chartLeft + 10;
|
|
7631
|
+
if (dataLine.statusColor) {
|
|
7632
|
+
const dotRadius = Math.max(2.5, fontSize / 4);
|
|
7633
|
+
ctx.fillStyle = dataLine.statusColor;
|
|
7634
|
+
ctx.beginPath();
|
|
7635
|
+
ctx.arc(cursorX + dotRadius, rowY + fontSize / 2, dotRadius, 0, Math.PI * 2);
|
|
7636
|
+
ctx.fill();
|
|
7637
|
+
cursorX += dotRadius * 2 + 6;
|
|
7638
|
+
}
|
|
7639
|
+
const headline = [dataLine.symbol, dataLine.interval, dataLine.exchange].filter((part) => part && part.length > 0).join(" \xB7 ");
|
|
7640
|
+
if (headline) {
|
|
7641
|
+
ctx.fillStyle = symbolColor;
|
|
7642
|
+
ctx.fillText(headline, cursorX, rowY);
|
|
7643
|
+
cursorX += measureTextWidth(headline) + 10;
|
|
7644
|
+
}
|
|
7645
|
+
if (dataLine.showOhlc !== false) {
|
|
7646
|
+
const ohlc = `O${formatPrice(bar.o)} H${formatPrice(bar.h)} L${formatPrice(bar.l)} C${formatPrice(bar.c)}`;
|
|
7647
|
+
ctx.fillStyle = changeColor;
|
|
7648
|
+
ctx.fillText(ohlc, cursorX, rowY);
|
|
7649
|
+
cursorX += measureTextWidth(ohlc) + 8;
|
|
7650
|
+
}
|
|
7651
|
+
if (dataLine.showChange !== false) {
|
|
7652
|
+
const pct = bar.o !== 0 ? changeAbs / bar.o * 100 : 0;
|
|
7653
|
+
const changeText = `${changeAbs >= 0 ? "+" : ""}${formatPrice(changeAbs)} (${changeAbs >= 0 ? "+" : ""}${pct.toFixed(2)}%)`;
|
|
7654
|
+
ctx.fillStyle = changeColor;
|
|
7655
|
+
ctx.fillText(changeText, cursorX, rowY);
|
|
7656
|
+
cursorX += measureTextWidth(changeText) + 10;
|
|
7657
|
+
}
|
|
7658
|
+
if (dataLine.showVolume && bar.v !== void 0) {
|
|
7659
|
+
const volumeText = `Vol ${bar.v >= 1e6 ? `${(bar.v / 1e6).toFixed(2)}M` : bar.v >= 1e3 ? `${(bar.v / 1e3).toFixed(1)}K` : String(Math.round(bar.v))}`;
|
|
7660
|
+
ctx.fillStyle = textColor;
|
|
7661
|
+
ctx.fillText(volumeText, cursorX, rowY);
|
|
7662
|
+
cursorX += measureTextWidth(volumeText) + 10;
|
|
7663
|
+
}
|
|
7664
|
+
for (const detail of dataLine.details ?? []) {
|
|
7665
|
+
const text = detail.label ? `${detail.label} ${detail.value}` : detail.value;
|
|
7666
|
+
const width2 = measureTextWidth(text);
|
|
7667
|
+
if (cursorX + width2 > chartRight - 4) break;
|
|
7668
|
+
if (detail.background) {
|
|
7669
|
+
ctx.fillStyle = detail.background;
|
|
7670
|
+
fillRoundedRect(cursorX - 4, rowY - 2, width2 + 8, fontSize + 4, 3);
|
|
7671
|
+
}
|
|
7672
|
+
ctx.fillStyle = detail.color || textColor;
|
|
7673
|
+
ctx.fillText(text, cursorX, rowY);
|
|
7674
|
+
cursorX += width2 + 12;
|
|
7675
|
+
}
|
|
7676
|
+
dataLineBottom = rowY + fontSize + 4;
|
|
7677
|
+
ctx.font = prevFont;
|
|
7678
|
+
}
|
|
7679
|
+
}
|
|
6897
7680
|
if (labels.visible && (labels.showIndicatorNames || labels.showIndicatorValues)) {
|
|
6898
7681
|
const isLegendInputValue = (value) => {
|
|
6899
7682
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
@@ -6922,7 +7705,7 @@ function createChart(element, options = {}) {
|
|
|
6922
7705
|
const isRight = position === "top-right" || position === "bottom-right";
|
|
6923
7706
|
const isBottom = position === "bottom-left" || position === "bottom-right";
|
|
6924
7707
|
const legendX = isRight ? chartRight - offsetX : chartLeft + offsetX;
|
|
6925
|
-
const legendY = isBottom ? chartBottom - offsetY : chartTop + offsetY;
|
|
7708
|
+
const legendY = isBottom ? chartBottom - offsetY : Math.max(chartTop + offsetY, dataLineBottom);
|
|
6926
7709
|
drawText(legendText, legendX, legendY, isRight ? "right" : "left", isBottom ? "bottom" : "top", labels.indicatorTextColor);
|
|
6927
7710
|
ctx.font = prevFont;
|
|
6928
7711
|
}
|
|
@@ -6933,11 +7716,36 @@ function createChart(element, options = {}) {
|
|
|
6933
7716
|
for (const tick of timeTicks) {
|
|
6934
7717
|
const tickTime = getTimeForIndex(tick.index);
|
|
6935
7718
|
if (!tickTime) continue;
|
|
6936
|
-
const
|
|
7719
|
+
const tickMs = tickTime.getTime();
|
|
7720
|
+
const timeLabel = tick.weight >= 9 ? zoneClock.formatYear(tickMs) : tick.weight === 8 ? zoneClock.formatMonth(tickMs) : tick.weight === 7 ? zoneClock.formatDayMonth(tickMs) : formatClock(tickTime);
|
|
6937
7721
|
drawText(timeLabel, tick.x, (fullChartBottom + height) / 2, "center", "middle", xAxis.textColor);
|
|
6938
7722
|
}
|
|
6939
7723
|
ctx.font = prevFont;
|
|
6940
7724
|
}
|
|
7725
|
+
if (timescaleMarks.length > 0 && data.length > 0) {
|
|
7726
|
+
const radius = 6;
|
|
7727
|
+
const markY = fullChartBottom - radius - 2;
|
|
7728
|
+
for (const mark of timescaleMarks) {
|
|
7729
|
+
const ms = typeof mark.time === "number" ? mark.time : Date.parse(String(mark.time));
|
|
7730
|
+
if (!Number.isFinite(ms)) continue;
|
|
7731
|
+
const index = findNearestIndexForTimeMs(ms);
|
|
7732
|
+
if (index === null) continue;
|
|
7733
|
+
const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
7734
|
+
if (x < chartLeft - 10 || x > chartRight + 10) continue;
|
|
7735
|
+
const key = mark.id ?? `ts:${index}:${mark.label ?? ""}`;
|
|
7736
|
+
const hovered = hoveredMarkKey === `timescale|${key}`;
|
|
7737
|
+
drawMarkBadge(
|
|
7738
|
+
x,
|
|
7739
|
+
markY,
|
|
7740
|
+
hovered ? radius + 1.5 : radius,
|
|
7741
|
+
mark.color ?? "#5b8def",
|
|
7742
|
+
mark.shape ?? "circle",
|
|
7743
|
+
mark.label ?? "",
|
|
7744
|
+
mark.textColor ?? "#0b1220"
|
|
7745
|
+
);
|
|
7746
|
+
markRegions.push({ kind: "timescale", mark, id: key, x, y: markY, radius: radius + 3 });
|
|
7747
|
+
}
|
|
7748
|
+
}
|
|
6941
7749
|
if (labels.visible && labels.showCountdownToBarClose && lastPoint) {
|
|
6942
7750
|
const stepMs = getTimeStepMs();
|
|
6943
7751
|
const nowMs = Date.now() + (Number(mergedOptions.clockOffsetMs) || 0);
|
|
@@ -7463,27 +8271,24 @@ function createChart(element, options = {}) {
|
|
|
7463
8271
|
scheduleDraw();
|
|
7464
8272
|
};
|
|
7465
8273
|
const fitContent = () => {
|
|
7466
|
-
|
|
7467
|
-
updateFollowLatest(true);
|
|
7468
|
-
emitViewportChange();
|
|
7469
|
-
scheduleDraw();
|
|
8274
|
+
const target = getFitXTarget();
|
|
8275
|
+
animateViewportTo(target.center, target.span, () => updateFollowLatest(true));
|
|
7470
8276
|
};
|
|
7471
8277
|
const resetViewport = () => {
|
|
7472
8278
|
cancelKineticPan();
|
|
7473
|
-
|
|
8279
|
+
const target = getFitXTarget();
|
|
7474
8280
|
resetYViewport();
|
|
7475
|
-
updateFollowLatest(true);
|
|
7476
|
-
emitViewportChange();
|
|
7477
|
-
scheduleDraw();
|
|
8281
|
+
animateViewportTo(target.center, target.span, () => updateFollowLatest(true));
|
|
7478
8282
|
};
|
|
7479
8283
|
const isFollowingLatest = () => followLatest;
|
|
7480
8284
|
const setFollowingLatest = (follow) => {
|
|
7481
8285
|
if (follow && data.length > 0) {
|
|
7482
8286
|
cancelKineticPan();
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
8287
|
+
animateViewportTo(
|
|
8288
|
+
data.length - xSpan / 2 + rightEdgePaddingBars,
|
|
8289
|
+
xSpan,
|
|
8290
|
+
() => updateFollowLatest(true)
|
|
8291
|
+
);
|
|
7487
8292
|
} else {
|
|
7488
8293
|
updateFollowLatest(follow);
|
|
7489
8294
|
}
|
|
@@ -7566,6 +8371,22 @@ function createChart(element, options = {}) {
|
|
|
7566
8371
|
(region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
|
|
7567
8372
|
);
|
|
7568
8373
|
};
|
|
8374
|
+
const getAlertRegionAt = (x, y) => {
|
|
8375
|
+
const hits = alertRegions.filter(
|
|
8376
|
+
(region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
|
|
8377
|
+
);
|
|
8378
|
+
if (hits.length === 0) return null;
|
|
8379
|
+
return hits.find((region) => region.part === "remove") ?? hits[0];
|
|
8380
|
+
};
|
|
8381
|
+
const getMarkAt = (x, y) => {
|
|
8382
|
+
const tolerance = touchInputActive ? Math.max(1, mergedOptions.touch?.hitToleranceScale ?? 2.2) : 1;
|
|
8383
|
+
for (let index = markRegions.length - 1; index >= 0; index -= 1) {
|
|
8384
|
+
const region = markRegions[index];
|
|
8385
|
+
const reach = region.radius * tolerance;
|
|
8386
|
+
if (Math.abs(x - region.x) <= reach && Math.abs(y - region.y) <= reach) return region;
|
|
8387
|
+
}
|
|
8388
|
+
return null;
|
|
8389
|
+
};
|
|
7569
8390
|
const getCrosshairPriceActionRegion = (x, y) => {
|
|
7570
8391
|
if (!crosshairPriceActionRegion) {
|
|
7571
8392
|
return null;
|
|
@@ -8725,7 +9546,10 @@ function createChart(element, options = {}) {
|
|
|
8725
9546
|
pointerDownInfo = null;
|
|
8726
9547
|
orderDragState = null;
|
|
8727
9548
|
actionDragState = null;
|
|
8728
|
-
drawingDragState
|
|
9549
|
+
if (drawingDragState) {
|
|
9550
|
+
drawingDragState = null;
|
|
9551
|
+
commitHistory();
|
|
9552
|
+
}
|
|
8729
9553
|
canvas.style.cursor = "default";
|
|
8730
9554
|
setCrosshairPoint(null);
|
|
8731
9555
|
};
|
|
@@ -8766,6 +9590,7 @@ function createChart(element, options = {}) {
|
|
|
8766
9590
|
canvas.focus({ preventScroll: true });
|
|
8767
9591
|
}
|
|
8768
9592
|
cancelKineticPan();
|
|
9593
|
+
cancelViewportTween();
|
|
8769
9594
|
panVelocitySamples.length = 0;
|
|
8770
9595
|
magnetModifierActive = event.metaKey || event.ctrlKey;
|
|
8771
9596
|
shiftKeyActive = event.shiftKey;
|
|
@@ -8819,6 +9644,20 @@ function createChart(element, options = {}) {
|
|
|
8819
9644
|
});
|
|
8820
9645
|
return;
|
|
8821
9646
|
}
|
|
9647
|
+
const markHit = getMarkAt(point.x, point.y);
|
|
9648
|
+
if (markHit && markClickHandler) {
|
|
9649
|
+
const rect = canvas.getBoundingClientRect();
|
|
9650
|
+
setCrosshairPoint(null);
|
|
9651
|
+
markClickHandler({
|
|
9652
|
+
kind: markHit.kind,
|
|
9653
|
+
mark: markHit.mark,
|
|
9654
|
+
x: markHit.x,
|
|
9655
|
+
y: markHit.y,
|
|
9656
|
+
clientX: rect.left + markHit.x,
|
|
9657
|
+
clientY: rect.top + markHit.y
|
|
9658
|
+
});
|
|
9659
|
+
return;
|
|
9660
|
+
}
|
|
8822
9661
|
const orderRegion = getOrderActionRegion(point.x, point.y);
|
|
8823
9662
|
if (orderRegion) {
|
|
8824
9663
|
if (orderRegion.draggable) {
|
|
@@ -8857,6 +9696,28 @@ function createChart(element, options = {}) {
|
|
|
8857
9696
|
setCrosshairPoint(null);
|
|
8858
9697
|
return;
|
|
8859
9698
|
}
|
|
9699
|
+
const alertHit = getAlertRegionAt(point.x, point.y);
|
|
9700
|
+
if (alertHit) {
|
|
9701
|
+
const target = alerts.find((alert) => alert.id === alertHit.id);
|
|
9702
|
+
if (target) {
|
|
9703
|
+
setCrosshairPoint(null);
|
|
9704
|
+
if (alertHit.part === "remove") {
|
|
9705
|
+
const removed = serializeAlert(target);
|
|
9706
|
+
removeAlert(target.id);
|
|
9707
|
+
alertActionHandler?.({ alert: removed, action: "remove" });
|
|
9708
|
+
return;
|
|
9709
|
+
}
|
|
9710
|
+
if (target.draggable) {
|
|
9711
|
+
activePointerId = event.pointerId;
|
|
9712
|
+
alertDragState = { id: target.id, startPrice: target.price, lastPrice: target.price, moved: false };
|
|
9713
|
+
capturePointer(event.pointerId);
|
|
9714
|
+
canvas.style.cursor = "ns-resize";
|
|
9715
|
+
return;
|
|
9716
|
+
}
|
|
9717
|
+
alertActionHandler?.({ alert: serializeAlert(target), action: "click", price: target.price });
|
|
9718
|
+
return;
|
|
9719
|
+
}
|
|
9720
|
+
}
|
|
8860
9721
|
const paneButtonHit = getPaneButtonHit(point.x, point.y);
|
|
8861
9722
|
if (paneButtonHit) {
|
|
8862
9723
|
setCrosshairPoint(null);
|
|
@@ -9099,6 +9960,24 @@ function createChart(element, options = {}) {
|
|
|
9099
9960
|
setCrosshairPoint(null);
|
|
9100
9961
|
return;
|
|
9101
9962
|
}
|
|
9963
|
+
if (alertDragState) {
|
|
9964
|
+
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
9965
|
+
return;
|
|
9966
|
+
}
|
|
9967
|
+
const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
|
|
9968
|
+
if (nextPrice !== alertDragState.lastPrice) {
|
|
9969
|
+
alertDragState.lastPrice = nextPrice;
|
|
9970
|
+
alertDragState.moved = true;
|
|
9971
|
+
const target = alerts.find((alert) => alert.id === alertDragState?.id);
|
|
9972
|
+
if (target) {
|
|
9973
|
+
target.price = nextPrice;
|
|
9974
|
+
target.lastPrice = data[data.length - 1]?.c ?? null;
|
|
9975
|
+
alertActionHandler?.({ alert: serializeAlert(target), action: "move", price: nextPrice, dragging: true });
|
|
9976
|
+
}
|
|
9977
|
+
scheduleDraw();
|
|
9978
|
+
}
|
|
9979
|
+
return;
|
|
9980
|
+
}
|
|
9102
9981
|
if (orderDragState) {
|
|
9103
9982
|
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
9104
9983
|
return;
|
|
@@ -9165,12 +10044,49 @@ function createChart(element, options = {}) {
|
|
|
9165
10044
|
setCrosshairPoint(null);
|
|
9166
10045
|
return;
|
|
9167
10046
|
}
|
|
10047
|
+
const markHover = getMarkAt(point.x, point.y);
|
|
10048
|
+
const nextMarkKey = markHover ? `${markHover.kind}|${markHover.id}` : null;
|
|
10049
|
+
if (nextMarkKey !== hoveredMarkKey) {
|
|
10050
|
+
hoveredMarkKey = nextMarkKey;
|
|
10051
|
+
if (markHover) {
|
|
10052
|
+
const rect = canvas.getBoundingClientRect();
|
|
10053
|
+
markHoverHandler?.({
|
|
10054
|
+
kind: markHover.kind,
|
|
10055
|
+
mark: markHover.mark,
|
|
10056
|
+
x: markHover.x,
|
|
10057
|
+
y: markHover.y,
|
|
10058
|
+
clientX: rect.left + markHover.x,
|
|
10059
|
+
clientY: rect.top + markHover.y
|
|
10060
|
+
});
|
|
10061
|
+
} else {
|
|
10062
|
+
markHoverHandler?.(null);
|
|
10063
|
+
}
|
|
10064
|
+
scheduleDraw();
|
|
10065
|
+
}
|
|
10066
|
+
if (markHover) {
|
|
10067
|
+
canvas.title = markHover.mark.text ?? markHover.mark.label ?? "";
|
|
10068
|
+
canvas.style.cursor = "pointer";
|
|
10069
|
+
setCrosshairPoint(null);
|
|
10070
|
+
return;
|
|
10071
|
+
}
|
|
9168
10072
|
const orderDragRegion = getOrderDragRegion(point.x, point.y);
|
|
9169
10073
|
if (orderDragRegion) {
|
|
9170
10074
|
canvas.style.cursor = "ns-resize";
|
|
9171
10075
|
setCrosshairPoint(null);
|
|
9172
10076
|
return;
|
|
9173
10077
|
}
|
|
10078
|
+
const alertHover = getAlertRegionAt(point.x, point.y);
|
|
10079
|
+
const nextAlertId = alertHover?.id ?? null;
|
|
10080
|
+
if (nextAlertId !== hoveredAlertId) {
|
|
10081
|
+
hoveredAlertId = nextAlertId;
|
|
10082
|
+
scheduleDraw();
|
|
10083
|
+
}
|
|
10084
|
+
if (alertHover) {
|
|
10085
|
+
canvas.title = alertHover.part === "remove" ? "Remove alert" : "Drag to move alert";
|
|
10086
|
+
canvas.style.cursor = alertHover.part === "remove" ? "pointer" : "ns-resize";
|
|
10087
|
+
setCrosshairPoint(null);
|
|
10088
|
+
return;
|
|
10089
|
+
}
|
|
9174
10090
|
const hoveredPane = getPaneAt(point.x, point.y);
|
|
9175
10091
|
const nextHoveredPaneId = hoveredPane?.id ?? null;
|
|
9176
10092
|
if (nextHoveredPaneId !== hoveredPaneId) {
|
|
@@ -9328,6 +10244,21 @@ function createChart(element, options = {}) {
|
|
|
9328
10244
|
scheduleDraw();
|
|
9329
10245
|
return;
|
|
9330
10246
|
}
|
|
10247
|
+
if (alertDragState) {
|
|
10248
|
+
const target = alerts.find((alert) => alert.id === alertDragState?.id);
|
|
10249
|
+
if (target && alertDragState.moved) {
|
|
10250
|
+
alertActionHandler?.({
|
|
10251
|
+
alert: serializeAlert(target),
|
|
10252
|
+
action: "move",
|
|
10253
|
+
price: alertDragState.lastPrice,
|
|
10254
|
+
dragging: false
|
|
10255
|
+
});
|
|
10256
|
+
} else if (target) {
|
|
10257
|
+
alertActionHandler?.({ alert: serializeAlert(target), action: "click", price: target.price });
|
|
10258
|
+
}
|
|
10259
|
+
alertDragState = null;
|
|
10260
|
+
canvas.style.cursor = "default";
|
|
10261
|
+
}
|
|
9331
10262
|
if (orderDragState) {
|
|
9332
10263
|
const moved = orderDragState.lastPrice !== orderDragState.startPrice;
|
|
9333
10264
|
const finalLine = orderLines.find((line) => line.id === orderDragState?.orderId);
|
|
@@ -9374,9 +10305,14 @@ function createChart(element, options = {}) {
|
|
|
9374
10305
|
});
|
|
9375
10306
|
paneDividerDrag = null;
|
|
9376
10307
|
}
|
|
9377
|
-
if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null)) {
|
|
10308
|
+
if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null || hoveredAlertId !== null || hoveredMarkKey !== null)) {
|
|
9378
10309
|
hoveredPaneId = null;
|
|
9379
10310
|
hoveredPaneButton = null;
|
|
10311
|
+
hoveredAlertId = null;
|
|
10312
|
+
if (hoveredMarkKey !== null) {
|
|
10313
|
+
hoveredMarkKey = null;
|
|
10314
|
+
markHoverHandler?.(null);
|
|
10315
|
+
}
|
|
9380
10316
|
if (canvas.title) {
|
|
9381
10317
|
canvas.title = "";
|
|
9382
10318
|
}
|
|
@@ -9430,6 +10366,7 @@ function createChart(element, options = {}) {
|
|
|
9430
10366
|
return;
|
|
9431
10367
|
}
|
|
9432
10368
|
cancelKineticPan();
|
|
10369
|
+
cancelViewportTween();
|
|
9433
10370
|
const point = getCanvasPoint(event);
|
|
9434
10371
|
const region = getHitRegion(point.x, point.y);
|
|
9435
10372
|
if (region === "outside") {
|
|
@@ -9551,9 +10488,6 @@ function createChart(element, options = {}) {
|
|
|
9551
10488
|
if (keyboard.enabled === false) {
|
|
9552
10489
|
return;
|
|
9553
10490
|
}
|
|
9554
|
-
if (event.altKey || event.ctrlKey || event.metaKey) {
|
|
9555
|
-
return;
|
|
9556
|
-
}
|
|
9557
10491
|
const panStep = Math.max(1, keyboard.panBars ?? 1) * (event.shiftKey ? 10 : 1);
|
|
9558
10492
|
const emit = (action, drawing) => {
|
|
9559
10493
|
keyboardShortcutHandler?.({
|
|
@@ -9563,6 +10497,21 @@ function createChart(element, options = {}) {
|
|
|
9563
10497
|
...drawing ? { drawing } : {}
|
|
9564
10498
|
});
|
|
9565
10499
|
};
|
|
10500
|
+
if ((event.ctrlKey || event.metaKey) && !event.altKey) {
|
|
10501
|
+
const key = event.key.toLowerCase();
|
|
10502
|
+
if (keyboard.undoRedo !== false && (key === "z" || key === "y")) {
|
|
10503
|
+
const wantsRedo = key === "y" || event.shiftKey;
|
|
10504
|
+
const applied = wantsRedo ? redo() : undo();
|
|
10505
|
+
if (applied) {
|
|
10506
|
+
event.preventDefault();
|
|
10507
|
+
emit(wantsRedo ? "redo" : "undo");
|
|
10508
|
+
}
|
|
10509
|
+
return;
|
|
10510
|
+
}
|
|
10511
|
+
}
|
|
10512
|
+
if (event.altKey || event.ctrlKey || event.metaKey) {
|
|
10513
|
+
return;
|
|
10514
|
+
}
|
|
9566
10515
|
switch (event.key) {
|
|
9567
10516
|
case "Delete":
|
|
9568
10517
|
case "Backspace": {
|
|
@@ -9707,6 +10656,21 @@ function createChart(element, options = {}) {
|
|
|
9707
10656
|
resetRightMarginCache();
|
|
9708
10657
|
doubleClickEnabled = mergedOptions.doubleClickEnabled;
|
|
9709
10658
|
doubleClickAction = mergedOptions.doubleClickAction;
|
|
10659
|
+
setIndicatorLiveThrottleMs(mergedOptions.indicatorUpdate?.liveThrottleMs ?? 80);
|
|
10660
|
+
if (nextOptions.timezone !== void 0) {
|
|
10661
|
+
timezoneSetting = nextOptions.timezone || "local";
|
|
10662
|
+
zoneClock = createZoneClock(timezoneSetting);
|
|
10663
|
+
}
|
|
10664
|
+
if (nextOptions.timeFormat !== void 0) {
|
|
10665
|
+
timeFormat = nextOptions.timeFormat === "12h" ? "12h" : "24h";
|
|
10666
|
+
}
|
|
10667
|
+
if (nextOptions.session !== void 0) {
|
|
10668
|
+
sessionOptions = { ...sessionOptions, ...nextOptions.session ?? {} };
|
|
10669
|
+
compiledSession = compileSession(sessionOptions.spec);
|
|
10670
|
+
}
|
|
10671
|
+
if (nextOptions.alerts !== void 0) {
|
|
10672
|
+
setAlerts(nextOptions.alerts ?? []);
|
|
10673
|
+
}
|
|
9710
10674
|
applyAccessibilityAttributes();
|
|
9711
10675
|
const isTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
|
|
9712
10676
|
if (!isTickerSmoothingEnabled) {
|
|
@@ -9767,7 +10731,7 @@ function createChart(element, options = {}) {
|
|
|
9767
10731
|
resetRightMarginCache();
|
|
9768
10732
|
scheduleDraw();
|
|
9769
10733
|
};
|
|
9770
|
-
const
|
|
10734
|
+
const setDataInternal = (nextData) => {
|
|
9771
10735
|
const hadData = data.length > 0;
|
|
9772
10736
|
const previousCount = data.length;
|
|
9773
10737
|
const previousCenterRounded = hadData ? Math.round(xCenter) : 0;
|
|
@@ -9809,9 +10773,10 @@ function createChart(element, options = {}) {
|
|
|
9809
10773
|
if (lastPoint) {
|
|
9810
10774
|
pushSmoothedTicker(lastPoint.c, lastPoint.v);
|
|
9811
10775
|
}
|
|
10776
|
+
evaluateAlertsFromLatestBar();
|
|
9812
10777
|
scheduleDraw();
|
|
9813
10778
|
};
|
|
9814
|
-
const
|
|
10779
|
+
const upsertBarInternal = (point) => {
|
|
9815
10780
|
const time = new Date(point.t);
|
|
9816
10781
|
const timeMs = time.getTime();
|
|
9817
10782
|
const open = Number(point.o);
|
|
@@ -9832,7 +10797,7 @@ function createChart(element, options = {}) {
|
|
|
9832
10797
|
};
|
|
9833
10798
|
const last = data[data.length - 1];
|
|
9834
10799
|
if (!last) {
|
|
9835
|
-
|
|
10800
|
+
setDataInternal([point]);
|
|
9836
10801
|
return;
|
|
9837
10802
|
}
|
|
9838
10803
|
const lastMs = last.time.getTime();
|
|
@@ -9854,8 +10819,163 @@ function createChart(element, options = {}) {
|
|
|
9854
10819
|
reanchorDrawingsToData();
|
|
9855
10820
|
}
|
|
9856
10821
|
pushSmoothedTicker(parsed.c, parsed.v);
|
|
10822
|
+
evaluateAlerts(parsed.c, timeMs, data.length - 1);
|
|
9857
10823
|
scheduleDraw();
|
|
9858
10824
|
};
|
|
10825
|
+
const toRawPoint = (point) => ({
|
|
10826
|
+
t: point.time.toISOString(),
|
|
10827
|
+
o: point.o,
|
|
10828
|
+
h: point.h,
|
|
10829
|
+
l: point.l,
|
|
10830
|
+
c: point.c,
|
|
10831
|
+
...point.v === void 0 ? {} : { v: point.v }
|
|
10832
|
+
});
|
|
10833
|
+
const getReplayState = () => {
|
|
10834
|
+
if (!replay) {
|
|
10835
|
+
return {
|
|
10836
|
+
active: false,
|
|
10837
|
+
playing: false,
|
|
10838
|
+
speed: 1,
|
|
10839
|
+
index: Math.max(0, data.length - 1),
|
|
10840
|
+
total: data.length,
|
|
10841
|
+
atEnd: true,
|
|
10842
|
+
timeMs: data.length > 0 ? data[data.length - 1].time.getTime() : null
|
|
10843
|
+
};
|
|
10844
|
+
}
|
|
10845
|
+
const total = replay.fullRaw.length;
|
|
10846
|
+
const revealed = replay.fullRaw[replay.cursor];
|
|
10847
|
+
const revealedMs = revealed ? Date.parse(String(revealed.t)) : Number.NaN;
|
|
10848
|
+
return {
|
|
10849
|
+
active: true,
|
|
10850
|
+
playing: replay.playing,
|
|
10851
|
+
speed: replay.speed,
|
|
10852
|
+
index: replay.cursor,
|
|
10853
|
+
total,
|
|
10854
|
+
atEnd: replay.cursor >= total - 1,
|
|
10855
|
+
timeMs: Number.isFinite(revealedMs) ? revealedMs : null
|
|
10856
|
+
};
|
|
10857
|
+
};
|
|
10858
|
+
const emitReplayState = () => {
|
|
10859
|
+
replayStateHandler?.(getReplayState());
|
|
10860
|
+
};
|
|
10861
|
+
const applyReplaySlice = () => {
|
|
10862
|
+
if (!replay) return;
|
|
10863
|
+
setDataInternal(replay.fullRaw.slice(0, replay.cursor + 1));
|
|
10864
|
+
};
|
|
10865
|
+
const clearReplayTimer = () => {
|
|
10866
|
+
if (replay?.timerId) {
|
|
10867
|
+
clearTimeout(replay.timerId);
|
|
10868
|
+
replay.timerId = null;
|
|
10869
|
+
}
|
|
10870
|
+
};
|
|
10871
|
+
const scheduleReplayTick = () => {
|
|
10872
|
+
if (!replay || !replay.playing) return;
|
|
10873
|
+
clearReplayTimer();
|
|
10874
|
+
const intervalMs = Math.max(16, 1e3 / Math.max(0.05, replay.speed));
|
|
10875
|
+
replay.timerId = setTimeout(() => {
|
|
10876
|
+
if (!replay || !replay.playing) return;
|
|
10877
|
+
if (replay.cursor >= replay.fullRaw.length - 1) {
|
|
10878
|
+
replay.playing = false;
|
|
10879
|
+
clearReplayTimer();
|
|
10880
|
+
emitReplayState();
|
|
10881
|
+
return;
|
|
10882
|
+
}
|
|
10883
|
+
replay.cursor += 1;
|
|
10884
|
+
applyReplaySlice();
|
|
10885
|
+
emitReplayState();
|
|
10886
|
+
scheduleReplayTick();
|
|
10887
|
+
}, intervalMs);
|
|
10888
|
+
};
|
|
10889
|
+
const startReplay = (startOptions = {}) => {
|
|
10890
|
+
const source = replay ? replay.fullRaw : data.map((point) => toRawPoint(point));
|
|
10891
|
+
if (source.length === 0) return;
|
|
10892
|
+
clearReplayTimer();
|
|
10893
|
+
let cursor;
|
|
10894
|
+
if (Number.isFinite(startOptions.fromTimeMs)) {
|
|
10895
|
+
const target = Number(startOptions.fromTimeMs);
|
|
10896
|
+
const resolved = findNearestIndexForTimeMs(target);
|
|
10897
|
+
cursor = resolved ?? Math.floor(source.length * 0.7);
|
|
10898
|
+
} else if (Number.isFinite(startOptions.fromIndex)) {
|
|
10899
|
+
cursor = Math.floor(Number(startOptions.fromIndex));
|
|
10900
|
+
} else {
|
|
10901
|
+
cursor = Math.floor(source.length * 0.7);
|
|
10902
|
+
}
|
|
10903
|
+
cursor = clamp(cursor, 0, source.length - 1);
|
|
10904
|
+
const speed = Math.max(0.05, Number(startOptions.speed) || replay?.speed || 1);
|
|
10905
|
+
replay = { fullRaw: source, cursor, playing: false, speed, timerId: null };
|
|
10906
|
+
applyReplaySlice();
|
|
10907
|
+
setFollowingLatest(true);
|
|
10908
|
+
if (startOptions.play) {
|
|
10909
|
+
replay.playing = true;
|
|
10910
|
+
scheduleReplayTick();
|
|
10911
|
+
}
|
|
10912
|
+
emitReplayState();
|
|
10913
|
+
};
|
|
10914
|
+
const stopReplay = () => {
|
|
10915
|
+
if (!replay) return;
|
|
10916
|
+
clearReplayTimer();
|
|
10917
|
+
const full = replay.fullRaw;
|
|
10918
|
+
replay = null;
|
|
10919
|
+
setDataInternal(full);
|
|
10920
|
+
setFollowingLatest(true);
|
|
10921
|
+
emitReplayState();
|
|
10922
|
+
};
|
|
10923
|
+
const replayStep = (bars = 1) => {
|
|
10924
|
+
if (!replay) return;
|
|
10925
|
+
const step = Math.trunc(bars) || 1;
|
|
10926
|
+
const next = clamp(replay.cursor + step, 0, replay.fullRaw.length - 1);
|
|
10927
|
+
if (next === replay.cursor) return;
|
|
10928
|
+
replay.cursor = next;
|
|
10929
|
+
applyReplaySlice();
|
|
10930
|
+
emitReplayState();
|
|
10931
|
+
};
|
|
10932
|
+
const replayPlay = (speed) => {
|
|
10933
|
+
if (!replay) return;
|
|
10934
|
+
if (Number.isFinite(speed)) replay.speed = Math.max(0.05, Number(speed));
|
|
10935
|
+
if (replay.cursor >= replay.fullRaw.length - 1) return;
|
|
10936
|
+
replay.playing = true;
|
|
10937
|
+
scheduleReplayTick();
|
|
10938
|
+
emitReplayState();
|
|
10939
|
+
};
|
|
10940
|
+
const replayPause = () => {
|
|
10941
|
+
if (!replay || !replay.playing) return;
|
|
10942
|
+
replay.playing = false;
|
|
10943
|
+
clearReplayTimer();
|
|
10944
|
+
emitReplayState();
|
|
10945
|
+
};
|
|
10946
|
+
const setReplaySpeed = (speed) => {
|
|
10947
|
+
if (!replay || !Number.isFinite(speed)) return;
|
|
10948
|
+
replay.speed = clamp(Number(speed), 0.05, 100);
|
|
10949
|
+
if (replay.playing) scheduleReplayTick();
|
|
10950
|
+
emitReplayState();
|
|
10951
|
+
};
|
|
10952
|
+
const onReplayStateChange = (handler) => {
|
|
10953
|
+
replayStateHandler = handler;
|
|
10954
|
+
};
|
|
10955
|
+
const setData = (nextData) => {
|
|
10956
|
+
if (replay) {
|
|
10957
|
+
replay.fullRaw = nextData.slice();
|
|
10958
|
+
replay.cursor = clamp(replay.cursor, 0, Math.max(0, replay.fullRaw.length - 1));
|
|
10959
|
+
applyReplaySlice();
|
|
10960
|
+
emitReplayState();
|
|
10961
|
+
return;
|
|
10962
|
+
}
|
|
10963
|
+
setDataInternal(nextData);
|
|
10964
|
+
};
|
|
10965
|
+
const upsertBar = (point) => {
|
|
10966
|
+
if (replay) {
|
|
10967
|
+
const timeMs = Date.parse(String(point.t));
|
|
10968
|
+
if (!Number.isFinite(timeMs)) return;
|
|
10969
|
+
const buffer = replay.fullRaw;
|
|
10970
|
+
const lastRaw = buffer[buffer.length - 1];
|
|
10971
|
+
const lastMs = lastRaw ? Date.parse(String(lastRaw.t)) : Number.NaN;
|
|
10972
|
+
if (Number.isFinite(lastMs) && timeMs === lastMs) buffer[buffer.length - 1] = point;
|
|
10973
|
+
else if (!Number.isFinite(lastMs) || timeMs > lastMs) buffer.push(point);
|
|
10974
|
+
emitReplayState();
|
|
10975
|
+
return;
|
|
10976
|
+
}
|
|
10977
|
+
upsertBarInternal(point);
|
|
10978
|
+
};
|
|
9859
10979
|
const setPriceLines = (lines) => {
|
|
9860
10980
|
priceLines = lines.map((line, index) => ({
|
|
9861
10981
|
...line,
|
|
@@ -10044,6 +11164,96 @@ function createChart(element, options = {}) {
|
|
|
10044
11164
|
tradeMarkers = Array.isArray(markers) ? markers.slice() : [];
|
|
10045
11165
|
scheduleDraw();
|
|
10046
11166
|
};
|
|
11167
|
+
const setBarMarks = (marks) => {
|
|
11168
|
+
barMarks = Array.isArray(marks) ? marks.slice() : [];
|
|
11169
|
+
scheduleDraw();
|
|
11170
|
+
};
|
|
11171
|
+
const getBarMarks = () => barMarks.slice();
|
|
11172
|
+
const setTimescaleMarks = (marks) => {
|
|
11173
|
+
timescaleMarks = Array.isArray(marks) ? marks.slice() : [];
|
|
11174
|
+
scheduleDraw();
|
|
11175
|
+
};
|
|
11176
|
+
const getTimescaleMarks = () => timescaleMarks.slice();
|
|
11177
|
+
const onMarkClick = (handler) => {
|
|
11178
|
+
markClickHandler = handler;
|
|
11179
|
+
};
|
|
11180
|
+
const onMarkHover = (handler) => {
|
|
11181
|
+
markHoverHandler = handler;
|
|
11182
|
+
};
|
|
11183
|
+
const setAlerts = (nextAlerts) => {
|
|
11184
|
+
alerts = (Array.isArray(nextAlerts) ? nextAlerts : []).map((alert) => normalizeAlert(alert));
|
|
11185
|
+
const last = data[data.length - 1];
|
|
11186
|
+
if (last) {
|
|
11187
|
+
for (const alert of alerts) alert.lastPrice = last.c;
|
|
11188
|
+
}
|
|
11189
|
+
resetRightMarginCache();
|
|
11190
|
+
scheduleDraw();
|
|
11191
|
+
};
|
|
11192
|
+
const addAlert = (alert) => {
|
|
11193
|
+
const next = normalizeAlert(alert);
|
|
11194
|
+
const last = data[data.length - 1];
|
|
11195
|
+
if (last) next.lastPrice = last.c;
|
|
11196
|
+
alerts.push(next);
|
|
11197
|
+
resetRightMarginCache();
|
|
11198
|
+
scheduleDraw();
|
|
11199
|
+
return next.id;
|
|
11200
|
+
};
|
|
11201
|
+
const updateAlert = (id, patch) => {
|
|
11202
|
+
const target = alerts.find((alert) => alert.id === id);
|
|
11203
|
+
if (!target) return;
|
|
11204
|
+
if (Number.isFinite(patch.price)) target.price = Number(patch.price);
|
|
11205
|
+
if (patch.condition) target.condition = patch.condition;
|
|
11206
|
+
if (patch.label !== void 0) target.label = patch.label;
|
|
11207
|
+
if (patch.color !== void 0) target.color = patch.color;
|
|
11208
|
+
if (patch.active !== void 0) target.active = patch.active;
|
|
11209
|
+
if (patch.once !== void 0) target.once = patch.once;
|
|
11210
|
+
if (patch.draggable !== void 0) target.draggable = patch.draggable;
|
|
11211
|
+
if (patch.triggered !== void 0) {
|
|
11212
|
+
target.triggered = patch.triggered;
|
|
11213
|
+
if (!patch.triggered) target.lastPrice = data[data.length - 1]?.c ?? null;
|
|
11214
|
+
}
|
|
11215
|
+
resetRightMarginCache();
|
|
11216
|
+
scheduleDraw();
|
|
11217
|
+
};
|
|
11218
|
+
const removeAlert = (id) => {
|
|
11219
|
+
alerts = alerts.filter((alert) => alert.id !== id);
|
|
11220
|
+
if (hoveredAlertId === id) hoveredAlertId = null;
|
|
11221
|
+
resetRightMarginCache();
|
|
11222
|
+
scheduleDraw();
|
|
11223
|
+
};
|
|
11224
|
+
const getAlerts = () => alerts.map((alert) => serializeAlert(alert));
|
|
11225
|
+
const onAlertTrigger = (handler) => {
|
|
11226
|
+
alertTriggerHandler = handler;
|
|
11227
|
+
};
|
|
11228
|
+
const onAlertAction = (handler) => {
|
|
11229
|
+
alertActionHandler = handler;
|
|
11230
|
+
};
|
|
11231
|
+
const setTimezone = (timezone) => {
|
|
11232
|
+
timezoneSetting = timezone || "local";
|
|
11233
|
+
zoneClock = createZoneClock(timezoneSetting);
|
|
11234
|
+
scheduleDraw();
|
|
11235
|
+
};
|
|
11236
|
+
const getTimezone = () => timezoneSetting;
|
|
11237
|
+
const setTimeFormat = (format) => {
|
|
11238
|
+
timeFormat = format === "12h" ? "12h" : "24h";
|
|
11239
|
+
resetRightMarginCache();
|
|
11240
|
+
scheduleDraw();
|
|
11241
|
+
};
|
|
11242
|
+
const getTimeFormat = () => timeFormat;
|
|
11243
|
+
const setSession = (session) => {
|
|
11244
|
+
if (session === null) {
|
|
11245
|
+
sessionOptions = { ...sessionOptions, spec: null };
|
|
11246
|
+
} else if (typeof session === "string") {
|
|
11247
|
+
sessionOptions = { ...sessionOptions, spec: session };
|
|
11248
|
+
} else if ("segments" in session || "timezone" in session || "days" in session) {
|
|
11249
|
+
sessionOptions = { ...sessionOptions, spec: session };
|
|
11250
|
+
} else {
|
|
11251
|
+
sessionOptions = { ...sessionOptions, ...session };
|
|
11252
|
+
}
|
|
11253
|
+
compiledSession = compileSession(sessionOptions.spec);
|
|
11254
|
+
scheduleDraw();
|
|
11255
|
+
};
|
|
11256
|
+
const getSession = () => ({ ...sessionOptions });
|
|
10047
11257
|
const setMagnetMode = (mode) => {
|
|
10048
11258
|
magnetMode = mode;
|
|
10049
11259
|
};
|
|
@@ -10063,7 +11273,13 @@ function createChart(element, options = {}) {
|
|
|
10063
11273
|
drawings = dedupeDrawingIds(nextDrawings.map((drawing) => normalizeDrawingState(drawing)));
|
|
10064
11274
|
draftDrawing = null;
|
|
10065
11275
|
reanchorDrawingsToData();
|
|
10066
|
-
|
|
11276
|
+
historySuspended += 1;
|
|
11277
|
+
try {
|
|
11278
|
+
emitDrawingsChange();
|
|
11279
|
+
} finally {
|
|
11280
|
+
historySuspended -= 1;
|
|
11281
|
+
}
|
|
11282
|
+
resetHistoryBaseline();
|
|
10067
11283
|
scheduleDraw();
|
|
10068
11284
|
};
|
|
10069
11285
|
const addDrawing = (drawing) => {
|
|
@@ -10148,7 +11364,10 @@ function createChart(element, options = {}) {
|
|
|
10148
11364
|
indicators: getIndicators(),
|
|
10149
11365
|
magnetMode: getMagnetMode(),
|
|
10150
11366
|
priceScale: getPriceScale(),
|
|
10151
|
-
drawingDefaults
|
|
11367
|
+
drawingDefaults,
|
|
11368
|
+
alerts: getAlerts(),
|
|
11369
|
+
timezone: getTimezone(),
|
|
11370
|
+
timeFormat: getTimeFormat()
|
|
10152
11371
|
};
|
|
10153
11372
|
};
|
|
10154
11373
|
const loadState = (state) => {
|
|
@@ -10177,15 +11396,28 @@ function createChart(element, options = {}) {
|
|
|
10177
11396
|
}
|
|
10178
11397
|
}
|
|
10179
11398
|
}
|
|
11399
|
+
if (Array.isArray(state.alerts)) {
|
|
11400
|
+
setAlerts(state.alerts);
|
|
11401
|
+
}
|
|
11402
|
+
if (typeof state.timezone === "string") {
|
|
11403
|
+
setTimezone(state.timezone);
|
|
11404
|
+
}
|
|
11405
|
+
if (state.timeFormat === "12h" || state.timeFormat === "24h") {
|
|
11406
|
+
setTimeFormat(state.timeFormat);
|
|
11407
|
+
}
|
|
10180
11408
|
if (state.viewport && typeof state.viewport === "object") {
|
|
10181
11409
|
setViewport(state.viewport);
|
|
10182
11410
|
}
|
|
11411
|
+
clearHistory();
|
|
10183
11412
|
};
|
|
10184
11413
|
const takeScreenshot = (options2 = {}) => {
|
|
10185
11414
|
return canvas.toDataURL(options2.type ?? "image/png", options2.quality);
|
|
10186
11415
|
};
|
|
10187
11416
|
const destroy = () => {
|
|
11417
|
+
cancelViewportTween();
|
|
10188
11418
|
cancelLongPressTimer();
|
|
11419
|
+
clearReplayTimer();
|
|
11420
|
+
replay = null;
|
|
10189
11421
|
datafeedUnsubscribe?.();
|
|
10190
11422
|
datafeedUnsubscribe = null;
|
|
10191
11423
|
datafeed = null;
|
|
@@ -10219,6 +11451,10 @@ function createChart(element, options = {}) {
|
|
|
10219
11451
|
}
|
|
10220
11452
|
element.innerHTML = "";
|
|
10221
11453
|
};
|
|
11454
|
+
if (Array.isArray(options.alerts) && options.alerts.length > 0) {
|
|
11455
|
+
setAlerts(options.alerts);
|
|
11456
|
+
}
|
|
11457
|
+
resetHistoryBaseline();
|
|
10222
11458
|
draw();
|
|
10223
11459
|
armCountdownHeartbeat();
|
|
10224
11460
|
return {
|
|
@@ -10302,6 +11538,42 @@ function createChart(element, options = {}) {
|
|
|
10302
11538
|
saveState,
|
|
10303
11539
|
loadState,
|
|
10304
11540
|
takeScreenshot,
|
|
11541
|
+
setTimezone,
|
|
11542
|
+
getTimezone,
|
|
11543
|
+
setTimeFormat,
|
|
11544
|
+
getTimeFormat,
|
|
11545
|
+
setSession,
|
|
11546
|
+
getSession,
|
|
11547
|
+
undo,
|
|
11548
|
+
redo,
|
|
11549
|
+
canUndo: () => historyPast.length > 0,
|
|
11550
|
+
canRedo: () => historyFuture.length > 0,
|
|
11551
|
+
getUndoRedoState,
|
|
11552
|
+
onUndoRedoStateChange: (handler) => {
|
|
11553
|
+
undoRedoStateHandler = handler;
|
|
11554
|
+
},
|
|
11555
|
+
clearHistory,
|
|
11556
|
+
startReplay,
|
|
11557
|
+
stopReplay,
|
|
11558
|
+
replayStep,
|
|
11559
|
+
replayPlay,
|
|
11560
|
+
replayPause,
|
|
11561
|
+
setReplaySpeed,
|
|
11562
|
+
getReplayState,
|
|
11563
|
+
onReplayStateChange,
|
|
11564
|
+
setAlerts,
|
|
11565
|
+
addAlert,
|
|
11566
|
+
updateAlert,
|
|
11567
|
+
removeAlert,
|
|
11568
|
+
getAlerts,
|
|
11569
|
+
onAlertTrigger,
|
|
11570
|
+
onAlertAction,
|
|
11571
|
+
setBarMarks,
|
|
11572
|
+
getBarMarks,
|
|
11573
|
+
setTimescaleMarks,
|
|
11574
|
+
getTimescaleMarks,
|
|
11575
|
+
onMarkClick,
|
|
11576
|
+
onMarkHover,
|
|
10305
11577
|
resize,
|
|
10306
11578
|
destroy
|
|
10307
11579
|
};
|
|
@@ -10312,8 +11584,10 @@ export {
|
|
|
10312
11584
|
FIB_DEFAULT_PALETTE,
|
|
10313
11585
|
POSITION_DEFAULT_COLORS,
|
|
10314
11586
|
SCRIPT_SOURCE_INPUT_VALUES,
|
|
11587
|
+
SESSION_PRESETS,
|
|
10315
11588
|
compileScriptIndicator,
|
|
10316
11589
|
createChart,
|
|
10317
11590
|
extractScriptInputs,
|
|
11591
|
+
isValidTimeZone,
|
|
10318
11592
|
validateScriptSource
|
|
10319
11593
|
};
|