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.
@@ -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
- if (existing && existing.fingerprint === fingerprint) {
373
- return existing.values;
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
- if (existing && existing.fingerprint === fingerprint) {
385
- builtInComputationCache.delete(key);
386
- builtInComputationCache.set(key, existing);
387
- return existing.value;
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
- const fitXViewport = () => {
3521
- const count = data.length;
3522
- if (count === 0) {
3523
- xCenter = 0;
3524
- xSpan = 60;
3525
- return;
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 requestedVisibleBars = clamp(Math.floor(mergedOptions.initialVisibleBars), minVisibleBars, maxSpan);
3529
- xSpan = requestedVisibleBars;
3530
- if (mergedOptions.initialViewport === "center") {
3531
- xCenter = count / 2;
3532
- } else {
3533
- xCenter = count - xSpan / 2 + rightEdgePaddingBars;
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
- if (mode === "time") {
4047
- return time.toLocaleTimeString(void 0, {
4048
- hour: "2-digit",
4049
- minute: "2-digit",
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
- return time.toLocaleTimeString(void 0, {
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
- cachedRightMargin = Math.max(cachedRightMargin, nextRightMargin);
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
- if (prevTime.getFullYear() !== time.getFullYear()) weight = 9;
5763
- else if (prevTime.getMonth() !== time.getMonth()) weight = 8;
5764
- else if (prevTime.getDate() !== time.getDate()) weight = 7;
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 = prevTime.getHours() * 60 + prevTime.getMinutes();
5767
- const curMinutes = time.getHours() * 60 + time.getMinutes();
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
- if (grid.sessionSeparators && data.length > 1) {
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 barDeltaMs = data.length > 1 ? Math.abs(data[Math.min(first, data.length - 1)].time.getTime() - data[Math.min(first, data.length - 1) - 1].time.getTime()) : 0;
5816
- if (barDeltaMs > 0 && barDeltaMs < 0.9 * 864e5) {
5817
- const SESSION_ANCHOR_MS = 22 * 36e5;
5818
- const sessionDayOf = (ms) => Math.floor((ms - SESSION_ANCHOR_MS) / 864e5);
5819
- ctx.save();
5820
- ctx.globalAlpha = clamp(grid.sessionSeparatorOpacity, 0, 1);
5821
- ctx.strokeStyle = grid.sessionSeparatorColor;
5822
- ctx.lineWidth = 1;
5823
- for (let index = first; index <= endIndex; index += 1) {
5824
- const point = data[index];
5825
- const prev = data[index - 1];
5826
- if (!point || !prev) continue;
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
- ctx.restore();
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 timeLabel = tick.weight >= 9 ? String(tickTime.getFullYear()) : tick.weight === 8 ? tickTime.toLocaleDateString(void 0, { month: "short" }) : tick.weight === 7 ? tickTime.toLocaleDateString(void 0, { month: "short", day: "numeric" }) : tickTime.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit", hour12: false });
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
- fitXViewport();
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
- fitXViewport();
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
- xCenter = data.length - xSpan / 2 + rightEdgePaddingBars;
7518
- clampXViewport();
7519
- updateFollowLatest(true);
7520
- scheduleDraw();
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 = null;
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 setData = (nextData) => {
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 upsertBar = (point) => {
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
- setData([point]);
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
- emitDrawingsChange();
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
  });