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.cjs
CHANGED
|
@@ -25,9 +25,11 @@ __export(index_exports, {
|
|
|
25
25
|
FIB_DEFAULT_PALETTE: () => FIB_DEFAULT_PALETTE,
|
|
26
26
|
POSITION_DEFAULT_COLORS: () => POSITION_DEFAULT_COLORS,
|
|
27
27
|
SCRIPT_SOURCE_INPUT_VALUES: () => SCRIPT_SOURCE_INPUT_VALUES,
|
|
28
|
+
SESSION_PRESETS: () => SESSION_PRESETS,
|
|
28
29
|
compileScriptIndicator: () => compileScriptIndicator,
|
|
29
30
|
createChart: () => createChart,
|
|
30
31
|
extractScriptInputs: () => extractScriptInputs,
|
|
32
|
+
isValidTimeZone: () => isValidTimeZone,
|
|
31
33
|
validateScriptSource: () => validateScriptSource
|
|
32
34
|
});
|
|
33
35
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -369,26 +371,45 @@ var getSeriesFingerprint = (data) => {
|
|
|
369
371
|
var withCachedSeries = (key, data, compute) => {
|
|
370
372
|
const fingerprint = getSeriesFingerprint(data);
|
|
371
373
|
const existing = builtInSeriesCache.get(key);
|
|
372
|
-
|
|
373
|
-
|
|
374
|
+
const shape = getSeriesShape(data);
|
|
375
|
+
if (existing) {
|
|
376
|
+
const unchanged = existing.fingerprint === fingerprint;
|
|
377
|
+
const liveTickWithinThrottle = !unchanged && indicatorLiveThrottleMs > 0 && existing.shape === shape && Date.now() - existing.computedAt < indicatorLiveThrottleMs;
|
|
378
|
+
if (unchanged || liveTickWithinThrottle) {
|
|
379
|
+
return existing.values;
|
|
380
|
+
}
|
|
374
381
|
}
|
|
375
382
|
const values = compute();
|
|
376
|
-
builtInSeriesCache.set(key, { fingerprint, values });
|
|
383
|
+
builtInSeriesCache.set(key, { fingerprint, values, shape, computedAt: Date.now() });
|
|
377
384
|
return values;
|
|
378
385
|
};
|
|
379
386
|
var builtInComputationCache = /* @__PURE__ */ new Map();
|
|
387
|
+
var indicatorLiveThrottleMs = 80;
|
|
388
|
+
var setIndicatorLiveThrottleMs = (ms) => {
|
|
389
|
+
indicatorLiveThrottleMs = Math.max(0, ms);
|
|
390
|
+
};
|
|
391
|
+
var getSeriesShape = (data) => {
|
|
392
|
+
const length = data.length;
|
|
393
|
+
const last = length > 0 ? data[length - 1] : void 0;
|
|
394
|
+
return last ? `${length}|${last.time.getTime()}` : "empty";
|
|
395
|
+
};
|
|
380
396
|
var COMPUTATION_CACHE_MAX_ENTRIES = 128;
|
|
381
397
|
var withCachedComputation = (key, data, compute) => {
|
|
382
398
|
const fingerprint = getSeriesFingerprint(data);
|
|
383
399
|
const existing = builtInComputationCache.get(key);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
400
|
+
const shape = getSeriesShape(data);
|
|
401
|
+
if (existing) {
|
|
402
|
+
const unchanged = existing.fingerprint === fingerprint;
|
|
403
|
+
const liveTickWithinThrottle = !unchanged && indicatorLiveThrottleMs > 0 && existing.shape === shape && Date.now() - existing.computedAt < indicatorLiveThrottleMs;
|
|
404
|
+
if (unchanged || liveTickWithinThrottle) {
|
|
405
|
+
builtInComputationCache.delete(key);
|
|
406
|
+
builtInComputationCache.set(key, existing);
|
|
407
|
+
return existing.value;
|
|
408
|
+
}
|
|
388
409
|
}
|
|
389
410
|
const value = compute();
|
|
390
411
|
builtInComputationCache.delete(key);
|
|
391
|
-
builtInComputationCache.set(key, { fingerprint, value });
|
|
412
|
+
builtInComputationCache.set(key, { fingerprint, value, shape, computedAt: Date.now() });
|
|
392
413
|
while (builtInComputationCache.size > COMPUTATION_CACHE_MAX_ENTRIES) {
|
|
393
414
|
const oldest = builtInComputationCache.keys().next().value;
|
|
394
415
|
if (oldest === void 0) break;
|
|
@@ -2758,6 +2779,237 @@ var CHART_THEMES = {
|
|
|
2758
2779
|
var CHART_THEME_NAMES = Object.keys(CHART_THEMES);
|
|
2759
2780
|
var resolveTheme = (theme) => typeof theme === "string" ? CHART_THEMES[theme] ?? CHART_THEMES.midnight : theme;
|
|
2760
2781
|
|
|
2782
|
+
// src/time.ts
|
|
2783
|
+
var DAY_MS = 864e5;
|
|
2784
|
+
var HOUR_MS = 36e5;
|
|
2785
|
+
var OFFSET_FORMATTER_CACHE = /* @__PURE__ */ new Map();
|
|
2786
|
+
var LABEL_FORMATTER_CACHE = /* @__PURE__ */ new Map();
|
|
2787
|
+
var getOffsetFormatter = (timeZone) => {
|
|
2788
|
+
let formatter = OFFSET_FORMATTER_CACHE.get(timeZone);
|
|
2789
|
+
if (!formatter) {
|
|
2790
|
+
formatter = new Intl.DateTimeFormat("en-US", {
|
|
2791
|
+
timeZone,
|
|
2792
|
+
hourCycle: "h23",
|
|
2793
|
+
year: "numeric",
|
|
2794
|
+
month: "2-digit",
|
|
2795
|
+
day: "2-digit",
|
|
2796
|
+
hour: "2-digit",
|
|
2797
|
+
minute: "2-digit",
|
|
2798
|
+
second: "2-digit"
|
|
2799
|
+
});
|
|
2800
|
+
OFFSET_FORMATTER_CACHE.set(timeZone, formatter);
|
|
2801
|
+
}
|
|
2802
|
+
return formatter;
|
|
2803
|
+
};
|
|
2804
|
+
var getLabelFormatter = (timeZone, options, key) => {
|
|
2805
|
+
const cacheKey = `${timeZone ?? "local"}|${key}`;
|
|
2806
|
+
let formatter = LABEL_FORMATTER_CACHE.get(cacheKey);
|
|
2807
|
+
if (!formatter) {
|
|
2808
|
+
formatter = new Intl.DateTimeFormat(void 0, timeZone ? { ...options, timeZone } : options);
|
|
2809
|
+
LABEL_FORMATTER_CACHE.set(cacheKey, formatter);
|
|
2810
|
+
}
|
|
2811
|
+
return formatter;
|
|
2812
|
+
};
|
|
2813
|
+
var isValidTimeZone = (timeZone) => {
|
|
2814
|
+
try {
|
|
2815
|
+
new Intl.DateTimeFormat("en-US", { timeZone });
|
|
2816
|
+
return true;
|
|
2817
|
+
} catch {
|
|
2818
|
+
return false;
|
|
2819
|
+
}
|
|
2820
|
+
};
|
|
2821
|
+
var floorDiv = (value, divisor) => Math.floor(value / divisor);
|
|
2822
|
+
var positiveMod = (value, divisor) => (value % divisor + divisor) % divisor;
|
|
2823
|
+
var computeOffsetMs = (timeZone, ms) => {
|
|
2824
|
+
const parts = getOffsetFormatter(timeZone).formatToParts(new Date(ms));
|
|
2825
|
+
let year = 0;
|
|
2826
|
+
let month = 1;
|
|
2827
|
+
let day = 1;
|
|
2828
|
+
let hour = 0;
|
|
2829
|
+
let minute = 0;
|
|
2830
|
+
let second = 0;
|
|
2831
|
+
for (const part of parts) {
|
|
2832
|
+
const value = Number(part.value);
|
|
2833
|
+
if (part.type === "year") year = value;
|
|
2834
|
+
else if (part.type === "month") month = value;
|
|
2835
|
+
else if (part.type === "day") day = value;
|
|
2836
|
+
else if (part.type === "hour") hour = value === 24 ? 0 : value;
|
|
2837
|
+
else if (part.type === "minute") minute = value;
|
|
2838
|
+
else if (part.type === "second") second = value;
|
|
2839
|
+
}
|
|
2840
|
+
const asUtc = Date.UTC(year, month - 1, day, hour, minute, second);
|
|
2841
|
+
return asUtc - Math.floor(ms / 1e3) * 1e3;
|
|
2842
|
+
};
|
|
2843
|
+
var createZoneClock = (timeZone) => {
|
|
2844
|
+
const normalized = !timeZone || timeZone === "local" ? null : timeZone.toLowerCase() === "utc" ? "UTC" : isValidTimeZone(timeZone) ? timeZone : null;
|
|
2845
|
+
if (normalized === null) {
|
|
2846
|
+
const scratch = /* @__PURE__ */ new Date();
|
|
2847
|
+
const partsOf2 = (ms) => {
|
|
2848
|
+
scratch.setTime(ms);
|
|
2849
|
+
return {
|
|
2850
|
+
year: scratch.getFullYear(),
|
|
2851
|
+
month: scratch.getMonth() + 1,
|
|
2852
|
+
day: scratch.getDate(),
|
|
2853
|
+
hour: scratch.getHours(),
|
|
2854
|
+
minute: scratch.getMinutes()
|
|
2855
|
+
};
|
|
2856
|
+
};
|
|
2857
|
+
return {
|
|
2858
|
+
timeZone: null,
|
|
2859
|
+
offsetMs: (ms) => {
|
|
2860
|
+
scratch.setTime(ms);
|
|
2861
|
+
return -scratch.getTimezoneOffset() * 6e4;
|
|
2862
|
+
},
|
|
2863
|
+
dayKey: (ms) => {
|
|
2864
|
+
scratch.setTime(ms);
|
|
2865
|
+
return floorDiv(Date.UTC(scratch.getFullYear(), scratch.getMonth(), scratch.getDate()), DAY_MS);
|
|
2866
|
+
},
|
|
2867
|
+
minutesOfDay: (ms) => {
|
|
2868
|
+
scratch.setTime(ms);
|
|
2869
|
+
return scratch.getHours() * 60 + scratch.getMinutes();
|
|
2870
|
+
},
|
|
2871
|
+
weekday: (ms) => {
|
|
2872
|
+
scratch.setTime(ms);
|
|
2873
|
+
return scratch.getDay();
|
|
2874
|
+
},
|
|
2875
|
+
parts: partsOf2,
|
|
2876
|
+
formatTime: (ms, hour12) => formatClockTime(partsOf2(ms), hour12),
|
|
2877
|
+
formatDayMonth: (ms) => getLabelFormatter(null, { month: "short", day: "numeric" }, "md").format(ms),
|
|
2878
|
+
formatMonth: (ms) => getLabelFormatter(null, { month: "short" }, "m").format(ms),
|
|
2879
|
+
formatYear: (ms) => String(partsOf2(ms).year)
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2882
|
+
const zone = normalized;
|
|
2883
|
+
const offsetCache = /* @__PURE__ */ new Map();
|
|
2884
|
+
const offsetMs = (ms) => {
|
|
2885
|
+
const bucket = floorDiv(ms, HOUR_MS);
|
|
2886
|
+
const cached = offsetCache.get(bucket);
|
|
2887
|
+
if (cached !== void 0) return cached;
|
|
2888
|
+
const offset = computeOffsetMs(zone, ms);
|
|
2889
|
+
if (offsetCache.size > 2e4) offsetCache.clear();
|
|
2890
|
+
offsetCache.set(bucket, offset);
|
|
2891
|
+
return offset;
|
|
2892
|
+
};
|
|
2893
|
+
const localMs = (ms) => ms + offsetMs(ms);
|
|
2894
|
+
const partsOf = (ms) => {
|
|
2895
|
+
const local = new Date(localMs(ms));
|
|
2896
|
+
return {
|
|
2897
|
+
year: local.getUTCFullYear(),
|
|
2898
|
+
month: local.getUTCMonth() + 1,
|
|
2899
|
+
day: local.getUTCDate(),
|
|
2900
|
+
hour: local.getUTCHours(),
|
|
2901
|
+
minute: local.getUTCMinutes()
|
|
2902
|
+
};
|
|
2903
|
+
};
|
|
2904
|
+
return {
|
|
2905
|
+
timeZone: zone,
|
|
2906
|
+
offsetMs,
|
|
2907
|
+
dayKey: (ms) => floorDiv(localMs(ms), DAY_MS),
|
|
2908
|
+
minutesOfDay: (ms) => floorDiv(positiveMod(localMs(ms), DAY_MS), 6e4),
|
|
2909
|
+
// 1970-01-01 was a Thursday (index 4).
|
|
2910
|
+
weekday: (ms) => positiveMod(floorDiv(localMs(ms), DAY_MS) + 4, 7),
|
|
2911
|
+
parts: partsOf,
|
|
2912
|
+
formatTime: (ms, hour12) => formatClockTime(partsOf(ms), hour12),
|
|
2913
|
+
formatDayMonth: (ms) => getLabelFormatter(zone, { month: "short", day: "numeric" }, "md").format(ms),
|
|
2914
|
+
formatMonth: (ms) => getLabelFormatter(zone, { month: "short" }, "m").format(ms),
|
|
2915
|
+
formatYear: (ms) => String(partsOf(ms).year)
|
|
2916
|
+
};
|
|
2917
|
+
};
|
|
2918
|
+
var pad2 = (value) => value < 10 ? `0${value}` : String(value);
|
|
2919
|
+
var formatClockTime = (parts, hour12) => {
|
|
2920
|
+
if (!hour12) return `${pad2(parts.hour)}:${pad2(parts.minute)}`;
|
|
2921
|
+
const suffix = parts.hour < 12 ? "AM" : "PM";
|
|
2922
|
+
const hour = parts.hour % 12 === 0 ? 12 : parts.hour % 12;
|
|
2923
|
+
return `${hour}:${pad2(parts.minute)} ${suffix}`;
|
|
2924
|
+
};
|
|
2925
|
+
var parseHhMm = (value) => {
|
|
2926
|
+
const match = /^(\d{1,2}):?(\d{2})$/.exec(value.trim());
|
|
2927
|
+
if (!match) return null;
|
|
2928
|
+
const hour = Number(match[1]);
|
|
2929
|
+
const minute = Number(match[2]);
|
|
2930
|
+
if (!Number.isFinite(hour) || !Number.isFinite(minute)) return null;
|
|
2931
|
+
if (hour < 0 || hour > 24 || minute < 0 || minute > 59) return null;
|
|
2932
|
+
return hour * 60 + minute;
|
|
2933
|
+
};
|
|
2934
|
+
var SESSION_PRESETS = {
|
|
2935
|
+
"cme-futures": {
|
|
2936
|
+
timezone: "America/New_York",
|
|
2937
|
+
// Opening days: the session opens Sunday–Thursday evening and runs into
|
|
2938
|
+
// the following morning, so Friday evening (weekend) is correctly closed.
|
|
2939
|
+
days: [0, 1, 2, 3, 4],
|
|
2940
|
+
segments: [{ start: "18:00", end: "17:00" }]
|
|
2941
|
+
},
|
|
2942
|
+
"cme-rth": {
|
|
2943
|
+
timezone: "America/New_York",
|
|
2944
|
+
days: [1, 2, 3, 4, 5],
|
|
2945
|
+
segments: [{ start: "09:30", end: "16:00" }]
|
|
2946
|
+
},
|
|
2947
|
+
"us-equities": {
|
|
2948
|
+
timezone: "America/New_York",
|
|
2949
|
+
days: [1, 2, 3, 4, 5],
|
|
2950
|
+
segments: [{ start: "09:30", end: "16:00" }]
|
|
2951
|
+
},
|
|
2952
|
+
"us-equities-extended": {
|
|
2953
|
+
timezone: "America/New_York",
|
|
2954
|
+
days: [1, 2, 3, 4, 5],
|
|
2955
|
+
segments: [{ start: "04:00", end: "20:00" }]
|
|
2956
|
+
},
|
|
2957
|
+
"24x7": {
|
|
2958
|
+
days: [0, 1, 2, 3, 4, 5, 6],
|
|
2959
|
+
segments: [{ start: "00:00", end: "24:00" }]
|
|
2960
|
+
}
|
|
2961
|
+
};
|
|
2962
|
+
var resolveSessionSpec = (spec) => {
|
|
2963
|
+
if (!spec) return null;
|
|
2964
|
+
if (typeof spec === "string") return SESSION_PRESETS[spec] ?? null;
|
|
2965
|
+
return spec;
|
|
2966
|
+
};
|
|
2967
|
+
var compileSession = (spec) => {
|
|
2968
|
+
const resolved = resolveSessionSpec(spec);
|
|
2969
|
+
if (!resolved) return null;
|
|
2970
|
+
const rawSegments = resolved.segments?.length ? resolved.segments : [{ start: "00:00", end: "24:00" }];
|
|
2971
|
+
const segments = [];
|
|
2972
|
+
for (const segment of rawSegments) {
|
|
2973
|
+
const startMinutes = parseHhMm(segment.start);
|
|
2974
|
+
const endMinutesRaw = parseHhMm(segment.end);
|
|
2975
|
+
if (startMinutes === null || endMinutesRaw === null) continue;
|
|
2976
|
+
const endMinutes = endMinutesRaw === 0 ? 1440 : endMinutesRaw;
|
|
2977
|
+
segments.push({ startMinutes, endMinutes, wraps: endMinutes <= startMinutes });
|
|
2978
|
+
}
|
|
2979
|
+
if (segments.length === 0) return null;
|
|
2980
|
+
return {
|
|
2981
|
+
clock: createZoneClock(resolved.timezone ?? null),
|
|
2982
|
+
days: new Set(resolved.days ?? [1, 2, 3, 4, 5]),
|
|
2983
|
+
segments,
|
|
2984
|
+
overnight: segments.some((segment) => segment.wraps)
|
|
2985
|
+
};
|
|
2986
|
+
};
|
|
2987
|
+
var sessionInfoAt = (session, ms) => {
|
|
2988
|
+
const { clock } = session;
|
|
2989
|
+
const dayKey = clock.dayKey(ms);
|
|
2990
|
+
const minutes = clock.minutesOfDay(ms);
|
|
2991
|
+
const weekday = clock.weekday(ms);
|
|
2992
|
+
let sessionDay = dayKey;
|
|
2993
|
+
let inSession = false;
|
|
2994
|
+
for (const segment of session.segments) {
|
|
2995
|
+
if (segment.wraps) {
|
|
2996
|
+
if (minutes >= segment.startMinutes) {
|
|
2997
|
+
if (session.days.has(weekday)) {
|
|
2998
|
+
inSession = true;
|
|
2999
|
+
sessionDay = dayKey + 1;
|
|
3000
|
+
}
|
|
3001
|
+
} else if (minutes < segment.endMinutes) {
|
|
3002
|
+
const openedWeekday = positiveMod(weekday - 1, 7);
|
|
3003
|
+
if (session.days.has(openedWeekday)) inSession = true;
|
|
3004
|
+
sessionDay = dayKey;
|
|
3005
|
+
}
|
|
3006
|
+
} else if (minutes >= segment.startMinutes && minutes < segment.endMinutes && session.days.has(weekday)) {
|
|
3007
|
+
inSession = true;
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
return { inSession, sessionDay };
|
|
3011
|
+
};
|
|
3012
|
+
|
|
2761
3013
|
// src/options.ts
|
|
2762
3014
|
var DEFAULT_GRID_OPTIONS = {
|
|
2763
3015
|
color: "#2b2f38",
|
|
@@ -2958,10 +3210,36 @@ var DEFAULT_OPTIONS = {
|
|
|
2958
3210
|
accessibility: { label: "Price chart", description: "", focusable: true },
|
|
2959
3211
|
downsampling: { enabled: true, thresholdPx: 1.5 },
|
|
2960
3212
|
touch: { hitToleranceScale: 2.2, longPressTooltip: true, longPressMs: 400 },
|
|
3213
|
+
dataLine: {
|
|
3214
|
+
visible: false,
|
|
3215
|
+
symbol: "",
|
|
3216
|
+
interval: "",
|
|
3217
|
+
exchange: "",
|
|
3218
|
+
showOhlc: true,
|
|
3219
|
+
showChange: true,
|
|
3220
|
+
showVolume: false,
|
|
3221
|
+
details: [],
|
|
3222
|
+
statusColor: "",
|
|
3223
|
+
fontSize: 0,
|
|
3224
|
+
symbolColor: "",
|
|
3225
|
+
textColor: ""
|
|
3226
|
+
},
|
|
2961
3227
|
datafeedOptions: { prefetchThresholdBars: 150, cooldownMs: 1200, chunkBars: 1500 },
|
|
3228
|
+
animation: { viewportTransitions: true, durationMs: 260 },
|
|
3229
|
+
indicatorUpdate: { liveThrottleMs: 80 },
|
|
3230
|
+
timezone: "local",
|
|
3231
|
+
timeFormat: "24h",
|
|
3232
|
+
session: {
|
|
3233
|
+
spec: null,
|
|
3234
|
+
separators: true,
|
|
3235
|
+
highlightOutOfSession: false,
|
|
3236
|
+
outOfSessionColor: "rgba(120,132,158,0.07)"
|
|
3237
|
+
},
|
|
3238
|
+
history: { enabled: true, limit: 100 },
|
|
2962
3239
|
crosshair: DEFAULT_CROSSHAIR_OPTIONS,
|
|
2963
3240
|
grid: DEFAULT_GRID_OPTIONS,
|
|
2964
3241
|
watermark: DEFAULT_WATERMARK_OPTIONS,
|
|
3242
|
+
alerts: [],
|
|
2965
3243
|
priceLines: [],
|
|
2966
3244
|
orderLines: [],
|
|
2967
3245
|
tickerLine: {
|
|
@@ -3017,6 +3295,26 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
|
|
|
3017
3295
|
...baseOptions.touch,
|
|
3018
3296
|
...options.touch ?? {}
|
|
3019
3297
|
},
|
|
3298
|
+
dataLine: {
|
|
3299
|
+
...baseOptions.dataLine,
|
|
3300
|
+
...options.dataLine ?? {}
|
|
3301
|
+
},
|
|
3302
|
+
animation: {
|
|
3303
|
+
...baseOptions.animation,
|
|
3304
|
+
...options.animation ?? {}
|
|
3305
|
+
},
|
|
3306
|
+
indicatorUpdate: {
|
|
3307
|
+
...baseOptions.indicatorUpdate,
|
|
3308
|
+
...options.indicatorUpdate ?? {}
|
|
3309
|
+
},
|
|
3310
|
+
session: {
|
|
3311
|
+
...baseOptions.session,
|
|
3312
|
+
...options.session ?? {}
|
|
3313
|
+
},
|
|
3314
|
+
history: {
|
|
3315
|
+
...baseOptions.history,
|
|
3316
|
+
...options.history ?? {}
|
|
3317
|
+
},
|
|
3020
3318
|
datafeedOptions: {
|
|
3021
3319
|
...baseOptions.datafeedOptions,
|
|
3022
3320
|
...options.datafeedOptions ?? {}
|
|
@@ -3180,6 +3478,45 @@ function createChart(element, options = {}) {
|
|
|
3180
3478
|
...drawing.label === void 0 ? {} : { label: drawing.label }
|
|
3181
3479
|
});
|
|
3182
3480
|
let tradeMarkers = [];
|
|
3481
|
+
let timezoneSetting = mergedOptions.timezone ?? "local";
|
|
3482
|
+
let zoneClock = createZoneClock(timezoneSetting);
|
|
3483
|
+
let timeFormat = mergedOptions.timeFormat === "12h" ? "12h" : "24h";
|
|
3484
|
+
const DEFAULT_SESSION_OPTIONS = {
|
|
3485
|
+
spec: null,
|
|
3486
|
+
separators: true,
|
|
3487
|
+
highlightOutOfSession: false,
|
|
3488
|
+
outOfSessionColor: "rgba(120,132,158,0.07)"
|
|
3489
|
+
};
|
|
3490
|
+
let sessionOptions = {
|
|
3491
|
+
...DEFAULT_SESSION_OPTIONS,
|
|
3492
|
+
...mergedOptions.session ?? {}
|
|
3493
|
+
};
|
|
3494
|
+
let compiledSession = compileSession(sessionOptions.spec);
|
|
3495
|
+
const isHour12 = () => timeFormat === "12h";
|
|
3496
|
+
const formatClock = (time) => zoneClock.formatTime(time.getTime(), isHour12());
|
|
3497
|
+
let barMarks = [];
|
|
3498
|
+
let timescaleMarks = [];
|
|
3499
|
+
let markRegions = [];
|
|
3500
|
+
let hoveredMarkKey = null;
|
|
3501
|
+
let markClickHandler = null;
|
|
3502
|
+
let markHoverHandler = null;
|
|
3503
|
+
let alerts = [];
|
|
3504
|
+
let generatedAlertId = 1;
|
|
3505
|
+
let alertTriggerHandler = null;
|
|
3506
|
+
let alertActionHandler = null;
|
|
3507
|
+
let alertRegions = [];
|
|
3508
|
+
let alertDragState = null;
|
|
3509
|
+
let hoveredAlertId = null;
|
|
3510
|
+
let replay = null;
|
|
3511
|
+
let replayStateHandler = null;
|
|
3512
|
+
const historyEnabled = mergedOptions.history?.enabled !== false;
|
|
3513
|
+
const historyLimit = Math.max(1, Math.floor(Number(mergedOptions.history?.limit ?? 100)) || 100);
|
|
3514
|
+
let historyPast = [];
|
|
3515
|
+
let historyFuture = [];
|
|
3516
|
+
let historyBaseline = "";
|
|
3517
|
+
let historySuspended = 0;
|
|
3518
|
+
let undoRedoStateHandler = null;
|
|
3519
|
+
let lastUndoRedoSignature = "";
|
|
3183
3520
|
let indicators = (options.indicators ?? []).map((indicator) => normalizeIndicatorState(indicator));
|
|
3184
3521
|
let drawings = dedupeDrawingIds(
|
|
3185
3522
|
(options.drawings ?? []).map((drawing) => normalizeDrawingState(drawing))
|
|
@@ -3228,9 +3565,168 @@ function createChart(element, options = {}) {
|
|
|
3228
3565
|
let selectedDrawingId = null;
|
|
3229
3566
|
const drawingToolDefaults = /* @__PURE__ */ new Map();
|
|
3230
3567
|
const getDrawingToolDefaults = (tool) => drawingToolDefaults.get(tool) ?? {};
|
|
3568
|
+
const historyDoc = () => JSON.stringify(drawings.map((drawing) => serializeDrawing(drawing)));
|
|
3569
|
+
const describeHistoryChange = (beforeDoc, afterDoc) => {
|
|
3570
|
+
let before = [];
|
|
3571
|
+
let after = [];
|
|
3572
|
+
try {
|
|
3573
|
+
before = JSON.parse(beforeDoc);
|
|
3574
|
+
after = JSON.parse(afterDoc);
|
|
3575
|
+
} catch {
|
|
3576
|
+
return "Edit drawings";
|
|
3577
|
+
}
|
|
3578
|
+
const label = (type) => type ? type.replace(/-/g, " ") : "drawing";
|
|
3579
|
+
if (after.length > before.length) {
|
|
3580
|
+
const beforeIds = new Set(before.map((drawing) => drawing.id));
|
|
3581
|
+
const added = after.find((drawing) => !beforeIds.has(drawing.id));
|
|
3582
|
+
return `Add ${label(added?.type)}`;
|
|
3583
|
+
}
|
|
3584
|
+
if (after.length < before.length) {
|
|
3585
|
+
if (after.length === 0 && before.length > 1) return "Clear drawings";
|
|
3586
|
+
const afterIds = new Set(after.map((drawing) => drawing.id));
|
|
3587
|
+
const removed = before.find((drawing) => !afterIds.has(drawing.id));
|
|
3588
|
+
return `Delete ${label(removed?.type)}`;
|
|
3589
|
+
}
|
|
3590
|
+
const changed = after.find((drawing, index) => JSON.stringify(drawing) !== JSON.stringify(before[index]));
|
|
3591
|
+
return `Edit ${label(changed?.type)}`;
|
|
3592
|
+
};
|
|
3593
|
+
const getUndoRedoState = () => ({
|
|
3594
|
+
canUndo: historyPast.length > 0,
|
|
3595
|
+
canRedo: historyFuture.length > 0,
|
|
3596
|
+
undoLabel: historyPast.length > 0 ? historyPast[historyPast.length - 1].label : null,
|
|
3597
|
+
redoLabel: historyFuture.length > 0 ? historyFuture[historyFuture.length - 1].label : null
|
|
3598
|
+
});
|
|
3599
|
+
const emitUndoRedoState = () => {
|
|
3600
|
+
const state = getUndoRedoState();
|
|
3601
|
+
const signature = `${state.canUndo}|${state.canRedo}|${state.undoLabel}|${state.redoLabel}`;
|
|
3602
|
+
if (signature === lastUndoRedoSignature) return;
|
|
3603
|
+
lastUndoRedoSignature = signature;
|
|
3604
|
+
undoRedoStateHandler?.(state);
|
|
3605
|
+
};
|
|
3606
|
+
const commitHistory = () => {
|
|
3607
|
+
if (!historyEnabled || historySuspended > 0) return;
|
|
3608
|
+
if (drawingDragState !== null) return;
|
|
3609
|
+
const next = historyDoc();
|
|
3610
|
+
if (next === historyBaseline) return;
|
|
3611
|
+
historyPast.push({ label: describeHistoryChange(historyBaseline, next), doc: historyBaseline });
|
|
3612
|
+
if (historyPast.length > historyLimit) historyPast.shift();
|
|
3613
|
+
historyFuture = [];
|
|
3614
|
+
historyBaseline = next;
|
|
3615
|
+
emitUndoRedoState();
|
|
3616
|
+
};
|
|
3617
|
+
const resetHistoryBaseline = () => {
|
|
3618
|
+
historyBaseline = historyDoc();
|
|
3619
|
+
};
|
|
3620
|
+
const applyHistoryDoc = (doc) => {
|
|
3621
|
+
let parsed;
|
|
3622
|
+
try {
|
|
3623
|
+
parsed = JSON.parse(doc);
|
|
3624
|
+
} catch {
|
|
3625
|
+
return;
|
|
3626
|
+
}
|
|
3627
|
+
historySuspended += 1;
|
|
3628
|
+
try {
|
|
3629
|
+
drawings = dedupeDrawingIds(parsed.map((drawing) => normalizeDrawingState(drawing)));
|
|
3630
|
+
if (selectedDrawingId !== null && !drawings.some((drawing) => drawing.id === selectedDrawingId)) {
|
|
3631
|
+
selectedDrawingId = null;
|
|
3632
|
+
}
|
|
3633
|
+
draftDrawing = null;
|
|
3634
|
+
drawingDragState = null;
|
|
3635
|
+
historyBaseline = doc;
|
|
3636
|
+
emitDrawingsChange();
|
|
3637
|
+
scheduleDraw({ updateAutoScale: true });
|
|
3638
|
+
} finally {
|
|
3639
|
+
historySuspended -= 1;
|
|
3640
|
+
}
|
|
3641
|
+
};
|
|
3642
|
+
const undo = () => {
|
|
3643
|
+
if (!historyEnabled || historyPast.length === 0) return false;
|
|
3644
|
+
const entry = historyPast.pop();
|
|
3645
|
+
historyFuture.push({ label: entry.label, doc: historyBaseline });
|
|
3646
|
+
applyHistoryDoc(entry.doc);
|
|
3647
|
+
emitUndoRedoState();
|
|
3648
|
+
return true;
|
|
3649
|
+
};
|
|
3650
|
+
const redo = () => {
|
|
3651
|
+
if (!historyEnabled || historyFuture.length === 0) return false;
|
|
3652
|
+
const entry = historyFuture.pop();
|
|
3653
|
+
historyPast.push({ label: entry.label, doc: historyBaseline });
|
|
3654
|
+
applyHistoryDoc(entry.doc);
|
|
3655
|
+
emitUndoRedoState();
|
|
3656
|
+
return true;
|
|
3657
|
+
};
|
|
3658
|
+
const clearHistory = () => {
|
|
3659
|
+
historyPast = [];
|
|
3660
|
+
historyFuture = [];
|
|
3661
|
+
resetHistoryBaseline();
|
|
3662
|
+
emitUndoRedoState();
|
|
3663
|
+
};
|
|
3231
3664
|
const emitDrawingsChange = () => {
|
|
3665
|
+
commitHistory();
|
|
3232
3666
|
drawingsChangeHandler?.(drawings.map((drawing) => serializeDrawing(drawing)));
|
|
3233
3667
|
};
|
|
3668
|
+
const normalizeAlert = (alert) => ({
|
|
3669
|
+
id: alert.id ?? `alert-${generatedAlertId++}`,
|
|
3670
|
+
price: Number(alert.price),
|
|
3671
|
+
condition: alert.condition ?? "crossing",
|
|
3672
|
+
label: alert.label ?? "",
|
|
3673
|
+
color: alert.color ?? "#f7a600",
|
|
3674
|
+
active: alert.active !== false,
|
|
3675
|
+
triggered: alert.triggered === true,
|
|
3676
|
+
once: alert.once !== false,
|
|
3677
|
+
draggable: alert.draggable !== false,
|
|
3678
|
+
lastPrice: null
|
|
3679
|
+
});
|
|
3680
|
+
const serializeAlert = (alert) => ({
|
|
3681
|
+
id: alert.id,
|
|
3682
|
+
price: alert.price,
|
|
3683
|
+
condition: alert.condition,
|
|
3684
|
+
label: alert.label,
|
|
3685
|
+
color: alert.color,
|
|
3686
|
+
active: alert.active,
|
|
3687
|
+
triggered: alert.triggered,
|
|
3688
|
+
once: alert.once,
|
|
3689
|
+
draggable: alert.draggable
|
|
3690
|
+
});
|
|
3691
|
+
const evaluateAlerts = (price, timeMs, index) => {
|
|
3692
|
+
if (alerts.length === 0 || !Number.isFinite(price)) return;
|
|
3693
|
+
for (const alert of alerts) {
|
|
3694
|
+
if (!alert.active || alert.triggered || !Number.isFinite(alert.price)) {
|
|
3695
|
+
alert.lastPrice = price;
|
|
3696
|
+
continue;
|
|
3697
|
+
}
|
|
3698
|
+
const previous = alert.lastPrice;
|
|
3699
|
+
let hit = false;
|
|
3700
|
+
switch (alert.condition) {
|
|
3701
|
+
case "greater":
|
|
3702
|
+
hit = price > alert.price;
|
|
3703
|
+
break;
|
|
3704
|
+
case "less":
|
|
3705
|
+
hit = price < alert.price;
|
|
3706
|
+
break;
|
|
3707
|
+
case "crossing-up":
|
|
3708
|
+
hit = previous !== null && previous < alert.price && price >= alert.price;
|
|
3709
|
+
break;
|
|
3710
|
+
case "crossing-down":
|
|
3711
|
+
hit = previous !== null && previous > alert.price && price <= alert.price;
|
|
3712
|
+
break;
|
|
3713
|
+
default:
|
|
3714
|
+
hit = previous !== null && (previous < alert.price && price >= alert.price || previous > alert.price && price <= alert.price);
|
|
3715
|
+
break;
|
|
3716
|
+
}
|
|
3717
|
+
alert.lastPrice = price;
|
|
3718
|
+
if (!hit) continue;
|
|
3719
|
+
alert.triggered = true;
|
|
3720
|
+
if (alert.once) alert.active = false;
|
|
3721
|
+
alertTriggerHandler?.({ alert: serializeAlert(alert), price, timeMs, index });
|
|
3722
|
+
scheduleDraw({ overlayOnly: false });
|
|
3723
|
+
}
|
|
3724
|
+
};
|
|
3725
|
+
const evaluateAlertsFromLatestBar = () => {
|
|
3726
|
+
if (alerts.length === 0 || data.length === 0) return;
|
|
3727
|
+
const last = data[data.length - 1];
|
|
3728
|
+
evaluateAlerts(last.c, last.time.getTime(), data.length - 1);
|
|
3729
|
+
};
|
|
3234
3730
|
const orderWidgetWidthById = /* @__PURE__ */ new Map();
|
|
3235
3731
|
const orderPriceTagWidthById = /* @__PURE__ */ new Map();
|
|
3236
3732
|
let xCenter = 0;
|
|
@@ -3276,6 +3772,7 @@ function createChart(element, options = {}) {
|
|
|
3276
3772
|
let crosshairPoint = null;
|
|
3277
3773
|
let doubleClickEnabled = mergedOptions.doubleClickEnabled;
|
|
3278
3774
|
let doubleClickAction = mergedOptions.doubleClickAction;
|
|
3775
|
+
setIndicatorLiveThrottleMs(mergedOptions.indicatorUpdate?.liveThrottleMs ?? 80);
|
|
3279
3776
|
let noOverlappingLineLabels = DEFAULT_LABELS_OPTIONS.noOverlapping;
|
|
3280
3777
|
let rightAxisLabelSlots = [];
|
|
3281
3778
|
let plotLabelSlots = [];
|
|
@@ -3402,6 +3899,9 @@ function createChart(element, options = {}) {
|
|
|
3402
3899
|
let overlayLayout = null;
|
|
3403
3900
|
const margin = { top: 16, right: 72, bottom: 26, left: 12 };
|
|
3404
3901
|
let cachedRightMargin = margin.right;
|
|
3902
|
+
let rightMarginShrinkPendingSince = null;
|
|
3903
|
+
const RIGHT_MARGIN_SHRINK_DELAY_MS = 600;
|
|
3904
|
+
const RIGHT_MARGIN_SHRINK_EPSILON_PX = 2;
|
|
3405
3905
|
const minVisibleBars = Math.max(1, Math.floor(mergedOptions.minVisibleBars));
|
|
3406
3906
|
const maxVisibleBars = Math.max(minVisibleBars, Math.floor(mergedOptions.maxVisibleBars));
|
|
3407
3907
|
const maxPanBars = Math.max(0, Math.floor(mergedOptions.maxPanBars));
|
|
@@ -3517,21 +4017,69 @@ function createChart(element, options = {}) {
|
|
|
3517
4017
|
Math.min(highCenter, count + maxPanBars)
|
|
3518
4018
|
);
|
|
3519
4019
|
};
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
if (
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
4020
|
+
let viewportTweenRaf = null;
|
|
4021
|
+
const cancelViewportTween = () => {
|
|
4022
|
+
if (viewportTweenRaf !== null) {
|
|
4023
|
+
cancelAnimationFrame(viewportTweenRaf);
|
|
4024
|
+
viewportTweenRaf = null;
|
|
4025
|
+
}
|
|
4026
|
+
};
|
|
4027
|
+
const prefersReducedMotion = () => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
4028
|
+
const animateViewportTo = (targetCenter, targetSpan, onComplete) => {
|
|
4029
|
+
const animation = mergedOptions.animation ?? DEFAULT_OPTIONS.animation;
|
|
4030
|
+
const duration = Math.max(0, animation?.durationMs ?? 260);
|
|
4031
|
+
const startCenter = xCenter;
|
|
4032
|
+
const startSpan = xSpan;
|
|
4033
|
+
const centerDelta = targetCenter - startCenter;
|
|
4034
|
+
const spanRatio = startSpan > 0 ? targetSpan / startSpan : 1;
|
|
4035
|
+
const negligible = Math.abs(centerDelta) < 0.5 && Math.abs(spanRatio - 1) < 0.01;
|
|
4036
|
+
if (animation?.viewportTransitions === false || duration === 0 || negligible || prefersReducedMotion() || typeof requestAnimationFrame !== "function") {
|
|
4037
|
+
xCenter = targetCenter;
|
|
4038
|
+
xSpan = targetSpan;
|
|
4039
|
+
clampXViewport();
|
|
4040
|
+
onComplete?.();
|
|
4041
|
+
emitViewportChange();
|
|
4042
|
+
scheduleDraw();
|
|
4043
|
+
return false;
|
|
3526
4044
|
}
|
|
4045
|
+
cancelViewportTween();
|
|
4046
|
+
const startedAt = performance.now();
|
|
4047
|
+
const step = (now) => {
|
|
4048
|
+
const t = clamp((now - startedAt) / duration, 0, 1);
|
|
4049
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
4050
|
+
xCenter = startCenter + centerDelta * eased;
|
|
4051
|
+
xSpan = startSpan * Math.pow(spanRatio, eased);
|
|
4052
|
+
clampXViewport();
|
|
4053
|
+
scheduleDraw();
|
|
4054
|
+
if (t < 1) {
|
|
4055
|
+
viewportTweenRaf = requestAnimationFrame(step);
|
|
4056
|
+
return;
|
|
4057
|
+
}
|
|
4058
|
+
viewportTweenRaf = null;
|
|
4059
|
+
xCenter = targetCenter;
|
|
4060
|
+
xSpan = targetSpan;
|
|
4061
|
+
clampXViewport();
|
|
4062
|
+
onComplete?.();
|
|
4063
|
+
emitViewportChange();
|
|
4064
|
+
scheduleDraw();
|
|
4065
|
+
};
|
|
4066
|
+
viewportTweenRaf = requestAnimationFrame(step);
|
|
4067
|
+
return true;
|
|
4068
|
+
};
|
|
4069
|
+
const getFitXTarget = () => {
|
|
4070
|
+
const count = data.length;
|
|
4071
|
+
if (count === 0) return { center: 0, span: 60 };
|
|
3527
4072
|
const maxSpan = Math.min(maxVisibleBars, Math.max(minVisibleBars, count + maxPanBars * 2));
|
|
3528
|
-
const
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
}
|
|
3533
|
-
|
|
3534
|
-
|
|
4073
|
+
const span = clamp(Math.floor(mergedOptions.initialVisibleBars), minVisibleBars, maxSpan);
|
|
4074
|
+
return {
|
|
4075
|
+
center: mergedOptions.initialViewport === "center" ? count / 2 : count - span / 2 + rightEdgePaddingBars,
|
|
4076
|
+
span
|
|
4077
|
+
};
|
|
4078
|
+
};
|
|
4079
|
+
const fitXViewport = () => {
|
|
4080
|
+
const target = getFitXTarget();
|
|
4081
|
+
xSpan = target.span;
|
|
4082
|
+
xCenter = target.center;
|
|
3535
4083
|
clampXViewport();
|
|
3536
4084
|
};
|
|
3537
4085
|
const getYBounds = () => {
|
|
@@ -3681,6 +4229,7 @@ function createChart(element, options = {}) {
|
|
|
3681
4229
|
};
|
|
3682
4230
|
const resetRightMarginCache = () => {
|
|
3683
4231
|
cachedRightMargin = margin.right;
|
|
4232
|
+
rightMarginShrinkPendingSince = null;
|
|
3684
4233
|
};
|
|
3685
4234
|
const parseData = (nextData) => {
|
|
3686
4235
|
const dedupedByTime = /* @__PURE__ */ new Map();
|
|
@@ -4043,40 +4592,13 @@ function createChart(element, options = {}) {
|
|
|
4043
4592
|
datafeedUnsubscribe = typeof unsubscribe === "function" ? unsubscribe : null;
|
|
4044
4593
|
};
|
|
4045
4594
|
const formatHoverTimeLabel = (time, mode) => {
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
hour12: false
|
|
4051
|
-
});
|
|
4052
|
-
}
|
|
4053
|
-
if (mode === "datetime") {
|
|
4054
|
-
return time.toLocaleString(void 0, {
|
|
4055
|
-
month: "short",
|
|
4056
|
-
day: "numeric",
|
|
4057
|
-
hour: "2-digit",
|
|
4058
|
-
minute: "2-digit",
|
|
4059
|
-
hour12: false
|
|
4060
|
-
});
|
|
4061
|
-
}
|
|
4062
|
-
if (mode === "date") {
|
|
4063
|
-
return time.toLocaleDateString(void 0, {
|
|
4064
|
-
month: "short",
|
|
4065
|
-
day: "numeric"
|
|
4066
|
-
});
|
|
4067
|
-
}
|
|
4595
|
+
const ms = time.getTime();
|
|
4596
|
+
if (mode === "time") return formatClock(time);
|
|
4597
|
+
if (mode === "datetime") return `${zoneClock.formatDayMonth(ms)}, ${formatClock(time)}`;
|
|
4598
|
+
if (mode === "date") return zoneClock.formatDayMonth(ms);
|
|
4068
4599
|
const stepMs = getTimeStepMs();
|
|
4069
|
-
if (stepMs < 24 * 60 * 60 * 1e3)
|
|
4070
|
-
|
|
4071
|
-
hour: "2-digit",
|
|
4072
|
-
minute: "2-digit",
|
|
4073
|
-
hour12: false
|
|
4074
|
-
});
|
|
4075
|
-
}
|
|
4076
|
-
return time.toLocaleDateString(void 0, {
|
|
4077
|
-
month: "short",
|
|
4078
|
-
day: "numeric"
|
|
4079
|
-
});
|
|
4600
|
+
if (stepMs < 24 * 60 * 60 * 1e3) return formatClock(time);
|
|
4601
|
+
return zoneClock.formatDayMonth(ms);
|
|
4080
4602
|
};
|
|
4081
4603
|
const drawText = (text, x, y, align = "left", baseline = "alphabetic", color = mergedOptions.axis?.textColor ?? mergedOptions.axisColor) => {
|
|
4082
4604
|
ctx.fillStyle = color;
|
|
@@ -4173,6 +4695,86 @@ function createChart(element, options = {}) {
|
|
|
4173
4695
|
labelTextColorOn(mergedLine.labelBackgroundColor, mergedLine.labelTextColor)
|
|
4174
4696
|
);
|
|
4175
4697
|
};
|
|
4698
|
+
const drawBellGlyph = (x, y, size, color) => {
|
|
4699
|
+
const w = size;
|
|
4700
|
+
const h = size;
|
|
4701
|
+
ctx.save();
|
|
4702
|
+
ctx.strokeStyle = color;
|
|
4703
|
+
ctx.fillStyle = color;
|
|
4704
|
+
ctx.lineWidth = 1.2;
|
|
4705
|
+
ctx.lineJoin = "round";
|
|
4706
|
+
ctx.beginPath();
|
|
4707
|
+
ctx.moveTo(x - w * 0.42, y + h * 0.28);
|
|
4708
|
+
ctx.quadraticCurveTo(x - w * 0.3, y + h * 0.2, x - w * 0.3, y - h * 0.05);
|
|
4709
|
+
ctx.quadraticCurveTo(x - w * 0.3, y - h * 0.46, x, y - h * 0.46);
|
|
4710
|
+
ctx.quadraticCurveTo(x + w * 0.3, y - h * 0.46, x + w * 0.3, y - h * 0.05);
|
|
4711
|
+
ctx.quadraticCurveTo(x + w * 0.3, y + h * 0.2, x + w * 0.42, y + h * 0.28);
|
|
4712
|
+
ctx.closePath();
|
|
4713
|
+
ctx.stroke();
|
|
4714
|
+
ctx.beginPath();
|
|
4715
|
+
ctx.arc(x, y + h * 0.42, Math.max(1, w * 0.1), 0, Math.PI * 2);
|
|
4716
|
+
ctx.fill();
|
|
4717
|
+
ctx.restore();
|
|
4718
|
+
};
|
|
4719
|
+
const drawAlertLine = (alert, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
|
|
4720
|
+
const lineY = getLineY(alert.price, yFromPrice, chartTop, chartBottom, false);
|
|
4721
|
+
if (lineY === null) return;
|
|
4722
|
+
const dim = alert.triggered || !alert.active;
|
|
4723
|
+
const color = alert.color || "#f7a600";
|
|
4724
|
+
const axis = resolvedAxis;
|
|
4725
|
+
ctx.save();
|
|
4726
|
+
ctx.globalAlpha = dim ? 0.5 : 1;
|
|
4727
|
+
ctx.strokeStyle = color;
|
|
4728
|
+
ctx.lineWidth = 1;
|
|
4729
|
+
applyDashPattern("dashed", dashPatterns.dotted, dashPatterns.dashed);
|
|
4730
|
+
ctx.beginPath();
|
|
4731
|
+
ctx.moveTo(crisp(chartLeft), crisp(lineY));
|
|
4732
|
+
ctx.lineTo(crisp(chartRight), crisp(lineY));
|
|
4733
|
+
ctx.stroke();
|
|
4734
|
+
ctx.restore();
|
|
4735
|
+
const fontSize = Math.max(8, axis.fontSize);
|
|
4736
|
+
ctx.font = `${fontSize}px ${mergedOptions.fontFamily}`;
|
|
4737
|
+
const text = alert.label ? `${formatPrice(alert.price)} ${alert.label}` : formatPrice(alert.price);
|
|
4738
|
+
const paddingX = 6;
|
|
4739
|
+
const bellWidth = fontSize + 2;
|
|
4740
|
+
const labelHeight = 20;
|
|
4741
|
+
const hovered = hoveredAlertId === alert.id;
|
|
4742
|
+
const removeWidth = hovered ? labelHeight : 0;
|
|
4743
|
+
const contentWidth = Math.ceil(measureTextWidth(text)) + bellWidth + paddingX * 3 + removeWidth;
|
|
4744
|
+
const labelWidth = getRightAxisLabelWidth(chartRight, contentWidth);
|
|
4745
|
+
const labelX = getRightAxisLabelX(chartRight);
|
|
4746
|
+
const labelY = placeRightAxisLabel(lineY - labelHeight / 2, labelHeight, chartTop, chartBottom - labelHeight);
|
|
4747
|
+
ctx.save();
|
|
4748
|
+
ctx.globalAlpha = dim ? 0.6 : 1;
|
|
4749
|
+
ctx.fillStyle = color;
|
|
4750
|
+
fillRoundedRect(Math.round(labelX), Math.round(labelY), labelWidth, labelHeight, 3);
|
|
4751
|
+
const inkColor = labelTextColorOn(color, "#101114");
|
|
4752
|
+
drawBellGlyph(labelX + paddingX + bellWidth / 2, labelY + labelHeight / 2, fontSize, inkColor);
|
|
4753
|
+
drawText(text, labelX + paddingX * 2 + bellWidth, labelY + labelHeight / 2, "left", "middle", inkColor);
|
|
4754
|
+
if (hovered) {
|
|
4755
|
+
const removeX = labelX + labelWidth - removeWidth;
|
|
4756
|
+
ctx.strokeStyle = inkColor;
|
|
4757
|
+
ctx.lineWidth = 1.4;
|
|
4758
|
+
const inset = 6;
|
|
4759
|
+
ctx.beginPath();
|
|
4760
|
+
ctx.moveTo(removeX + inset, labelY + inset);
|
|
4761
|
+
ctx.lineTo(removeX + removeWidth - inset, labelY + labelHeight - inset);
|
|
4762
|
+
ctx.moveTo(removeX + removeWidth - inset, labelY + inset);
|
|
4763
|
+
ctx.lineTo(removeX + inset, labelY + labelHeight - inset);
|
|
4764
|
+
ctx.stroke();
|
|
4765
|
+
alertRegions.push({
|
|
4766
|
+
id: alert.id,
|
|
4767
|
+
x: removeX,
|
|
4768
|
+
y: labelY,
|
|
4769
|
+
width: removeWidth,
|
|
4770
|
+
height: labelHeight,
|
|
4771
|
+
part: "remove"
|
|
4772
|
+
});
|
|
4773
|
+
}
|
|
4774
|
+
ctx.restore();
|
|
4775
|
+
alertRegions.push({ id: alert.id, x: labelX, y: labelY, width: labelWidth - removeWidth, height: labelHeight, part: "line" });
|
|
4776
|
+
alertRegions.push({ id: alert.id, x: chartLeft, y: lineY - 5, width: chartRight - chartLeft, height: 10, part: "line" });
|
|
4777
|
+
};
|
|
4176
4778
|
const drawOrderLine = (line, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
|
|
4177
4779
|
const mergedLine = { ...DEFAULT_ORDER_LINE_OPTIONS, ...line };
|
|
4178
4780
|
const renderPrice = mergedLine.behavior === "follow" && Number.isFinite(mergedLine.followPrice) ? mergedLine.followPrice : mergedLine.price;
|
|
@@ -4457,6 +5059,37 @@ function createChart(element, options = {}) {
|
|
|
4457
5059
|
ctx.closePath();
|
|
4458
5060
|
ctx.stroke();
|
|
4459
5061
|
};
|
|
5062
|
+
const drawMarkBadge = (x, y, radius, color, shape, label, textColor) => {
|
|
5063
|
+
ctx.save();
|
|
5064
|
+
ctx.fillStyle = color;
|
|
5065
|
+
ctx.beginPath();
|
|
5066
|
+
if (shape === "square") {
|
|
5067
|
+
ctx.rect(x - radius, y - radius, radius * 2, radius * 2);
|
|
5068
|
+
} else if (shape === "diamond") {
|
|
5069
|
+
ctx.moveTo(x, y - radius);
|
|
5070
|
+
ctx.lineTo(x + radius, y);
|
|
5071
|
+
ctx.lineTo(x, y + radius);
|
|
5072
|
+
ctx.lineTo(x - radius, y);
|
|
5073
|
+
ctx.closePath();
|
|
5074
|
+
} else if (shape === "flag") {
|
|
5075
|
+
ctx.rect(x - radius * 0.16, y - radius, radius * 0.32, radius * 2);
|
|
5076
|
+
ctx.moveTo(x + radius * 0.16, y - radius);
|
|
5077
|
+
ctx.lineTo(x + radius * 1.35, y - radius * 0.45);
|
|
5078
|
+
ctx.lineTo(x + radius * 0.16, y + radius * 0.1);
|
|
5079
|
+
ctx.closePath();
|
|
5080
|
+
} else {
|
|
5081
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
5082
|
+
}
|
|
5083
|
+
ctx.fill();
|
|
5084
|
+
if (label && shape !== "flag") {
|
|
5085
|
+
ctx.fillStyle = textColor;
|
|
5086
|
+
ctx.font = `600 ${Math.max(8, Math.round(radius * 1.2))}px ${mergedOptions.fontFamily}`;
|
|
5087
|
+
ctx.textAlign = "center";
|
|
5088
|
+
ctx.textBaseline = "middle";
|
|
5089
|
+
ctx.fillText(label.slice(0, 2), x, y + 0.5);
|
|
5090
|
+
}
|
|
5091
|
+
ctx.restore();
|
|
5092
|
+
};
|
|
4460
5093
|
const setCrosshairPoint = (point) => {
|
|
4461
5094
|
const current = crosshairPoint;
|
|
4462
5095
|
if (point === null && current === null) {
|
|
@@ -4504,6 +5137,8 @@ function createChart(element, options = {}) {
|
|
|
4504
5137
|
const shouldUpdateAutoScale = options2.updateAutoScale ?? true;
|
|
4505
5138
|
orderActionRegions = [];
|
|
4506
5139
|
orderDragRegions = [];
|
|
5140
|
+
alertRegions = [];
|
|
5141
|
+
markRegions = [];
|
|
4507
5142
|
crosshairPriceActionRegion = null;
|
|
4508
5143
|
const pixelRatio = getPixelRatio();
|
|
4509
5144
|
canvas.style.width = `${width}px`;
|
|
@@ -4586,7 +5221,23 @@ function createChart(element, options = {}) {
|
|
|
4586
5221
|
}
|
|
4587
5222
|
const maxRightMargin = Math.max(margin.right, width - margin.left - 160);
|
|
4588
5223
|
const nextRightMargin = Math.min(maxRightMargin, Math.max(margin.right, Math.ceil(required + 1)));
|
|
4589
|
-
|
|
5224
|
+
if (nextRightMargin >= cachedRightMargin) {
|
|
5225
|
+
cachedRightMargin = nextRightMargin;
|
|
5226
|
+
rightMarginShrinkPendingSince = null;
|
|
5227
|
+
} else if (cachedRightMargin - nextRightMargin > RIGHT_MARGIN_SHRINK_EPSILON_PX) {
|
|
5228
|
+
const now = performance.now();
|
|
5229
|
+
if (rightMarginShrinkPendingSince === null) {
|
|
5230
|
+
rightMarginShrinkPendingSince = now;
|
|
5231
|
+
}
|
|
5232
|
+
if (now - rightMarginShrinkPendingSince >= RIGHT_MARGIN_SHRINK_DELAY_MS) {
|
|
5233
|
+
cachedRightMargin = nextRightMargin;
|
|
5234
|
+
rightMarginShrinkPendingSince = null;
|
|
5235
|
+
} else {
|
|
5236
|
+
scheduleDraw({ updateAutoScale: false });
|
|
5237
|
+
}
|
|
5238
|
+
} else {
|
|
5239
|
+
rightMarginShrinkPendingSince = null;
|
|
5240
|
+
}
|
|
4590
5241
|
return Math.min(maxRightMargin, cachedRightMargin);
|
|
4591
5242
|
};
|
|
4592
5243
|
const rightMargin = estimateRightMargin();
|
|
@@ -5759,12 +6410,14 @@ function createChart(element, options = {}) {
|
|
|
5759
6410
|
if (!prevTime) {
|
|
5760
6411
|
weight = 7;
|
|
5761
6412
|
} else {
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
6413
|
+
const prev = zoneClock.parts(prevTime.getTime());
|
|
6414
|
+
const current = zoneClock.parts(time.getTime());
|
|
6415
|
+
if (prev.year !== current.year) weight = 9;
|
|
6416
|
+
else if (prev.month !== current.month) weight = 8;
|
|
6417
|
+
else if (prev.day !== current.day) weight = 7;
|
|
5765
6418
|
else {
|
|
5766
|
-
const prevMinutes =
|
|
5767
|
-
const curMinutes =
|
|
6419
|
+
const prevMinutes = prev.hour * 60 + prev.minute;
|
|
6420
|
+
const curMinutes = current.hour * 60 + current.minute;
|
|
5768
6421
|
const crossed = (span) => Math.floor(prevMinutes / span) !== Math.floor(curMinutes / span);
|
|
5769
6422
|
if (crossed(360)) weight = 6;
|
|
5770
6423
|
else if (crossed(60)) weight = 5;
|
|
@@ -5810,31 +6463,64 @@ function createChart(element, options = {}) {
|
|
|
5810
6463
|
ctx.stroke();
|
|
5811
6464
|
ctx.restore();
|
|
5812
6465
|
}
|
|
5813
|
-
|
|
6466
|
+
const sessionSeparatorsOn = grid.sessionSeparators || compiledSession !== null && sessionOptions.separators !== false;
|
|
6467
|
+
const intradaySeries = (() => {
|
|
6468
|
+
if (data.length < 2) return false;
|
|
6469
|
+
const probe = Math.min(Math.max(1, startIndex), data.length - 1);
|
|
6470
|
+
const delta = Math.abs(data[probe].time.getTime() - data[probe - 1].time.getTime());
|
|
6471
|
+
return delta > 0 && delta < 0.9 * 864e5;
|
|
6472
|
+
})();
|
|
6473
|
+
if (compiledSession && sessionOptions.highlightOutOfSession && intradaySeries && data.length > 0) {
|
|
6474
|
+
const session = compiledSession;
|
|
6475
|
+
ctx.save();
|
|
6476
|
+
ctx.fillStyle = sessionOptions.outOfSessionColor;
|
|
6477
|
+
let runStart = null;
|
|
6478
|
+
const flushRun = (endExclusive) => {
|
|
6479
|
+
if (runStart === null) return;
|
|
6480
|
+
const left = chartLeft + (runStart - xStart) / xSpan * chartWidth;
|
|
6481
|
+
const right = chartLeft + (endExclusive - xStart) / xSpan * chartWidth;
|
|
6482
|
+
const clampedLeft = Math.max(chartLeft, left);
|
|
6483
|
+
const clampedRight = Math.min(chartRight, right);
|
|
6484
|
+
if (clampedRight > clampedLeft) {
|
|
6485
|
+
ctx.fillRect(clampedLeft, chartTop, clampedRight - clampedLeft, fullChartBottom - chartTop);
|
|
6486
|
+
}
|
|
6487
|
+
runStart = null;
|
|
6488
|
+
};
|
|
6489
|
+
for (let index = Math.max(0, startIndex); index <= endIndex; index += 1) {
|
|
6490
|
+
const point = data[index];
|
|
6491
|
+
if (!point) continue;
|
|
6492
|
+
const inSession = sessionInfoAt(session, point.time.getTime()).inSession;
|
|
6493
|
+
if (!inSession) {
|
|
6494
|
+
if (runStart === null) runStart = index;
|
|
6495
|
+
} else {
|
|
6496
|
+
flushRun(index);
|
|
6497
|
+
}
|
|
6498
|
+
}
|
|
6499
|
+
flushRun(endIndex + 1);
|
|
6500
|
+
ctx.restore();
|
|
6501
|
+
}
|
|
6502
|
+
if (sessionSeparatorsOn && data.length > 1 && intradaySeries) {
|
|
5814
6503
|
const first = Math.max(1, startIndex);
|
|
5815
|
-
const
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
if (sessionDayOf(point.time.getTime()) === sessionDayOf(prev.time.getTime())) {
|
|
5828
|
-
continue;
|
|
5829
|
-
}
|
|
5830
|
-
const x = chartLeft + (index - xStart) / xSpan * chartWidth;
|
|
5831
|
-
ctx.beginPath();
|
|
5832
|
-
ctx.moveTo(crisp(x), crisp(chartTop));
|
|
5833
|
-
ctx.lineTo(crisp(x), crisp(fullChartBottom));
|
|
5834
|
-
ctx.stroke();
|
|
6504
|
+
const session = compiledSession;
|
|
6505
|
+
const dayOf = (ms) => session ? sessionInfoAt(session, ms).sessionDay : zoneClock.dayKey(ms);
|
|
6506
|
+
ctx.save();
|
|
6507
|
+
ctx.globalAlpha = clamp(grid.sessionSeparatorOpacity, 0, 1);
|
|
6508
|
+
ctx.strokeStyle = grid.sessionSeparatorColor;
|
|
6509
|
+
ctx.lineWidth = 1;
|
|
6510
|
+
for (let index = first; index <= endIndex; index += 1) {
|
|
6511
|
+
const point = data[index];
|
|
6512
|
+
const prev = data[index - 1];
|
|
6513
|
+
if (!point || !prev) continue;
|
|
6514
|
+
if (dayOf(point.time.getTime()) === dayOf(prev.time.getTime())) {
|
|
6515
|
+
continue;
|
|
5835
6516
|
}
|
|
5836
|
-
|
|
6517
|
+
const x = chartLeft + (index - xStart) / xSpan * chartWidth;
|
|
6518
|
+
ctx.beginPath();
|
|
6519
|
+
ctx.moveTo(crisp(x), crisp(chartTop));
|
|
6520
|
+
ctx.lineTo(crisp(x), crisp(fullChartBottom));
|
|
6521
|
+
ctx.stroke();
|
|
5837
6522
|
}
|
|
6523
|
+
ctx.restore();
|
|
5838
6524
|
}
|
|
5839
6525
|
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
5840
6526
|
const useSmoothedCandle = tickerOpts.smoothing && smoothedTickerPrice !== null;
|
|
@@ -6415,6 +7101,35 @@ function createChart(element, options = {}) {
|
|
|
6415
7101
|
ctx.textAlign = prevAlign;
|
|
6416
7102
|
ctx.textBaseline = prevBaseline;
|
|
6417
7103
|
}
|
|
7104
|
+
if (barMarks.length > 0 && data.length > 0) {
|
|
7105
|
+
const prevFont = ctx.font;
|
|
7106
|
+
const prevAlign = ctx.textAlign;
|
|
7107
|
+
const prevBaseline = ctx.textBaseline;
|
|
7108
|
+
ctx.textAlign = "center";
|
|
7109
|
+
ctx.textBaseline = "middle";
|
|
7110
|
+
for (const mark of barMarks) {
|
|
7111
|
+
const ms = typeof mark.time === "number" ? mark.time : Date.parse(String(mark.time));
|
|
7112
|
+
if (!Number.isFinite(ms)) continue;
|
|
7113
|
+
const index = findNearestIndexForTimeMs(ms);
|
|
7114
|
+
if (index === null) continue;
|
|
7115
|
+
const bar = data[index];
|
|
7116
|
+
if (!bar) continue;
|
|
7117
|
+
const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
7118
|
+
if (x < chartLeft - 20 || x > chartRight + 20) continue;
|
|
7119
|
+
const radius = Math.max(4, Number(mark.size) || 7);
|
|
7120
|
+
const above = mark.position === "above";
|
|
7121
|
+
const y = Number.isFinite(mark.price) ? yFromPrice(Number(mark.price)) : above ? yFromPrice(bar.h) - radius - 8 : yFromPrice(bar.l) + radius + 8;
|
|
7122
|
+
if (y < chartTop - radius || y > chartBottom + radius) continue;
|
|
7123
|
+
const key = mark.id ?? `bar:${index}:${mark.label ?? ""}`;
|
|
7124
|
+
const hovered = hoveredMarkKey === `bar|${key}`;
|
|
7125
|
+
const color = mark.color ?? "#f7a600";
|
|
7126
|
+
drawMarkBadge(x, y, hovered ? radius + 1.5 : radius, color, mark.shape ?? "circle", mark.label ?? "", mark.textColor ?? "#101114");
|
|
7127
|
+
markRegions.push({ kind: "bar", mark, id: key, x, y, radius: radius + 3 });
|
|
7128
|
+
}
|
|
7129
|
+
ctx.font = prevFont;
|
|
7130
|
+
ctx.textAlign = prevAlign;
|
|
7131
|
+
ctx.textBaseline = prevBaseline;
|
|
7132
|
+
}
|
|
6418
7133
|
ctx.restore();
|
|
6419
7134
|
const positionAxisGutter = Math.max(0, width - chartRight);
|
|
6420
7135
|
if (positionAxisGutter > 0) {
|
|
@@ -6925,9 +7640,79 @@ function createChart(element, options = {}) {
|
|
|
6925
7640
|
for (const priceLine of priceLines) {
|
|
6926
7641
|
drawPriceLine(priceLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
|
|
6927
7642
|
}
|
|
7643
|
+
for (const alert of alerts) {
|
|
7644
|
+
drawAlertLine(alert, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
|
|
7645
|
+
}
|
|
6928
7646
|
for (const orderLine of orderLines) {
|
|
6929
7647
|
drawOrderLine(orderLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
|
|
6930
7648
|
}
|
|
7649
|
+
let dataLineBottom = chartTop;
|
|
7650
|
+
const dataLine = mergedOptions.dataLine;
|
|
7651
|
+
if (dataLine?.visible && data.length > 0) {
|
|
7652
|
+
const hoverIndex = crosshairPoint && getHitRegion(crosshairPoint.x, crosshairPoint.y) === "plot" ? indexFromCanvasX(crosshairPoint.x) : null;
|
|
7653
|
+
const barIndex = hoverIndex !== null && hoverIndex >= 0 && hoverIndex < data.length ? hoverIndex : data.length - 1;
|
|
7654
|
+
const bar = data[barIndex];
|
|
7655
|
+
if (bar) {
|
|
7656
|
+
const prevFont = ctx.font;
|
|
7657
|
+
const fontSize = Math.max(9, dataLine.fontSize || axis.fontSize);
|
|
7658
|
+
ctx.font = `${fontSize}px ${mergedOptions.fontFamily}`;
|
|
7659
|
+
ctx.textBaseline = "top";
|
|
7660
|
+
ctx.textAlign = "left";
|
|
7661
|
+
const symbolColor = dataLine.symbolColor || axis.textColor;
|
|
7662
|
+
const textColor = dataLine.textColor || labels.indicatorTextColor;
|
|
7663
|
+
const changeAbs = bar.c - bar.o;
|
|
7664
|
+
const changeColor = changeAbs >= 0 ? mergedOptions.upColor : mergedOptions.downColor;
|
|
7665
|
+
const rowY = chartTop + 6;
|
|
7666
|
+
let cursorX = chartLeft + 10;
|
|
7667
|
+
if (dataLine.statusColor) {
|
|
7668
|
+
const dotRadius = Math.max(2.5, fontSize / 4);
|
|
7669
|
+
ctx.fillStyle = dataLine.statusColor;
|
|
7670
|
+
ctx.beginPath();
|
|
7671
|
+
ctx.arc(cursorX + dotRadius, rowY + fontSize / 2, dotRadius, 0, Math.PI * 2);
|
|
7672
|
+
ctx.fill();
|
|
7673
|
+
cursorX += dotRadius * 2 + 6;
|
|
7674
|
+
}
|
|
7675
|
+
const headline = [dataLine.symbol, dataLine.interval, dataLine.exchange].filter((part) => part && part.length > 0).join(" \xB7 ");
|
|
7676
|
+
if (headline) {
|
|
7677
|
+
ctx.fillStyle = symbolColor;
|
|
7678
|
+
ctx.fillText(headline, cursorX, rowY);
|
|
7679
|
+
cursorX += measureTextWidth(headline) + 10;
|
|
7680
|
+
}
|
|
7681
|
+
if (dataLine.showOhlc !== false) {
|
|
7682
|
+
const ohlc = `O${formatPrice(bar.o)} H${formatPrice(bar.h)} L${formatPrice(bar.l)} C${formatPrice(bar.c)}`;
|
|
7683
|
+
ctx.fillStyle = changeColor;
|
|
7684
|
+
ctx.fillText(ohlc, cursorX, rowY);
|
|
7685
|
+
cursorX += measureTextWidth(ohlc) + 8;
|
|
7686
|
+
}
|
|
7687
|
+
if (dataLine.showChange !== false) {
|
|
7688
|
+
const pct = bar.o !== 0 ? changeAbs / bar.o * 100 : 0;
|
|
7689
|
+
const changeText = `${changeAbs >= 0 ? "+" : ""}${formatPrice(changeAbs)} (${changeAbs >= 0 ? "+" : ""}${pct.toFixed(2)}%)`;
|
|
7690
|
+
ctx.fillStyle = changeColor;
|
|
7691
|
+
ctx.fillText(changeText, cursorX, rowY);
|
|
7692
|
+
cursorX += measureTextWidth(changeText) + 10;
|
|
7693
|
+
}
|
|
7694
|
+
if (dataLine.showVolume && bar.v !== void 0) {
|
|
7695
|
+
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))}`;
|
|
7696
|
+
ctx.fillStyle = textColor;
|
|
7697
|
+
ctx.fillText(volumeText, cursorX, rowY);
|
|
7698
|
+
cursorX += measureTextWidth(volumeText) + 10;
|
|
7699
|
+
}
|
|
7700
|
+
for (const detail of dataLine.details ?? []) {
|
|
7701
|
+
const text = detail.label ? `${detail.label} ${detail.value}` : detail.value;
|
|
7702
|
+
const width2 = measureTextWidth(text);
|
|
7703
|
+
if (cursorX + width2 > chartRight - 4) break;
|
|
7704
|
+
if (detail.background) {
|
|
7705
|
+
ctx.fillStyle = detail.background;
|
|
7706
|
+
fillRoundedRect(cursorX - 4, rowY - 2, width2 + 8, fontSize + 4, 3);
|
|
7707
|
+
}
|
|
7708
|
+
ctx.fillStyle = detail.color || textColor;
|
|
7709
|
+
ctx.fillText(text, cursorX, rowY);
|
|
7710
|
+
cursorX += width2 + 12;
|
|
7711
|
+
}
|
|
7712
|
+
dataLineBottom = rowY + fontSize + 4;
|
|
7713
|
+
ctx.font = prevFont;
|
|
7714
|
+
}
|
|
7715
|
+
}
|
|
6931
7716
|
if (labels.visible && (labels.showIndicatorNames || labels.showIndicatorValues)) {
|
|
6932
7717
|
const isLegendInputValue = (value) => {
|
|
6933
7718
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
@@ -6956,7 +7741,7 @@ function createChart(element, options = {}) {
|
|
|
6956
7741
|
const isRight = position === "top-right" || position === "bottom-right";
|
|
6957
7742
|
const isBottom = position === "bottom-left" || position === "bottom-right";
|
|
6958
7743
|
const legendX = isRight ? chartRight - offsetX : chartLeft + offsetX;
|
|
6959
|
-
const legendY = isBottom ? chartBottom - offsetY : chartTop + offsetY;
|
|
7744
|
+
const legendY = isBottom ? chartBottom - offsetY : Math.max(chartTop + offsetY, dataLineBottom);
|
|
6960
7745
|
drawText(legendText, legendX, legendY, isRight ? "right" : "left", isBottom ? "bottom" : "top", labels.indicatorTextColor);
|
|
6961
7746
|
ctx.font = prevFont;
|
|
6962
7747
|
}
|
|
@@ -6967,11 +7752,36 @@ function createChart(element, options = {}) {
|
|
|
6967
7752
|
for (const tick of timeTicks) {
|
|
6968
7753
|
const tickTime = getTimeForIndex(tick.index);
|
|
6969
7754
|
if (!tickTime) continue;
|
|
6970
|
-
const
|
|
7755
|
+
const tickMs = tickTime.getTime();
|
|
7756
|
+
const timeLabel = tick.weight >= 9 ? zoneClock.formatYear(tickMs) : tick.weight === 8 ? zoneClock.formatMonth(tickMs) : tick.weight === 7 ? zoneClock.formatDayMonth(tickMs) : formatClock(tickTime);
|
|
6971
7757
|
drawText(timeLabel, tick.x, (fullChartBottom + height) / 2, "center", "middle", xAxis.textColor);
|
|
6972
7758
|
}
|
|
6973
7759
|
ctx.font = prevFont;
|
|
6974
7760
|
}
|
|
7761
|
+
if (timescaleMarks.length > 0 && data.length > 0) {
|
|
7762
|
+
const radius = 6;
|
|
7763
|
+
const markY = fullChartBottom - radius - 2;
|
|
7764
|
+
for (const mark of timescaleMarks) {
|
|
7765
|
+
const ms = typeof mark.time === "number" ? mark.time : Date.parse(String(mark.time));
|
|
7766
|
+
if (!Number.isFinite(ms)) continue;
|
|
7767
|
+
const index = findNearestIndexForTimeMs(ms);
|
|
7768
|
+
if (index === null) continue;
|
|
7769
|
+
const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
7770
|
+
if (x < chartLeft - 10 || x > chartRight + 10) continue;
|
|
7771
|
+
const key = mark.id ?? `ts:${index}:${mark.label ?? ""}`;
|
|
7772
|
+
const hovered = hoveredMarkKey === `timescale|${key}`;
|
|
7773
|
+
drawMarkBadge(
|
|
7774
|
+
x,
|
|
7775
|
+
markY,
|
|
7776
|
+
hovered ? radius + 1.5 : radius,
|
|
7777
|
+
mark.color ?? "#5b8def",
|
|
7778
|
+
mark.shape ?? "circle",
|
|
7779
|
+
mark.label ?? "",
|
|
7780
|
+
mark.textColor ?? "#0b1220"
|
|
7781
|
+
);
|
|
7782
|
+
markRegions.push({ kind: "timescale", mark, id: key, x, y: markY, radius: radius + 3 });
|
|
7783
|
+
}
|
|
7784
|
+
}
|
|
6975
7785
|
if (labels.visible && labels.showCountdownToBarClose && lastPoint) {
|
|
6976
7786
|
const stepMs = getTimeStepMs();
|
|
6977
7787
|
const nowMs = Date.now() + (Number(mergedOptions.clockOffsetMs) || 0);
|
|
@@ -7497,27 +8307,24 @@ function createChart(element, options = {}) {
|
|
|
7497
8307
|
scheduleDraw();
|
|
7498
8308
|
};
|
|
7499
8309
|
const fitContent = () => {
|
|
7500
|
-
|
|
7501
|
-
updateFollowLatest(true);
|
|
7502
|
-
emitViewportChange();
|
|
7503
|
-
scheduleDraw();
|
|
8310
|
+
const target = getFitXTarget();
|
|
8311
|
+
animateViewportTo(target.center, target.span, () => updateFollowLatest(true));
|
|
7504
8312
|
};
|
|
7505
8313
|
const resetViewport = () => {
|
|
7506
8314
|
cancelKineticPan();
|
|
7507
|
-
|
|
8315
|
+
const target = getFitXTarget();
|
|
7508
8316
|
resetYViewport();
|
|
7509
|
-
updateFollowLatest(true);
|
|
7510
|
-
emitViewportChange();
|
|
7511
|
-
scheduleDraw();
|
|
8317
|
+
animateViewportTo(target.center, target.span, () => updateFollowLatest(true));
|
|
7512
8318
|
};
|
|
7513
8319
|
const isFollowingLatest = () => followLatest;
|
|
7514
8320
|
const setFollowingLatest = (follow) => {
|
|
7515
8321
|
if (follow && data.length > 0) {
|
|
7516
8322
|
cancelKineticPan();
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
8323
|
+
animateViewportTo(
|
|
8324
|
+
data.length - xSpan / 2 + rightEdgePaddingBars,
|
|
8325
|
+
xSpan,
|
|
8326
|
+
() => updateFollowLatest(true)
|
|
8327
|
+
);
|
|
7521
8328
|
} else {
|
|
7522
8329
|
updateFollowLatest(follow);
|
|
7523
8330
|
}
|
|
@@ -7600,6 +8407,22 @@ function createChart(element, options = {}) {
|
|
|
7600
8407
|
(region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
|
|
7601
8408
|
);
|
|
7602
8409
|
};
|
|
8410
|
+
const getAlertRegionAt = (x, y) => {
|
|
8411
|
+
const hits = alertRegions.filter(
|
|
8412
|
+
(region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
|
|
8413
|
+
);
|
|
8414
|
+
if (hits.length === 0) return null;
|
|
8415
|
+
return hits.find((region) => region.part === "remove") ?? hits[0];
|
|
8416
|
+
};
|
|
8417
|
+
const getMarkAt = (x, y) => {
|
|
8418
|
+
const tolerance = touchInputActive ? Math.max(1, mergedOptions.touch?.hitToleranceScale ?? 2.2) : 1;
|
|
8419
|
+
for (let index = markRegions.length - 1; index >= 0; index -= 1) {
|
|
8420
|
+
const region = markRegions[index];
|
|
8421
|
+
const reach = region.radius * tolerance;
|
|
8422
|
+
if (Math.abs(x - region.x) <= reach && Math.abs(y - region.y) <= reach) return region;
|
|
8423
|
+
}
|
|
8424
|
+
return null;
|
|
8425
|
+
};
|
|
7603
8426
|
const getCrosshairPriceActionRegion = (x, y) => {
|
|
7604
8427
|
if (!crosshairPriceActionRegion) {
|
|
7605
8428
|
return null;
|
|
@@ -8759,7 +9582,10 @@ function createChart(element, options = {}) {
|
|
|
8759
9582
|
pointerDownInfo = null;
|
|
8760
9583
|
orderDragState = null;
|
|
8761
9584
|
actionDragState = null;
|
|
8762
|
-
drawingDragState
|
|
9585
|
+
if (drawingDragState) {
|
|
9586
|
+
drawingDragState = null;
|
|
9587
|
+
commitHistory();
|
|
9588
|
+
}
|
|
8763
9589
|
canvas.style.cursor = "default";
|
|
8764
9590
|
setCrosshairPoint(null);
|
|
8765
9591
|
};
|
|
@@ -8800,6 +9626,7 @@ function createChart(element, options = {}) {
|
|
|
8800
9626
|
canvas.focus({ preventScroll: true });
|
|
8801
9627
|
}
|
|
8802
9628
|
cancelKineticPan();
|
|
9629
|
+
cancelViewportTween();
|
|
8803
9630
|
panVelocitySamples.length = 0;
|
|
8804
9631
|
magnetModifierActive = event.metaKey || event.ctrlKey;
|
|
8805
9632
|
shiftKeyActive = event.shiftKey;
|
|
@@ -8853,6 +9680,20 @@ function createChart(element, options = {}) {
|
|
|
8853
9680
|
});
|
|
8854
9681
|
return;
|
|
8855
9682
|
}
|
|
9683
|
+
const markHit = getMarkAt(point.x, point.y);
|
|
9684
|
+
if (markHit && markClickHandler) {
|
|
9685
|
+
const rect = canvas.getBoundingClientRect();
|
|
9686
|
+
setCrosshairPoint(null);
|
|
9687
|
+
markClickHandler({
|
|
9688
|
+
kind: markHit.kind,
|
|
9689
|
+
mark: markHit.mark,
|
|
9690
|
+
x: markHit.x,
|
|
9691
|
+
y: markHit.y,
|
|
9692
|
+
clientX: rect.left + markHit.x,
|
|
9693
|
+
clientY: rect.top + markHit.y
|
|
9694
|
+
});
|
|
9695
|
+
return;
|
|
9696
|
+
}
|
|
8856
9697
|
const orderRegion = getOrderActionRegion(point.x, point.y);
|
|
8857
9698
|
if (orderRegion) {
|
|
8858
9699
|
if (orderRegion.draggable) {
|
|
@@ -8891,6 +9732,28 @@ function createChart(element, options = {}) {
|
|
|
8891
9732
|
setCrosshairPoint(null);
|
|
8892
9733
|
return;
|
|
8893
9734
|
}
|
|
9735
|
+
const alertHit = getAlertRegionAt(point.x, point.y);
|
|
9736
|
+
if (alertHit) {
|
|
9737
|
+
const target = alerts.find((alert) => alert.id === alertHit.id);
|
|
9738
|
+
if (target) {
|
|
9739
|
+
setCrosshairPoint(null);
|
|
9740
|
+
if (alertHit.part === "remove") {
|
|
9741
|
+
const removed = serializeAlert(target);
|
|
9742
|
+
removeAlert(target.id);
|
|
9743
|
+
alertActionHandler?.({ alert: removed, action: "remove" });
|
|
9744
|
+
return;
|
|
9745
|
+
}
|
|
9746
|
+
if (target.draggable) {
|
|
9747
|
+
activePointerId = event.pointerId;
|
|
9748
|
+
alertDragState = { id: target.id, startPrice: target.price, lastPrice: target.price, moved: false };
|
|
9749
|
+
capturePointer(event.pointerId);
|
|
9750
|
+
canvas.style.cursor = "ns-resize";
|
|
9751
|
+
return;
|
|
9752
|
+
}
|
|
9753
|
+
alertActionHandler?.({ alert: serializeAlert(target), action: "click", price: target.price });
|
|
9754
|
+
return;
|
|
9755
|
+
}
|
|
9756
|
+
}
|
|
8894
9757
|
const paneButtonHit = getPaneButtonHit(point.x, point.y);
|
|
8895
9758
|
if (paneButtonHit) {
|
|
8896
9759
|
setCrosshairPoint(null);
|
|
@@ -9133,6 +9996,24 @@ function createChart(element, options = {}) {
|
|
|
9133
9996
|
setCrosshairPoint(null);
|
|
9134
9997
|
return;
|
|
9135
9998
|
}
|
|
9999
|
+
if (alertDragState) {
|
|
10000
|
+
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
10001
|
+
return;
|
|
10002
|
+
}
|
|
10003
|
+
const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
|
|
10004
|
+
if (nextPrice !== alertDragState.lastPrice) {
|
|
10005
|
+
alertDragState.lastPrice = nextPrice;
|
|
10006
|
+
alertDragState.moved = true;
|
|
10007
|
+
const target = alerts.find((alert) => alert.id === alertDragState?.id);
|
|
10008
|
+
if (target) {
|
|
10009
|
+
target.price = nextPrice;
|
|
10010
|
+
target.lastPrice = data[data.length - 1]?.c ?? null;
|
|
10011
|
+
alertActionHandler?.({ alert: serializeAlert(target), action: "move", price: nextPrice, dragging: true });
|
|
10012
|
+
}
|
|
10013
|
+
scheduleDraw();
|
|
10014
|
+
}
|
|
10015
|
+
return;
|
|
10016
|
+
}
|
|
9136
10017
|
if (orderDragState) {
|
|
9137
10018
|
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
9138
10019
|
return;
|
|
@@ -9199,12 +10080,49 @@ function createChart(element, options = {}) {
|
|
|
9199
10080
|
setCrosshairPoint(null);
|
|
9200
10081
|
return;
|
|
9201
10082
|
}
|
|
10083
|
+
const markHover = getMarkAt(point.x, point.y);
|
|
10084
|
+
const nextMarkKey = markHover ? `${markHover.kind}|${markHover.id}` : null;
|
|
10085
|
+
if (nextMarkKey !== hoveredMarkKey) {
|
|
10086
|
+
hoveredMarkKey = nextMarkKey;
|
|
10087
|
+
if (markHover) {
|
|
10088
|
+
const rect = canvas.getBoundingClientRect();
|
|
10089
|
+
markHoverHandler?.({
|
|
10090
|
+
kind: markHover.kind,
|
|
10091
|
+
mark: markHover.mark,
|
|
10092
|
+
x: markHover.x,
|
|
10093
|
+
y: markHover.y,
|
|
10094
|
+
clientX: rect.left + markHover.x,
|
|
10095
|
+
clientY: rect.top + markHover.y
|
|
10096
|
+
});
|
|
10097
|
+
} else {
|
|
10098
|
+
markHoverHandler?.(null);
|
|
10099
|
+
}
|
|
10100
|
+
scheduleDraw();
|
|
10101
|
+
}
|
|
10102
|
+
if (markHover) {
|
|
10103
|
+
canvas.title = markHover.mark.text ?? markHover.mark.label ?? "";
|
|
10104
|
+
canvas.style.cursor = "pointer";
|
|
10105
|
+
setCrosshairPoint(null);
|
|
10106
|
+
return;
|
|
10107
|
+
}
|
|
9202
10108
|
const orderDragRegion = getOrderDragRegion(point.x, point.y);
|
|
9203
10109
|
if (orderDragRegion) {
|
|
9204
10110
|
canvas.style.cursor = "ns-resize";
|
|
9205
10111
|
setCrosshairPoint(null);
|
|
9206
10112
|
return;
|
|
9207
10113
|
}
|
|
10114
|
+
const alertHover = getAlertRegionAt(point.x, point.y);
|
|
10115
|
+
const nextAlertId = alertHover?.id ?? null;
|
|
10116
|
+
if (nextAlertId !== hoveredAlertId) {
|
|
10117
|
+
hoveredAlertId = nextAlertId;
|
|
10118
|
+
scheduleDraw();
|
|
10119
|
+
}
|
|
10120
|
+
if (alertHover) {
|
|
10121
|
+
canvas.title = alertHover.part === "remove" ? "Remove alert" : "Drag to move alert";
|
|
10122
|
+
canvas.style.cursor = alertHover.part === "remove" ? "pointer" : "ns-resize";
|
|
10123
|
+
setCrosshairPoint(null);
|
|
10124
|
+
return;
|
|
10125
|
+
}
|
|
9208
10126
|
const hoveredPane = getPaneAt(point.x, point.y);
|
|
9209
10127
|
const nextHoveredPaneId = hoveredPane?.id ?? null;
|
|
9210
10128
|
if (nextHoveredPaneId !== hoveredPaneId) {
|
|
@@ -9362,6 +10280,21 @@ function createChart(element, options = {}) {
|
|
|
9362
10280
|
scheduleDraw();
|
|
9363
10281
|
return;
|
|
9364
10282
|
}
|
|
10283
|
+
if (alertDragState) {
|
|
10284
|
+
const target = alerts.find((alert) => alert.id === alertDragState?.id);
|
|
10285
|
+
if (target && alertDragState.moved) {
|
|
10286
|
+
alertActionHandler?.({
|
|
10287
|
+
alert: serializeAlert(target),
|
|
10288
|
+
action: "move",
|
|
10289
|
+
price: alertDragState.lastPrice,
|
|
10290
|
+
dragging: false
|
|
10291
|
+
});
|
|
10292
|
+
} else if (target) {
|
|
10293
|
+
alertActionHandler?.({ alert: serializeAlert(target), action: "click", price: target.price });
|
|
10294
|
+
}
|
|
10295
|
+
alertDragState = null;
|
|
10296
|
+
canvas.style.cursor = "default";
|
|
10297
|
+
}
|
|
9365
10298
|
if (orderDragState) {
|
|
9366
10299
|
const moved = orderDragState.lastPrice !== orderDragState.startPrice;
|
|
9367
10300
|
const finalLine = orderLines.find((line) => line.id === orderDragState?.orderId);
|
|
@@ -9408,9 +10341,14 @@ function createChart(element, options = {}) {
|
|
|
9408
10341
|
});
|
|
9409
10342
|
paneDividerDrag = null;
|
|
9410
10343
|
}
|
|
9411
|
-
if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null)) {
|
|
10344
|
+
if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null || hoveredAlertId !== null || hoveredMarkKey !== null)) {
|
|
9412
10345
|
hoveredPaneId = null;
|
|
9413
10346
|
hoveredPaneButton = null;
|
|
10347
|
+
hoveredAlertId = null;
|
|
10348
|
+
if (hoveredMarkKey !== null) {
|
|
10349
|
+
hoveredMarkKey = null;
|
|
10350
|
+
markHoverHandler?.(null);
|
|
10351
|
+
}
|
|
9414
10352
|
if (canvas.title) {
|
|
9415
10353
|
canvas.title = "";
|
|
9416
10354
|
}
|
|
@@ -9464,6 +10402,7 @@ function createChart(element, options = {}) {
|
|
|
9464
10402
|
return;
|
|
9465
10403
|
}
|
|
9466
10404
|
cancelKineticPan();
|
|
10405
|
+
cancelViewportTween();
|
|
9467
10406
|
const point = getCanvasPoint(event);
|
|
9468
10407
|
const region = getHitRegion(point.x, point.y);
|
|
9469
10408
|
if (region === "outside") {
|
|
@@ -9585,9 +10524,6 @@ function createChart(element, options = {}) {
|
|
|
9585
10524
|
if (keyboard.enabled === false) {
|
|
9586
10525
|
return;
|
|
9587
10526
|
}
|
|
9588
|
-
if (event.altKey || event.ctrlKey || event.metaKey) {
|
|
9589
|
-
return;
|
|
9590
|
-
}
|
|
9591
10527
|
const panStep = Math.max(1, keyboard.panBars ?? 1) * (event.shiftKey ? 10 : 1);
|
|
9592
10528
|
const emit = (action, drawing) => {
|
|
9593
10529
|
keyboardShortcutHandler?.({
|
|
@@ -9597,6 +10533,21 @@ function createChart(element, options = {}) {
|
|
|
9597
10533
|
...drawing ? { drawing } : {}
|
|
9598
10534
|
});
|
|
9599
10535
|
};
|
|
10536
|
+
if ((event.ctrlKey || event.metaKey) && !event.altKey) {
|
|
10537
|
+
const key = event.key.toLowerCase();
|
|
10538
|
+
if (keyboard.undoRedo !== false && (key === "z" || key === "y")) {
|
|
10539
|
+
const wantsRedo = key === "y" || event.shiftKey;
|
|
10540
|
+
const applied = wantsRedo ? redo() : undo();
|
|
10541
|
+
if (applied) {
|
|
10542
|
+
event.preventDefault();
|
|
10543
|
+
emit(wantsRedo ? "redo" : "undo");
|
|
10544
|
+
}
|
|
10545
|
+
return;
|
|
10546
|
+
}
|
|
10547
|
+
}
|
|
10548
|
+
if (event.altKey || event.ctrlKey || event.metaKey) {
|
|
10549
|
+
return;
|
|
10550
|
+
}
|
|
9600
10551
|
switch (event.key) {
|
|
9601
10552
|
case "Delete":
|
|
9602
10553
|
case "Backspace": {
|
|
@@ -9741,6 +10692,21 @@ function createChart(element, options = {}) {
|
|
|
9741
10692
|
resetRightMarginCache();
|
|
9742
10693
|
doubleClickEnabled = mergedOptions.doubleClickEnabled;
|
|
9743
10694
|
doubleClickAction = mergedOptions.doubleClickAction;
|
|
10695
|
+
setIndicatorLiveThrottleMs(mergedOptions.indicatorUpdate?.liveThrottleMs ?? 80);
|
|
10696
|
+
if (nextOptions.timezone !== void 0) {
|
|
10697
|
+
timezoneSetting = nextOptions.timezone || "local";
|
|
10698
|
+
zoneClock = createZoneClock(timezoneSetting);
|
|
10699
|
+
}
|
|
10700
|
+
if (nextOptions.timeFormat !== void 0) {
|
|
10701
|
+
timeFormat = nextOptions.timeFormat === "12h" ? "12h" : "24h";
|
|
10702
|
+
}
|
|
10703
|
+
if (nextOptions.session !== void 0) {
|
|
10704
|
+
sessionOptions = { ...sessionOptions, ...nextOptions.session ?? {} };
|
|
10705
|
+
compiledSession = compileSession(sessionOptions.spec);
|
|
10706
|
+
}
|
|
10707
|
+
if (nextOptions.alerts !== void 0) {
|
|
10708
|
+
setAlerts(nextOptions.alerts ?? []);
|
|
10709
|
+
}
|
|
9744
10710
|
applyAccessibilityAttributes();
|
|
9745
10711
|
const isTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
|
|
9746
10712
|
if (!isTickerSmoothingEnabled) {
|
|
@@ -9801,7 +10767,7 @@ function createChart(element, options = {}) {
|
|
|
9801
10767
|
resetRightMarginCache();
|
|
9802
10768
|
scheduleDraw();
|
|
9803
10769
|
};
|
|
9804
|
-
const
|
|
10770
|
+
const setDataInternal = (nextData) => {
|
|
9805
10771
|
const hadData = data.length > 0;
|
|
9806
10772
|
const previousCount = data.length;
|
|
9807
10773
|
const previousCenterRounded = hadData ? Math.round(xCenter) : 0;
|
|
@@ -9843,9 +10809,10 @@ function createChart(element, options = {}) {
|
|
|
9843
10809
|
if (lastPoint) {
|
|
9844
10810
|
pushSmoothedTicker(lastPoint.c, lastPoint.v);
|
|
9845
10811
|
}
|
|
10812
|
+
evaluateAlertsFromLatestBar();
|
|
9846
10813
|
scheduleDraw();
|
|
9847
10814
|
};
|
|
9848
|
-
const
|
|
10815
|
+
const upsertBarInternal = (point) => {
|
|
9849
10816
|
const time = new Date(point.t);
|
|
9850
10817
|
const timeMs = time.getTime();
|
|
9851
10818
|
const open = Number(point.o);
|
|
@@ -9866,7 +10833,7 @@ function createChart(element, options = {}) {
|
|
|
9866
10833
|
};
|
|
9867
10834
|
const last = data[data.length - 1];
|
|
9868
10835
|
if (!last) {
|
|
9869
|
-
|
|
10836
|
+
setDataInternal([point]);
|
|
9870
10837
|
return;
|
|
9871
10838
|
}
|
|
9872
10839
|
const lastMs = last.time.getTime();
|
|
@@ -9888,8 +10855,163 @@ function createChart(element, options = {}) {
|
|
|
9888
10855
|
reanchorDrawingsToData();
|
|
9889
10856
|
}
|
|
9890
10857
|
pushSmoothedTicker(parsed.c, parsed.v);
|
|
10858
|
+
evaluateAlerts(parsed.c, timeMs, data.length - 1);
|
|
9891
10859
|
scheduleDraw();
|
|
9892
10860
|
};
|
|
10861
|
+
const toRawPoint = (point) => ({
|
|
10862
|
+
t: point.time.toISOString(),
|
|
10863
|
+
o: point.o,
|
|
10864
|
+
h: point.h,
|
|
10865
|
+
l: point.l,
|
|
10866
|
+
c: point.c,
|
|
10867
|
+
...point.v === void 0 ? {} : { v: point.v }
|
|
10868
|
+
});
|
|
10869
|
+
const getReplayState = () => {
|
|
10870
|
+
if (!replay) {
|
|
10871
|
+
return {
|
|
10872
|
+
active: false,
|
|
10873
|
+
playing: false,
|
|
10874
|
+
speed: 1,
|
|
10875
|
+
index: Math.max(0, data.length - 1),
|
|
10876
|
+
total: data.length,
|
|
10877
|
+
atEnd: true,
|
|
10878
|
+
timeMs: data.length > 0 ? data[data.length - 1].time.getTime() : null
|
|
10879
|
+
};
|
|
10880
|
+
}
|
|
10881
|
+
const total = replay.fullRaw.length;
|
|
10882
|
+
const revealed = replay.fullRaw[replay.cursor];
|
|
10883
|
+
const revealedMs = revealed ? Date.parse(String(revealed.t)) : Number.NaN;
|
|
10884
|
+
return {
|
|
10885
|
+
active: true,
|
|
10886
|
+
playing: replay.playing,
|
|
10887
|
+
speed: replay.speed,
|
|
10888
|
+
index: replay.cursor,
|
|
10889
|
+
total,
|
|
10890
|
+
atEnd: replay.cursor >= total - 1,
|
|
10891
|
+
timeMs: Number.isFinite(revealedMs) ? revealedMs : null
|
|
10892
|
+
};
|
|
10893
|
+
};
|
|
10894
|
+
const emitReplayState = () => {
|
|
10895
|
+
replayStateHandler?.(getReplayState());
|
|
10896
|
+
};
|
|
10897
|
+
const applyReplaySlice = () => {
|
|
10898
|
+
if (!replay) return;
|
|
10899
|
+
setDataInternal(replay.fullRaw.slice(0, replay.cursor + 1));
|
|
10900
|
+
};
|
|
10901
|
+
const clearReplayTimer = () => {
|
|
10902
|
+
if (replay?.timerId) {
|
|
10903
|
+
clearTimeout(replay.timerId);
|
|
10904
|
+
replay.timerId = null;
|
|
10905
|
+
}
|
|
10906
|
+
};
|
|
10907
|
+
const scheduleReplayTick = () => {
|
|
10908
|
+
if (!replay || !replay.playing) return;
|
|
10909
|
+
clearReplayTimer();
|
|
10910
|
+
const intervalMs = Math.max(16, 1e3 / Math.max(0.05, replay.speed));
|
|
10911
|
+
replay.timerId = setTimeout(() => {
|
|
10912
|
+
if (!replay || !replay.playing) return;
|
|
10913
|
+
if (replay.cursor >= replay.fullRaw.length - 1) {
|
|
10914
|
+
replay.playing = false;
|
|
10915
|
+
clearReplayTimer();
|
|
10916
|
+
emitReplayState();
|
|
10917
|
+
return;
|
|
10918
|
+
}
|
|
10919
|
+
replay.cursor += 1;
|
|
10920
|
+
applyReplaySlice();
|
|
10921
|
+
emitReplayState();
|
|
10922
|
+
scheduleReplayTick();
|
|
10923
|
+
}, intervalMs);
|
|
10924
|
+
};
|
|
10925
|
+
const startReplay = (startOptions = {}) => {
|
|
10926
|
+
const source = replay ? replay.fullRaw : data.map((point) => toRawPoint(point));
|
|
10927
|
+
if (source.length === 0) return;
|
|
10928
|
+
clearReplayTimer();
|
|
10929
|
+
let cursor;
|
|
10930
|
+
if (Number.isFinite(startOptions.fromTimeMs)) {
|
|
10931
|
+
const target = Number(startOptions.fromTimeMs);
|
|
10932
|
+
const resolved = findNearestIndexForTimeMs(target);
|
|
10933
|
+
cursor = resolved ?? Math.floor(source.length * 0.7);
|
|
10934
|
+
} else if (Number.isFinite(startOptions.fromIndex)) {
|
|
10935
|
+
cursor = Math.floor(Number(startOptions.fromIndex));
|
|
10936
|
+
} else {
|
|
10937
|
+
cursor = Math.floor(source.length * 0.7);
|
|
10938
|
+
}
|
|
10939
|
+
cursor = clamp(cursor, 0, source.length - 1);
|
|
10940
|
+
const speed = Math.max(0.05, Number(startOptions.speed) || replay?.speed || 1);
|
|
10941
|
+
replay = { fullRaw: source, cursor, playing: false, speed, timerId: null };
|
|
10942
|
+
applyReplaySlice();
|
|
10943
|
+
setFollowingLatest(true);
|
|
10944
|
+
if (startOptions.play) {
|
|
10945
|
+
replay.playing = true;
|
|
10946
|
+
scheduleReplayTick();
|
|
10947
|
+
}
|
|
10948
|
+
emitReplayState();
|
|
10949
|
+
};
|
|
10950
|
+
const stopReplay = () => {
|
|
10951
|
+
if (!replay) return;
|
|
10952
|
+
clearReplayTimer();
|
|
10953
|
+
const full = replay.fullRaw;
|
|
10954
|
+
replay = null;
|
|
10955
|
+
setDataInternal(full);
|
|
10956
|
+
setFollowingLatest(true);
|
|
10957
|
+
emitReplayState();
|
|
10958
|
+
};
|
|
10959
|
+
const replayStep = (bars = 1) => {
|
|
10960
|
+
if (!replay) return;
|
|
10961
|
+
const step = Math.trunc(bars) || 1;
|
|
10962
|
+
const next = clamp(replay.cursor + step, 0, replay.fullRaw.length - 1);
|
|
10963
|
+
if (next === replay.cursor) return;
|
|
10964
|
+
replay.cursor = next;
|
|
10965
|
+
applyReplaySlice();
|
|
10966
|
+
emitReplayState();
|
|
10967
|
+
};
|
|
10968
|
+
const replayPlay = (speed) => {
|
|
10969
|
+
if (!replay) return;
|
|
10970
|
+
if (Number.isFinite(speed)) replay.speed = Math.max(0.05, Number(speed));
|
|
10971
|
+
if (replay.cursor >= replay.fullRaw.length - 1) return;
|
|
10972
|
+
replay.playing = true;
|
|
10973
|
+
scheduleReplayTick();
|
|
10974
|
+
emitReplayState();
|
|
10975
|
+
};
|
|
10976
|
+
const replayPause = () => {
|
|
10977
|
+
if (!replay || !replay.playing) return;
|
|
10978
|
+
replay.playing = false;
|
|
10979
|
+
clearReplayTimer();
|
|
10980
|
+
emitReplayState();
|
|
10981
|
+
};
|
|
10982
|
+
const setReplaySpeed = (speed) => {
|
|
10983
|
+
if (!replay || !Number.isFinite(speed)) return;
|
|
10984
|
+
replay.speed = clamp(Number(speed), 0.05, 100);
|
|
10985
|
+
if (replay.playing) scheduleReplayTick();
|
|
10986
|
+
emitReplayState();
|
|
10987
|
+
};
|
|
10988
|
+
const onReplayStateChange = (handler) => {
|
|
10989
|
+
replayStateHandler = handler;
|
|
10990
|
+
};
|
|
10991
|
+
const setData = (nextData) => {
|
|
10992
|
+
if (replay) {
|
|
10993
|
+
replay.fullRaw = nextData.slice();
|
|
10994
|
+
replay.cursor = clamp(replay.cursor, 0, Math.max(0, replay.fullRaw.length - 1));
|
|
10995
|
+
applyReplaySlice();
|
|
10996
|
+
emitReplayState();
|
|
10997
|
+
return;
|
|
10998
|
+
}
|
|
10999
|
+
setDataInternal(nextData);
|
|
11000
|
+
};
|
|
11001
|
+
const upsertBar = (point) => {
|
|
11002
|
+
if (replay) {
|
|
11003
|
+
const timeMs = Date.parse(String(point.t));
|
|
11004
|
+
if (!Number.isFinite(timeMs)) return;
|
|
11005
|
+
const buffer = replay.fullRaw;
|
|
11006
|
+
const lastRaw = buffer[buffer.length - 1];
|
|
11007
|
+
const lastMs = lastRaw ? Date.parse(String(lastRaw.t)) : Number.NaN;
|
|
11008
|
+
if (Number.isFinite(lastMs) && timeMs === lastMs) buffer[buffer.length - 1] = point;
|
|
11009
|
+
else if (!Number.isFinite(lastMs) || timeMs > lastMs) buffer.push(point);
|
|
11010
|
+
emitReplayState();
|
|
11011
|
+
return;
|
|
11012
|
+
}
|
|
11013
|
+
upsertBarInternal(point);
|
|
11014
|
+
};
|
|
9893
11015
|
const setPriceLines = (lines) => {
|
|
9894
11016
|
priceLines = lines.map((line, index) => ({
|
|
9895
11017
|
...line,
|
|
@@ -10078,6 +11200,96 @@ function createChart(element, options = {}) {
|
|
|
10078
11200
|
tradeMarkers = Array.isArray(markers) ? markers.slice() : [];
|
|
10079
11201
|
scheduleDraw();
|
|
10080
11202
|
};
|
|
11203
|
+
const setBarMarks = (marks) => {
|
|
11204
|
+
barMarks = Array.isArray(marks) ? marks.slice() : [];
|
|
11205
|
+
scheduleDraw();
|
|
11206
|
+
};
|
|
11207
|
+
const getBarMarks = () => barMarks.slice();
|
|
11208
|
+
const setTimescaleMarks = (marks) => {
|
|
11209
|
+
timescaleMarks = Array.isArray(marks) ? marks.slice() : [];
|
|
11210
|
+
scheduleDraw();
|
|
11211
|
+
};
|
|
11212
|
+
const getTimescaleMarks = () => timescaleMarks.slice();
|
|
11213
|
+
const onMarkClick = (handler) => {
|
|
11214
|
+
markClickHandler = handler;
|
|
11215
|
+
};
|
|
11216
|
+
const onMarkHover = (handler) => {
|
|
11217
|
+
markHoverHandler = handler;
|
|
11218
|
+
};
|
|
11219
|
+
const setAlerts = (nextAlerts) => {
|
|
11220
|
+
alerts = (Array.isArray(nextAlerts) ? nextAlerts : []).map((alert) => normalizeAlert(alert));
|
|
11221
|
+
const last = data[data.length - 1];
|
|
11222
|
+
if (last) {
|
|
11223
|
+
for (const alert of alerts) alert.lastPrice = last.c;
|
|
11224
|
+
}
|
|
11225
|
+
resetRightMarginCache();
|
|
11226
|
+
scheduleDraw();
|
|
11227
|
+
};
|
|
11228
|
+
const addAlert = (alert) => {
|
|
11229
|
+
const next = normalizeAlert(alert);
|
|
11230
|
+
const last = data[data.length - 1];
|
|
11231
|
+
if (last) next.lastPrice = last.c;
|
|
11232
|
+
alerts.push(next);
|
|
11233
|
+
resetRightMarginCache();
|
|
11234
|
+
scheduleDraw();
|
|
11235
|
+
return next.id;
|
|
11236
|
+
};
|
|
11237
|
+
const updateAlert = (id, patch) => {
|
|
11238
|
+
const target = alerts.find((alert) => alert.id === id);
|
|
11239
|
+
if (!target) return;
|
|
11240
|
+
if (Number.isFinite(patch.price)) target.price = Number(patch.price);
|
|
11241
|
+
if (patch.condition) target.condition = patch.condition;
|
|
11242
|
+
if (patch.label !== void 0) target.label = patch.label;
|
|
11243
|
+
if (patch.color !== void 0) target.color = patch.color;
|
|
11244
|
+
if (patch.active !== void 0) target.active = patch.active;
|
|
11245
|
+
if (patch.once !== void 0) target.once = patch.once;
|
|
11246
|
+
if (patch.draggable !== void 0) target.draggable = patch.draggable;
|
|
11247
|
+
if (patch.triggered !== void 0) {
|
|
11248
|
+
target.triggered = patch.triggered;
|
|
11249
|
+
if (!patch.triggered) target.lastPrice = data[data.length - 1]?.c ?? null;
|
|
11250
|
+
}
|
|
11251
|
+
resetRightMarginCache();
|
|
11252
|
+
scheduleDraw();
|
|
11253
|
+
};
|
|
11254
|
+
const removeAlert = (id) => {
|
|
11255
|
+
alerts = alerts.filter((alert) => alert.id !== id);
|
|
11256
|
+
if (hoveredAlertId === id) hoveredAlertId = null;
|
|
11257
|
+
resetRightMarginCache();
|
|
11258
|
+
scheduleDraw();
|
|
11259
|
+
};
|
|
11260
|
+
const getAlerts = () => alerts.map((alert) => serializeAlert(alert));
|
|
11261
|
+
const onAlertTrigger = (handler) => {
|
|
11262
|
+
alertTriggerHandler = handler;
|
|
11263
|
+
};
|
|
11264
|
+
const onAlertAction = (handler) => {
|
|
11265
|
+
alertActionHandler = handler;
|
|
11266
|
+
};
|
|
11267
|
+
const setTimezone = (timezone) => {
|
|
11268
|
+
timezoneSetting = timezone || "local";
|
|
11269
|
+
zoneClock = createZoneClock(timezoneSetting);
|
|
11270
|
+
scheduleDraw();
|
|
11271
|
+
};
|
|
11272
|
+
const getTimezone = () => timezoneSetting;
|
|
11273
|
+
const setTimeFormat = (format) => {
|
|
11274
|
+
timeFormat = format === "12h" ? "12h" : "24h";
|
|
11275
|
+
resetRightMarginCache();
|
|
11276
|
+
scheduleDraw();
|
|
11277
|
+
};
|
|
11278
|
+
const getTimeFormat = () => timeFormat;
|
|
11279
|
+
const setSession = (session) => {
|
|
11280
|
+
if (session === null) {
|
|
11281
|
+
sessionOptions = { ...sessionOptions, spec: null };
|
|
11282
|
+
} else if (typeof session === "string") {
|
|
11283
|
+
sessionOptions = { ...sessionOptions, spec: session };
|
|
11284
|
+
} else if ("segments" in session || "timezone" in session || "days" in session) {
|
|
11285
|
+
sessionOptions = { ...sessionOptions, spec: session };
|
|
11286
|
+
} else {
|
|
11287
|
+
sessionOptions = { ...sessionOptions, ...session };
|
|
11288
|
+
}
|
|
11289
|
+
compiledSession = compileSession(sessionOptions.spec);
|
|
11290
|
+
scheduleDraw();
|
|
11291
|
+
};
|
|
11292
|
+
const getSession = () => ({ ...sessionOptions });
|
|
10081
11293
|
const setMagnetMode = (mode) => {
|
|
10082
11294
|
magnetMode = mode;
|
|
10083
11295
|
};
|
|
@@ -10097,7 +11309,13 @@ function createChart(element, options = {}) {
|
|
|
10097
11309
|
drawings = dedupeDrawingIds(nextDrawings.map((drawing) => normalizeDrawingState(drawing)));
|
|
10098
11310
|
draftDrawing = null;
|
|
10099
11311
|
reanchorDrawingsToData();
|
|
10100
|
-
|
|
11312
|
+
historySuspended += 1;
|
|
11313
|
+
try {
|
|
11314
|
+
emitDrawingsChange();
|
|
11315
|
+
} finally {
|
|
11316
|
+
historySuspended -= 1;
|
|
11317
|
+
}
|
|
11318
|
+
resetHistoryBaseline();
|
|
10101
11319
|
scheduleDraw();
|
|
10102
11320
|
};
|
|
10103
11321
|
const addDrawing = (drawing) => {
|
|
@@ -10182,7 +11400,10 @@ function createChart(element, options = {}) {
|
|
|
10182
11400
|
indicators: getIndicators(),
|
|
10183
11401
|
magnetMode: getMagnetMode(),
|
|
10184
11402
|
priceScale: getPriceScale(),
|
|
10185
|
-
drawingDefaults
|
|
11403
|
+
drawingDefaults,
|
|
11404
|
+
alerts: getAlerts(),
|
|
11405
|
+
timezone: getTimezone(),
|
|
11406
|
+
timeFormat: getTimeFormat()
|
|
10186
11407
|
};
|
|
10187
11408
|
};
|
|
10188
11409
|
const loadState = (state) => {
|
|
@@ -10211,15 +11432,28 @@ function createChart(element, options = {}) {
|
|
|
10211
11432
|
}
|
|
10212
11433
|
}
|
|
10213
11434
|
}
|
|
11435
|
+
if (Array.isArray(state.alerts)) {
|
|
11436
|
+
setAlerts(state.alerts);
|
|
11437
|
+
}
|
|
11438
|
+
if (typeof state.timezone === "string") {
|
|
11439
|
+
setTimezone(state.timezone);
|
|
11440
|
+
}
|
|
11441
|
+
if (state.timeFormat === "12h" || state.timeFormat === "24h") {
|
|
11442
|
+
setTimeFormat(state.timeFormat);
|
|
11443
|
+
}
|
|
10214
11444
|
if (state.viewport && typeof state.viewport === "object") {
|
|
10215
11445
|
setViewport(state.viewport);
|
|
10216
11446
|
}
|
|
11447
|
+
clearHistory();
|
|
10217
11448
|
};
|
|
10218
11449
|
const takeScreenshot = (options2 = {}) => {
|
|
10219
11450
|
return canvas.toDataURL(options2.type ?? "image/png", options2.quality);
|
|
10220
11451
|
};
|
|
10221
11452
|
const destroy = () => {
|
|
11453
|
+
cancelViewportTween();
|
|
10222
11454
|
cancelLongPressTimer();
|
|
11455
|
+
clearReplayTimer();
|
|
11456
|
+
replay = null;
|
|
10223
11457
|
datafeedUnsubscribe?.();
|
|
10224
11458
|
datafeedUnsubscribe = null;
|
|
10225
11459
|
datafeed = null;
|
|
@@ -10253,6 +11487,10 @@ function createChart(element, options = {}) {
|
|
|
10253
11487
|
}
|
|
10254
11488
|
element.innerHTML = "";
|
|
10255
11489
|
};
|
|
11490
|
+
if (Array.isArray(options.alerts) && options.alerts.length > 0) {
|
|
11491
|
+
setAlerts(options.alerts);
|
|
11492
|
+
}
|
|
11493
|
+
resetHistoryBaseline();
|
|
10256
11494
|
draw();
|
|
10257
11495
|
armCountdownHeartbeat();
|
|
10258
11496
|
return {
|
|
@@ -10336,6 +11574,42 @@ function createChart(element, options = {}) {
|
|
|
10336
11574
|
saveState,
|
|
10337
11575
|
loadState,
|
|
10338
11576
|
takeScreenshot,
|
|
11577
|
+
setTimezone,
|
|
11578
|
+
getTimezone,
|
|
11579
|
+
setTimeFormat,
|
|
11580
|
+
getTimeFormat,
|
|
11581
|
+
setSession,
|
|
11582
|
+
getSession,
|
|
11583
|
+
undo,
|
|
11584
|
+
redo,
|
|
11585
|
+
canUndo: () => historyPast.length > 0,
|
|
11586
|
+
canRedo: () => historyFuture.length > 0,
|
|
11587
|
+
getUndoRedoState,
|
|
11588
|
+
onUndoRedoStateChange: (handler) => {
|
|
11589
|
+
undoRedoStateHandler = handler;
|
|
11590
|
+
},
|
|
11591
|
+
clearHistory,
|
|
11592
|
+
startReplay,
|
|
11593
|
+
stopReplay,
|
|
11594
|
+
replayStep,
|
|
11595
|
+
replayPlay,
|
|
11596
|
+
replayPause,
|
|
11597
|
+
setReplaySpeed,
|
|
11598
|
+
getReplayState,
|
|
11599
|
+
onReplayStateChange,
|
|
11600
|
+
setAlerts,
|
|
11601
|
+
addAlert,
|
|
11602
|
+
updateAlert,
|
|
11603
|
+
removeAlert,
|
|
11604
|
+
getAlerts,
|
|
11605
|
+
onAlertTrigger,
|
|
11606
|
+
onAlertAction,
|
|
11607
|
+
setBarMarks,
|
|
11608
|
+
getBarMarks,
|
|
11609
|
+
setTimescaleMarks,
|
|
11610
|
+
getTimescaleMarks,
|
|
11611
|
+
onMarkClick,
|
|
11612
|
+
onMarkHover,
|
|
10339
11613
|
resize,
|
|
10340
11614
|
destroy
|
|
10341
11615
|
};
|
|
@@ -10347,8 +11621,10 @@ function createChart(element, options = {}) {
|
|
|
10347
11621
|
FIB_DEFAULT_PALETTE,
|
|
10348
11622
|
POSITION_DEFAULT_COLORS,
|
|
10349
11623
|
SCRIPT_SOURCE_INPUT_VALUES,
|
|
11624
|
+
SESSION_PRESETS,
|
|
10350
11625
|
compileScriptIndicator,
|
|
10351
11626
|
createChart,
|
|
10352
11627
|
extractScriptInputs,
|
|
11628
|
+
isValidTimeZone,
|
|
10353
11629
|
validateScriptSource
|
|
10354
11630
|
});
|