hyperprop-charting-library 0.1.143 → 0.1.145

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.
@@ -2743,6 +2743,237 @@ var CHART_THEMES = {
2743
2743
  var CHART_THEME_NAMES = Object.keys(CHART_THEMES);
2744
2744
  var resolveTheme = (theme) => typeof theme === "string" ? CHART_THEMES[theme] ?? CHART_THEMES.midnight : theme;
2745
2745
 
2746
+ // src/time.ts
2747
+ var DAY_MS = 864e5;
2748
+ var HOUR_MS = 36e5;
2749
+ var OFFSET_FORMATTER_CACHE = /* @__PURE__ */ new Map();
2750
+ var LABEL_FORMATTER_CACHE = /* @__PURE__ */ new Map();
2751
+ var getOffsetFormatter = (timeZone) => {
2752
+ let formatter = OFFSET_FORMATTER_CACHE.get(timeZone);
2753
+ if (!formatter) {
2754
+ formatter = new Intl.DateTimeFormat("en-US", {
2755
+ timeZone,
2756
+ hourCycle: "h23",
2757
+ year: "numeric",
2758
+ month: "2-digit",
2759
+ day: "2-digit",
2760
+ hour: "2-digit",
2761
+ minute: "2-digit",
2762
+ second: "2-digit"
2763
+ });
2764
+ OFFSET_FORMATTER_CACHE.set(timeZone, formatter);
2765
+ }
2766
+ return formatter;
2767
+ };
2768
+ var getLabelFormatter = (timeZone, options, key) => {
2769
+ const cacheKey = `${timeZone ?? "local"}|${key}`;
2770
+ let formatter = LABEL_FORMATTER_CACHE.get(cacheKey);
2771
+ if (!formatter) {
2772
+ formatter = new Intl.DateTimeFormat(void 0, timeZone ? { ...options, timeZone } : options);
2773
+ LABEL_FORMATTER_CACHE.set(cacheKey, formatter);
2774
+ }
2775
+ return formatter;
2776
+ };
2777
+ var isValidTimeZone = (timeZone) => {
2778
+ try {
2779
+ new Intl.DateTimeFormat("en-US", { timeZone });
2780
+ return true;
2781
+ } catch {
2782
+ return false;
2783
+ }
2784
+ };
2785
+ var floorDiv = (value, divisor) => Math.floor(value / divisor);
2786
+ var positiveMod = (value, divisor) => (value % divisor + divisor) % divisor;
2787
+ var computeOffsetMs = (timeZone, ms) => {
2788
+ const parts = getOffsetFormatter(timeZone).formatToParts(new Date(ms));
2789
+ let year = 0;
2790
+ let month = 1;
2791
+ let day = 1;
2792
+ let hour = 0;
2793
+ let minute = 0;
2794
+ let second = 0;
2795
+ for (const part of parts) {
2796
+ const value = Number(part.value);
2797
+ if (part.type === "year") year = value;
2798
+ else if (part.type === "month") month = value;
2799
+ else if (part.type === "day") day = value;
2800
+ else if (part.type === "hour") hour = value === 24 ? 0 : value;
2801
+ else if (part.type === "minute") minute = value;
2802
+ else if (part.type === "second") second = value;
2803
+ }
2804
+ const asUtc = Date.UTC(year, month - 1, day, hour, minute, second);
2805
+ return asUtc - Math.floor(ms / 1e3) * 1e3;
2806
+ };
2807
+ var createZoneClock = (timeZone) => {
2808
+ const normalized = !timeZone || timeZone === "local" ? null : timeZone.toLowerCase() === "utc" ? "UTC" : isValidTimeZone(timeZone) ? timeZone : null;
2809
+ if (normalized === null) {
2810
+ const scratch = /* @__PURE__ */ new Date();
2811
+ const partsOf2 = (ms) => {
2812
+ scratch.setTime(ms);
2813
+ return {
2814
+ year: scratch.getFullYear(),
2815
+ month: scratch.getMonth() + 1,
2816
+ day: scratch.getDate(),
2817
+ hour: scratch.getHours(),
2818
+ minute: scratch.getMinutes()
2819
+ };
2820
+ };
2821
+ return {
2822
+ timeZone: null,
2823
+ offsetMs: (ms) => {
2824
+ scratch.setTime(ms);
2825
+ return -scratch.getTimezoneOffset() * 6e4;
2826
+ },
2827
+ dayKey: (ms) => {
2828
+ scratch.setTime(ms);
2829
+ return floorDiv(Date.UTC(scratch.getFullYear(), scratch.getMonth(), scratch.getDate()), DAY_MS);
2830
+ },
2831
+ minutesOfDay: (ms) => {
2832
+ scratch.setTime(ms);
2833
+ return scratch.getHours() * 60 + scratch.getMinutes();
2834
+ },
2835
+ weekday: (ms) => {
2836
+ scratch.setTime(ms);
2837
+ return scratch.getDay();
2838
+ },
2839
+ parts: partsOf2,
2840
+ formatTime: (ms, hour12) => formatClockTime(partsOf2(ms), hour12),
2841
+ formatDayMonth: (ms) => getLabelFormatter(null, { month: "short", day: "numeric" }, "md").format(ms),
2842
+ formatMonth: (ms) => getLabelFormatter(null, { month: "short" }, "m").format(ms),
2843
+ formatYear: (ms) => String(partsOf2(ms).year)
2844
+ };
2845
+ }
2846
+ const zone = normalized;
2847
+ const offsetCache = /* @__PURE__ */ new Map();
2848
+ const offsetMs = (ms) => {
2849
+ const bucket = floorDiv(ms, HOUR_MS);
2850
+ const cached = offsetCache.get(bucket);
2851
+ if (cached !== void 0) return cached;
2852
+ const offset = computeOffsetMs(zone, ms);
2853
+ if (offsetCache.size > 2e4) offsetCache.clear();
2854
+ offsetCache.set(bucket, offset);
2855
+ return offset;
2856
+ };
2857
+ const localMs = (ms) => ms + offsetMs(ms);
2858
+ const partsOf = (ms) => {
2859
+ const local = new Date(localMs(ms));
2860
+ return {
2861
+ year: local.getUTCFullYear(),
2862
+ month: local.getUTCMonth() + 1,
2863
+ day: local.getUTCDate(),
2864
+ hour: local.getUTCHours(),
2865
+ minute: local.getUTCMinutes()
2866
+ };
2867
+ };
2868
+ return {
2869
+ timeZone: zone,
2870
+ offsetMs,
2871
+ dayKey: (ms) => floorDiv(localMs(ms), DAY_MS),
2872
+ minutesOfDay: (ms) => floorDiv(positiveMod(localMs(ms), DAY_MS), 6e4),
2873
+ // 1970-01-01 was a Thursday (index 4).
2874
+ weekday: (ms) => positiveMod(floorDiv(localMs(ms), DAY_MS) + 4, 7),
2875
+ parts: partsOf,
2876
+ formatTime: (ms, hour12) => formatClockTime(partsOf(ms), hour12),
2877
+ formatDayMonth: (ms) => getLabelFormatter(zone, { month: "short", day: "numeric" }, "md").format(ms),
2878
+ formatMonth: (ms) => getLabelFormatter(zone, { month: "short" }, "m").format(ms),
2879
+ formatYear: (ms) => String(partsOf(ms).year)
2880
+ };
2881
+ };
2882
+ var pad2 = (value) => value < 10 ? `0${value}` : String(value);
2883
+ var formatClockTime = (parts, hour12) => {
2884
+ if (!hour12) return `${pad2(parts.hour)}:${pad2(parts.minute)}`;
2885
+ const suffix = parts.hour < 12 ? "AM" : "PM";
2886
+ const hour = parts.hour % 12 === 0 ? 12 : parts.hour % 12;
2887
+ return `${hour}:${pad2(parts.minute)} ${suffix}`;
2888
+ };
2889
+ var parseHhMm = (value) => {
2890
+ const match = /^(\d{1,2}):?(\d{2})$/.exec(value.trim());
2891
+ if (!match) return null;
2892
+ const hour = Number(match[1]);
2893
+ const minute = Number(match[2]);
2894
+ if (!Number.isFinite(hour) || !Number.isFinite(minute)) return null;
2895
+ if (hour < 0 || hour > 24 || minute < 0 || minute > 59) return null;
2896
+ return hour * 60 + minute;
2897
+ };
2898
+ var SESSION_PRESETS = {
2899
+ "cme-futures": {
2900
+ timezone: "America/New_York",
2901
+ // Opening days: the session opens Sunday–Thursday evening and runs into
2902
+ // the following morning, so Friday evening (weekend) is correctly closed.
2903
+ days: [0, 1, 2, 3, 4],
2904
+ segments: [{ start: "18:00", end: "17:00" }]
2905
+ },
2906
+ "cme-rth": {
2907
+ timezone: "America/New_York",
2908
+ days: [1, 2, 3, 4, 5],
2909
+ segments: [{ start: "09:30", end: "16:00" }]
2910
+ },
2911
+ "us-equities": {
2912
+ timezone: "America/New_York",
2913
+ days: [1, 2, 3, 4, 5],
2914
+ segments: [{ start: "09:30", end: "16:00" }]
2915
+ },
2916
+ "us-equities-extended": {
2917
+ timezone: "America/New_York",
2918
+ days: [1, 2, 3, 4, 5],
2919
+ segments: [{ start: "04:00", end: "20:00" }]
2920
+ },
2921
+ "24x7": {
2922
+ days: [0, 1, 2, 3, 4, 5, 6],
2923
+ segments: [{ start: "00:00", end: "24:00" }]
2924
+ }
2925
+ };
2926
+ var resolveSessionSpec = (spec) => {
2927
+ if (!spec) return null;
2928
+ if (typeof spec === "string") return SESSION_PRESETS[spec] ?? null;
2929
+ return spec;
2930
+ };
2931
+ var compileSession = (spec) => {
2932
+ const resolved = resolveSessionSpec(spec);
2933
+ if (!resolved) return null;
2934
+ const rawSegments = resolved.segments?.length ? resolved.segments : [{ start: "00:00", end: "24:00" }];
2935
+ const segments = [];
2936
+ for (const segment of rawSegments) {
2937
+ const startMinutes = parseHhMm(segment.start);
2938
+ const endMinutesRaw = parseHhMm(segment.end);
2939
+ if (startMinutes === null || endMinutesRaw === null) continue;
2940
+ const endMinutes = endMinutesRaw === 0 ? 1440 : endMinutesRaw;
2941
+ segments.push({ startMinutes, endMinutes, wraps: endMinutes <= startMinutes });
2942
+ }
2943
+ if (segments.length === 0) return null;
2944
+ return {
2945
+ clock: createZoneClock(resolved.timezone ?? null),
2946
+ days: new Set(resolved.days ?? [1, 2, 3, 4, 5]),
2947
+ segments,
2948
+ overnight: segments.some((segment) => segment.wraps)
2949
+ };
2950
+ };
2951
+ var sessionInfoAt = (session, ms) => {
2952
+ const { clock } = session;
2953
+ const dayKey = clock.dayKey(ms);
2954
+ const minutes = clock.minutesOfDay(ms);
2955
+ const weekday = clock.weekday(ms);
2956
+ let sessionDay = dayKey;
2957
+ let inSession = false;
2958
+ for (const segment of session.segments) {
2959
+ if (segment.wraps) {
2960
+ if (minutes >= segment.startMinutes) {
2961
+ if (session.days.has(weekday)) {
2962
+ inSession = true;
2963
+ sessionDay = dayKey + 1;
2964
+ }
2965
+ } else if (minutes < segment.endMinutes) {
2966
+ const openedWeekday = positiveMod(weekday - 1, 7);
2967
+ if (session.days.has(openedWeekday)) inSession = true;
2968
+ sessionDay = dayKey;
2969
+ }
2970
+ } else if (minutes >= segment.startMinutes && minutes < segment.endMinutes && session.days.has(weekday)) {
2971
+ inSession = true;
2972
+ }
2973
+ }
2974
+ return { inSession, sessionDay };
2975
+ };
2976
+
2746
2977
  // src/options.ts
2747
2978
  var DEFAULT_GRID_OPTIONS = {
2748
2979
  color: "#2b2f38",
@@ -2960,9 +3191,19 @@ var DEFAULT_OPTIONS = {
2960
3191
  datafeedOptions: { prefetchThresholdBars: 150, cooldownMs: 1200, chunkBars: 1500 },
2961
3192
  animation: { viewportTransitions: true, durationMs: 260 },
2962
3193
  indicatorUpdate: { liveThrottleMs: 80 },
3194
+ timezone: "local",
3195
+ timeFormat: "24h",
3196
+ session: {
3197
+ spec: null,
3198
+ separators: true,
3199
+ highlightOutOfSession: false,
3200
+ outOfSessionColor: "rgba(120,132,158,0.07)"
3201
+ },
3202
+ history: { enabled: true, limit: 100 },
2963
3203
  crosshair: DEFAULT_CROSSHAIR_OPTIONS,
2964
3204
  grid: DEFAULT_GRID_OPTIONS,
2965
3205
  watermark: DEFAULT_WATERMARK_OPTIONS,
3206
+ alerts: [],
2966
3207
  priceLines: [],
2967
3208
  orderLines: [],
2968
3209
  tickerLine: {
@@ -3030,6 +3271,14 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
3030
3271
  ...baseOptions.indicatorUpdate,
3031
3272
  ...options.indicatorUpdate ?? {}
3032
3273
  },
3274
+ session: {
3275
+ ...baseOptions.session,
3276
+ ...options.session ?? {}
3277
+ },
3278
+ history: {
3279
+ ...baseOptions.history,
3280
+ ...options.history ?? {}
3281
+ },
3033
3282
  datafeedOptions: {
3034
3283
  ...baseOptions.datafeedOptions,
3035
3284
  ...options.datafeedOptions ?? {}
@@ -3193,6 +3442,45 @@ function createChart(element, options = {}) {
3193
3442
  ...drawing.label === void 0 ? {} : { label: drawing.label }
3194
3443
  });
3195
3444
  let tradeMarkers = [];
3445
+ let timezoneSetting = mergedOptions.timezone ?? "local";
3446
+ let zoneClock = createZoneClock(timezoneSetting);
3447
+ let timeFormat = mergedOptions.timeFormat === "12h" ? "12h" : "24h";
3448
+ const DEFAULT_SESSION_OPTIONS = {
3449
+ spec: null,
3450
+ separators: true,
3451
+ highlightOutOfSession: false,
3452
+ outOfSessionColor: "rgba(120,132,158,0.07)"
3453
+ };
3454
+ let sessionOptions = {
3455
+ ...DEFAULT_SESSION_OPTIONS,
3456
+ ...mergedOptions.session ?? {}
3457
+ };
3458
+ let compiledSession = compileSession(sessionOptions.spec);
3459
+ const isHour12 = () => timeFormat === "12h";
3460
+ const formatClock = (time) => zoneClock.formatTime(time.getTime(), isHour12());
3461
+ let barMarks = [];
3462
+ let timescaleMarks = [];
3463
+ let markRegions = [];
3464
+ let hoveredMarkKey = null;
3465
+ let markClickHandler = null;
3466
+ let markHoverHandler = null;
3467
+ let alerts = [];
3468
+ let generatedAlertId = 1;
3469
+ let alertTriggerHandler = null;
3470
+ let alertActionHandler = null;
3471
+ let alertRegions = [];
3472
+ let alertDragState = null;
3473
+ let hoveredAlertId = null;
3474
+ let replay = null;
3475
+ let replayStateHandler = null;
3476
+ const historyEnabled = mergedOptions.history?.enabled !== false;
3477
+ const historyLimit = Math.max(1, Math.floor(Number(mergedOptions.history?.limit ?? 100)) || 100);
3478
+ let historyPast = [];
3479
+ let historyFuture = [];
3480
+ let historyBaseline = "";
3481
+ let historySuspended = 0;
3482
+ let undoRedoStateHandler = null;
3483
+ let lastUndoRedoSignature = "";
3196
3484
  let indicators = (options.indicators ?? []).map((indicator) => normalizeIndicatorState(indicator));
3197
3485
  let drawings = dedupeDrawingIds(
3198
3486
  (options.drawings ?? []).map((drawing) => normalizeDrawingState(drawing))
@@ -3241,9 +3529,168 @@ function createChart(element, options = {}) {
3241
3529
  let selectedDrawingId = null;
3242
3530
  const drawingToolDefaults = /* @__PURE__ */ new Map();
3243
3531
  const getDrawingToolDefaults = (tool) => drawingToolDefaults.get(tool) ?? {};
3532
+ const historyDoc = () => JSON.stringify(drawings.map((drawing) => serializeDrawing(drawing)));
3533
+ const describeHistoryChange = (beforeDoc, afterDoc) => {
3534
+ let before = [];
3535
+ let after = [];
3536
+ try {
3537
+ before = JSON.parse(beforeDoc);
3538
+ after = JSON.parse(afterDoc);
3539
+ } catch {
3540
+ return "Edit drawings";
3541
+ }
3542
+ const label = (type) => type ? type.replace(/-/g, " ") : "drawing";
3543
+ if (after.length > before.length) {
3544
+ const beforeIds = new Set(before.map((drawing) => drawing.id));
3545
+ const added = after.find((drawing) => !beforeIds.has(drawing.id));
3546
+ return `Add ${label(added?.type)}`;
3547
+ }
3548
+ if (after.length < before.length) {
3549
+ if (after.length === 0 && before.length > 1) return "Clear drawings";
3550
+ const afterIds = new Set(after.map((drawing) => drawing.id));
3551
+ const removed = before.find((drawing) => !afterIds.has(drawing.id));
3552
+ return `Delete ${label(removed?.type)}`;
3553
+ }
3554
+ const changed = after.find((drawing, index) => JSON.stringify(drawing) !== JSON.stringify(before[index]));
3555
+ return `Edit ${label(changed?.type)}`;
3556
+ };
3557
+ const getUndoRedoState = () => ({
3558
+ canUndo: historyPast.length > 0,
3559
+ canRedo: historyFuture.length > 0,
3560
+ undoLabel: historyPast.length > 0 ? historyPast[historyPast.length - 1].label : null,
3561
+ redoLabel: historyFuture.length > 0 ? historyFuture[historyFuture.length - 1].label : null
3562
+ });
3563
+ const emitUndoRedoState = () => {
3564
+ const state = getUndoRedoState();
3565
+ const signature = `${state.canUndo}|${state.canRedo}|${state.undoLabel}|${state.redoLabel}`;
3566
+ if (signature === lastUndoRedoSignature) return;
3567
+ lastUndoRedoSignature = signature;
3568
+ undoRedoStateHandler?.(state);
3569
+ };
3570
+ const commitHistory = () => {
3571
+ if (!historyEnabled || historySuspended > 0) return;
3572
+ if (drawingDragState !== null) return;
3573
+ const next = historyDoc();
3574
+ if (next === historyBaseline) return;
3575
+ historyPast.push({ label: describeHistoryChange(historyBaseline, next), doc: historyBaseline });
3576
+ if (historyPast.length > historyLimit) historyPast.shift();
3577
+ historyFuture = [];
3578
+ historyBaseline = next;
3579
+ emitUndoRedoState();
3580
+ };
3581
+ const resetHistoryBaseline = () => {
3582
+ historyBaseline = historyDoc();
3583
+ };
3584
+ const applyHistoryDoc = (doc) => {
3585
+ let parsed;
3586
+ try {
3587
+ parsed = JSON.parse(doc);
3588
+ } catch {
3589
+ return;
3590
+ }
3591
+ historySuspended += 1;
3592
+ try {
3593
+ drawings = dedupeDrawingIds(parsed.map((drawing) => normalizeDrawingState(drawing)));
3594
+ if (selectedDrawingId !== null && !drawings.some((drawing) => drawing.id === selectedDrawingId)) {
3595
+ selectedDrawingId = null;
3596
+ }
3597
+ draftDrawing = null;
3598
+ drawingDragState = null;
3599
+ historyBaseline = doc;
3600
+ emitDrawingsChange();
3601
+ scheduleDraw({ updateAutoScale: true });
3602
+ } finally {
3603
+ historySuspended -= 1;
3604
+ }
3605
+ };
3606
+ const undo = () => {
3607
+ if (!historyEnabled || historyPast.length === 0) return false;
3608
+ const entry = historyPast.pop();
3609
+ historyFuture.push({ label: entry.label, doc: historyBaseline });
3610
+ applyHistoryDoc(entry.doc);
3611
+ emitUndoRedoState();
3612
+ return true;
3613
+ };
3614
+ const redo = () => {
3615
+ if (!historyEnabled || historyFuture.length === 0) return false;
3616
+ const entry = historyFuture.pop();
3617
+ historyPast.push({ label: entry.label, doc: historyBaseline });
3618
+ applyHistoryDoc(entry.doc);
3619
+ emitUndoRedoState();
3620
+ return true;
3621
+ };
3622
+ const clearHistory = () => {
3623
+ historyPast = [];
3624
+ historyFuture = [];
3625
+ resetHistoryBaseline();
3626
+ emitUndoRedoState();
3627
+ };
3244
3628
  const emitDrawingsChange = () => {
3629
+ commitHistory();
3245
3630
  drawingsChangeHandler?.(drawings.map((drawing) => serializeDrawing(drawing)));
3246
3631
  };
3632
+ const normalizeAlert = (alert) => ({
3633
+ id: alert.id ?? `alert-${generatedAlertId++}`,
3634
+ price: Number(alert.price),
3635
+ condition: alert.condition ?? "crossing",
3636
+ label: alert.label ?? "",
3637
+ color: alert.color ?? "#f7a600",
3638
+ active: alert.active !== false,
3639
+ triggered: alert.triggered === true,
3640
+ once: alert.once !== false,
3641
+ draggable: alert.draggable !== false,
3642
+ lastPrice: null
3643
+ });
3644
+ const serializeAlert = (alert) => ({
3645
+ id: alert.id,
3646
+ price: alert.price,
3647
+ condition: alert.condition,
3648
+ label: alert.label,
3649
+ color: alert.color,
3650
+ active: alert.active,
3651
+ triggered: alert.triggered,
3652
+ once: alert.once,
3653
+ draggable: alert.draggable
3654
+ });
3655
+ const evaluateAlerts = (price, timeMs, index) => {
3656
+ if (alerts.length === 0 || !Number.isFinite(price)) return;
3657
+ for (const alert of alerts) {
3658
+ if (!alert.active || alert.triggered || !Number.isFinite(alert.price)) {
3659
+ alert.lastPrice = price;
3660
+ continue;
3661
+ }
3662
+ const previous = alert.lastPrice;
3663
+ let hit = false;
3664
+ switch (alert.condition) {
3665
+ case "greater":
3666
+ hit = price > alert.price;
3667
+ break;
3668
+ case "less":
3669
+ hit = price < alert.price;
3670
+ break;
3671
+ case "crossing-up":
3672
+ hit = previous !== null && previous < alert.price && price >= alert.price;
3673
+ break;
3674
+ case "crossing-down":
3675
+ hit = previous !== null && previous > alert.price && price <= alert.price;
3676
+ break;
3677
+ default:
3678
+ hit = previous !== null && (previous < alert.price && price >= alert.price || previous > alert.price && price <= alert.price);
3679
+ break;
3680
+ }
3681
+ alert.lastPrice = price;
3682
+ if (!hit) continue;
3683
+ alert.triggered = true;
3684
+ if (alert.once) alert.active = false;
3685
+ alertTriggerHandler?.({ alert: serializeAlert(alert), price, timeMs, index });
3686
+ scheduleDraw({ overlayOnly: false });
3687
+ }
3688
+ };
3689
+ const evaluateAlertsFromLatestBar = () => {
3690
+ if (alerts.length === 0 || data.length === 0) return;
3691
+ const last = data[data.length - 1];
3692
+ evaluateAlerts(last.c, last.time.getTime(), data.length - 1);
3693
+ };
3247
3694
  const orderWidgetWidthById = /* @__PURE__ */ new Map();
3248
3695
  const orderPriceTagWidthById = /* @__PURE__ */ new Map();
3249
3696
  let xCenter = 0;
@@ -3416,6 +3863,9 @@ function createChart(element, options = {}) {
3416
3863
  let overlayLayout = null;
3417
3864
  const margin = { top: 16, right: 72, bottom: 26, left: 12 };
3418
3865
  let cachedRightMargin = margin.right;
3866
+ let rightMarginShrinkPendingSince = null;
3867
+ const RIGHT_MARGIN_SHRINK_DELAY_MS = 600;
3868
+ const RIGHT_MARGIN_SHRINK_EPSILON_PX = 2;
3419
3869
  const minVisibleBars = Math.max(1, Math.floor(mergedOptions.minVisibleBars));
3420
3870
  const maxVisibleBars = Math.max(minVisibleBars, Math.floor(mergedOptions.maxVisibleBars));
3421
3871
  const maxPanBars = Math.max(0, Math.floor(mergedOptions.maxPanBars));
@@ -3743,6 +4193,7 @@ function createChart(element, options = {}) {
3743
4193
  };
3744
4194
  const resetRightMarginCache = () => {
3745
4195
  cachedRightMargin = margin.right;
4196
+ rightMarginShrinkPendingSince = null;
3746
4197
  };
3747
4198
  const parseData = (nextData) => {
3748
4199
  const dedupedByTime = /* @__PURE__ */ new Map();
@@ -4105,40 +4556,13 @@ function createChart(element, options = {}) {
4105
4556
  datafeedUnsubscribe = typeof unsubscribe === "function" ? unsubscribe : null;
4106
4557
  };
4107
4558
  const formatHoverTimeLabel = (time, mode) => {
4108
- if (mode === "time") {
4109
- return time.toLocaleTimeString(void 0, {
4110
- hour: "2-digit",
4111
- minute: "2-digit",
4112
- hour12: false
4113
- });
4114
- }
4115
- if (mode === "datetime") {
4116
- return time.toLocaleString(void 0, {
4117
- month: "short",
4118
- day: "numeric",
4119
- hour: "2-digit",
4120
- minute: "2-digit",
4121
- hour12: false
4122
- });
4123
- }
4124
- if (mode === "date") {
4125
- return time.toLocaleDateString(void 0, {
4126
- month: "short",
4127
- day: "numeric"
4128
- });
4129
- }
4559
+ const ms = time.getTime();
4560
+ if (mode === "time") return formatClock(time);
4561
+ if (mode === "datetime") return `${zoneClock.formatDayMonth(ms)}, ${formatClock(time)}`;
4562
+ if (mode === "date") return zoneClock.formatDayMonth(ms);
4130
4563
  const stepMs = getTimeStepMs();
4131
- if (stepMs < 24 * 60 * 60 * 1e3) {
4132
- return time.toLocaleTimeString(void 0, {
4133
- hour: "2-digit",
4134
- minute: "2-digit",
4135
- hour12: false
4136
- });
4137
- }
4138
- return time.toLocaleDateString(void 0, {
4139
- month: "short",
4140
- day: "numeric"
4141
- });
4564
+ if (stepMs < 24 * 60 * 60 * 1e3) return formatClock(time);
4565
+ return zoneClock.formatDayMonth(ms);
4142
4566
  };
4143
4567
  const drawText = (text, x, y, align = "left", baseline = "alphabetic", color = mergedOptions.axis?.textColor ?? mergedOptions.axisColor) => {
4144
4568
  ctx.fillStyle = color;
@@ -4235,6 +4659,86 @@ function createChart(element, options = {}) {
4235
4659
  labelTextColorOn(mergedLine.labelBackgroundColor, mergedLine.labelTextColor)
4236
4660
  );
4237
4661
  };
4662
+ const drawBellGlyph = (x, y, size, color) => {
4663
+ const w = size;
4664
+ const h = size;
4665
+ ctx.save();
4666
+ ctx.strokeStyle = color;
4667
+ ctx.fillStyle = color;
4668
+ ctx.lineWidth = 1.2;
4669
+ ctx.lineJoin = "round";
4670
+ ctx.beginPath();
4671
+ ctx.moveTo(x - w * 0.42, y + h * 0.28);
4672
+ ctx.quadraticCurveTo(x - w * 0.3, y + h * 0.2, x - w * 0.3, y - h * 0.05);
4673
+ ctx.quadraticCurveTo(x - w * 0.3, y - h * 0.46, x, y - h * 0.46);
4674
+ ctx.quadraticCurveTo(x + w * 0.3, y - h * 0.46, x + w * 0.3, y - h * 0.05);
4675
+ ctx.quadraticCurveTo(x + w * 0.3, y + h * 0.2, x + w * 0.42, y + h * 0.28);
4676
+ ctx.closePath();
4677
+ ctx.stroke();
4678
+ ctx.beginPath();
4679
+ ctx.arc(x, y + h * 0.42, Math.max(1, w * 0.1), 0, Math.PI * 2);
4680
+ ctx.fill();
4681
+ ctx.restore();
4682
+ };
4683
+ const drawAlertLine = (alert, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
4684
+ const lineY = getLineY(alert.price, yFromPrice, chartTop, chartBottom, false);
4685
+ if (lineY === null) return;
4686
+ const dim = alert.triggered || !alert.active;
4687
+ const color = alert.color || "#f7a600";
4688
+ const axis = resolvedAxis;
4689
+ ctx.save();
4690
+ ctx.globalAlpha = dim ? 0.5 : 1;
4691
+ ctx.strokeStyle = color;
4692
+ ctx.lineWidth = 1;
4693
+ applyDashPattern("dashed", dashPatterns.dotted, dashPatterns.dashed);
4694
+ ctx.beginPath();
4695
+ ctx.moveTo(crisp(chartLeft), crisp(lineY));
4696
+ ctx.lineTo(crisp(chartRight), crisp(lineY));
4697
+ ctx.stroke();
4698
+ ctx.restore();
4699
+ const fontSize = Math.max(8, axis.fontSize);
4700
+ ctx.font = `${fontSize}px ${mergedOptions.fontFamily}`;
4701
+ const text = alert.label ? `${formatPrice(alert.price)} ${alert.label}` : formatPrice(alert.price);
4702
+ const paddingX = 6;
4703
+ const bellWidth = fontSize + 2;
4704
+ const labelHeight = 20;
4705
+ const hovered = hoveredAlertId === alert.id;
4706
+ const removeWidth = hovered ? labelHeight : 0;
4707
+ const contentWidth = Math.ceil(measureTextWidth(text)) + bellWidth + paddingX * 3 + removeWidth;
4708
+ const labelWidth = getRightAxisLabelWidth(chartRight, contentWidth);
4709
+ const labelX = getRightAxisLabelX(chartRight);
4710
+ const labelY = placeRightAxisLabel(lineY - labelHeight / 2, labelHeight, chartTop, chartBottom - labelHeight);
4711
+ ctx.save();
4712
+ ctx.globalAlpha = dim ? 0.6 : 1;
4713
+ ctx.fillStyle = color;
4714
+ fillRoundedRect(Math.round(labelX), Math.round(labelY), labelWidth, labelHeight, 3);
4715
+ const inkColor = labelTextColorOn(color, "#101114");
4716
+ drawBellGlyph(labelX + paddingX + bellWidth / 2, labelY + labelHeight / 2, fontSize, inkColor);
4717
+ drawText(text, labelX + paddingX * 2 + bellWidth, labelY + labelHeight / 2, "left", "middle", inkColor);
4718
+ if (hovered) {
4719
+ const removeX = labelX + labelWidth - removeWidth;
4720
+ ctx.strokeStyle = inkColor;
4721
+ ctx.lineWidth = 1.4;
4722
+ const inset = 6;
4723
+ ctx.beginPath();
4724
+ ctx.moveTo(removeX + inset, labelY + inset);
4725
+ ctx.lineTo(removeX + removeWidth - inset, labelY + labelHeight - inset);
4726
+ ctx.moveTo(removeX + removeWidth - inset, labelY + inset);
4727
+ ctx.lineTo(removeX + inset, labelY + labelHeight - inset);
4728
+ ctx.stroke();
4729
+ alertRegions.push({
4730
+ id: alert.id,
4731
+ x: removeX,
4732
+ y: labelY,
4733
+ width: removeWidth,
4734
+ height: labelHeight,
4735
+ part: "remove"
4736
+ });
4737
+ }
4738
+ ctx.restore();
4739
+ alertRegions.push({ id: alert.id, x: labelX, y: labelY, width: labelWidth - removeWidth, height: labelHeight, part: "line" });
4740
+ alertRegions.push({ id: alert.id, x: chartLeft, y: lineY - 5, width: chartRight - chartLeft, height: 10, part: "line" });
4741
+ };
4238
4742
  const drawOrderLine = (line, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
4239
4743
  const mergedLine = { ...DEFAULT_ORDER_LINE_OPTIONS, ...line };
4240
4744
  const renderPrice = mergedLine.behavior === "follow" && Number.isFinite(mergedLine.followPrice) ? mergedLine.followPrice : mergedLine.price;
@@ -4519,6 +5023,37 @@ function createChart(element, options = {}) {
4519
5023
  ctx.closePath();
4520
5024
  ctx.stroke();
4521
5025
  };
5026
+ const drawMarkBadge = (x, y, radius, color, shape, label, textColor) => {
5027
+ ctx.save();
5028
+ ctx.fillStyle = color;
5029
+ ctx.beginPath();
5030
+ if (shape === "square") {
5031
+ ctx.rect(x - radius, y - radius, radius * 2, radius * 2);
5032
+ } else if (shape === "diamond") {
5033
+ ctx.moveTo(x, y - radius);
5034
+ ctx.lineTo(x + radius, y);
5035
+ ctx.lineTo(x, y + radius);
5036
+ ctx.lineTo(x - radius, y);
5037
+ ctx.closePath();
5038
+ } else if (shape === "flag") {
5039
+ ctx.rect(x - radius * 0.16, y - radius, radius * 0.32, radius * 2);
5040
+ ctx.moveTo(x + radius * 0.16, y - radius);
5041
+ ctx.lineTo(x + radius * 1.35, y - radius * 0.45);
5042
+ ctx.lineTo(x + radius * 0.16, y + radius * 0.1);
5043
+ ctx.closePath();
5044
+ } else {
5045
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
5046
+ }
5047
+ ctx.fill();
5048
+ if (label && shape !== "flag") {
5049
+ ctx.fillStyle = textColor;
5050
+ ctx.font = `600 ${Math.max(8, Math.round(radius * 1.2))}px ${mergedOptions.fontFamily}`;
5051
+ ctx.textAlign = "center";
5052
+ ctx.textBaseline = "middle";
5053
+ ctx.fillText(label.slice(0, 2), x, y + 0.5);
5054
+ }
5055
+ ctx.restore();
5056
+ };
4522
5057
  const setCrosshairPoint = (point) => {
4523
5058
  const current = crosshairPoint;
4524
5059
  if (point === null && current === null) {
@@ -4566,6 +5101,8 @@ function createChart(element, options = {}) {
4566
5101
  const shouldUpdateAutoScale = options2.updateAutoScale ?? true;
4567
5102
  orderActionRegions = [];
4568
5103
  orderDragRegions = [];
5104
+ alertRegions = [];
5105
+ markRegions = [];
4569
5106
  crosshairPriceActionRegion = null;
4570
5107
  const pixelRatio = getPixelRatio();
4571
5108
  canvas.style.width = `${width}px`;
@@ -4648,7 +5185,23 @@ function createChart(element, options = {}) {
4648
5185
  }
4649
5186
  const maxRightMargin = Math.max(margin.right, width - margin.left - 160);
4650
5187
  const nextRightMargin = Math.min(maxRightMargin, Math.max(margin.right, Math.ceil(required + 1)));
4651
- cachedRightMargin = Math.max(cachedRightMargin, nextRightMargin);
5188
+ if (nextRightMargin >= cachedRightMargin) {
5189
+ cachedRightMargin = nextRightMargin;
5190
+ rightMarginShrinkPendingSince = null;
5191
+ } else if (cachedRightMargin - nextRightMargin > RIGHT_MARGIN_SHRINK_EPSILON_PX) {
5192
+ const now = performance.now();
5193
+ if (rightMarginShrinkPendingSince === null) {
5194
+ rightMarginShrinkPendingSince = now;
5195
+ }
5196
+ if (now - rightMarginShrinkPendingSince >= RIGHT_MARGIN_SHRINK_DELAY_MS) {
5197
+ cachedRightMargin = nextRightMargin;
5198
+ rightMarginShrinkPendingSince = null;
5199
+ } else {
5200
+ scheduleDraw({ updateAutoScale: false });
5201
+ }
5202
+ } else {
5203
+ rightMarginShrinkPendingSince = null;
5204
+ }
4652
5205
  return Math.min(maxRightMargin, cachedRightMargin);
4653
5206
  };
4654
5207
  const rightMargin = estimateRightMargin();
@@ -5821,12 +6374,14 @@ function createChart(element, options = {}) {
5821
6374
  if (!prevTime) {
5822
6375
  weight = 7;
5823
6376
  } else {
5824
- if (prevTime.getFullYear() !== time.getFullYear()) weight = 9;
5825
- else if (prevTime.getMonth() !== time.getMonth()) weight = 8;
5826
- else if (prevTime.getDate() !== time.getDate()) weight = 7;
6377
+ const prev = zoneClock.parts(prevTime.getTime());
6378
+ const current = zoneClock.parts(time.getTime());
6379
+ if (prev.year !== current.year) weight = 9;
6380
+ else if (prev.month !== current.month) weight = 8;
6381
+ else if (prev.day !== current.day) weight = 7;
5827
6382
  else {
5828
- const prevMinutes = prevTime.getHours() * 60 + prevTime.getMinutes();
5829
- const curMinutes = time.getHours() * 60 + time.getMinutes();
6383
+ const prevMinutes = prev.hour * 60 + prev.minute;
6384
+ const curMinutes = current.hour * 60 + current.minute;
5830
6385
  const crossed = (span) => Math.floor(prevMinutes / span) !== Math.floor(curMinutes / span);
5831
6386
  if (crossed(360)) weight = 6;
5832
6387
  else if (crossed(60)) weight = 5;
@@ -5872,31 +6427,64 @@ function createChart(element, options = {}) {
5872
6427
  ctx.stroke();
5873
6428
  ctx.restore();
5874
6429
  }
5875
- if (grid.sessionSeparators && data.length > 1) {
6430
+ const sessionSeparatorsOn = grid.sessionSeparators || compiledSession !== null && sessionOptions.separators !== false;
6431
+ const intradaySeries = (() => {
6432
+ if (data.length < 2) return false;
6433
+ const probe = Math.min(Math.max(1, startIndex), data.length - 1);
6434
+ const delta = Math.abs(data[probe].time.getTime() - data[probe - 1].time.getTime());
6435
+ return delta > 0 && delta < 0.9 * 864e5;
6436
+ })();
6437
+ if (compiledSession && sessionOptions.highlightOutOfSession && intradaySeries && data.length > 0) {
6438
+ const session = compiledSession;
6439
+ ctx.save();
6440
+ ctx.fillStyle = sessionOptions.outOfSessionColor;
6441
+ let runStart = null;
6442
+ const flushRun = (endExclusive) => {
6443
+ if (runStart === null) return;
6444
+ const left = chartLeft + (runStart - xStart) / xSpan * chartWidth;
6445
+ const right = chartLeft + (endExclusive - xStart) / xSpan * chartWidth;
6446
+ const clampedLeft = Math.max(chartLeft, left);
6447
+ const clampedRight = Math.min(chartRight, right);
6448
+ if (clampedRight > clampedLeft) {
6449
+ ctx.fillRect(clampedLeft, chartTop, clampedRight - clampedLeft, fullChartBottom - chartTop);
6450
+ }
6451
+ runStart = null;
6452
+ };
6453
+ for (let index = Math.max(0, startIndex); index <= endIndex; index += 1) {
6454
+ const point = data[index];
6455
+ if (!point) continue;
6456
+ const inSession = sessionInfoAt(session, point.time.getTime()).inSession;
6457
+ if (!inSession) {
6458
+ if (runStart === null) runStart = index;
6459
+ } else {
6460
+ flushRun(index);
6461
+ }
6462
+ }
6463
+ flushRun(endIndex + 1);
6464
+ ctx.restore();
6465
+ }
6466
+ if (sessionSeparatorsOn && data.length > 1 && intradaySeries) {
5876
6467
  const first = Math.max(1, startIndex);
5877
- 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;
5878
- if (barDeltaMs > 0 && barDeltaMs < 0.9 * 864e5) {
5879
- const SESSION_ANCHOR_MS = 22 * 36e5;
5880
- const sessionDayOf = (ms) => Math.floor((ms - SESSION_ANCHOR_MS) / 864e5);
5881
- ctx.save();
5882
- ctx.globalAlpha = clamp(grid.sessionSeparatorOpacity, 0, 1);
5883
- ctx.strokeStyle = grid.sessionSeparatorColor;
5884
- ctx.lineWidth = 1;
5885
- for (let index = first; index <= endIndex; index += 1) {
5886
- const point = data[index];
5887
- const prev = data[index - 1];
5888
- if (!point || !prev) continue;
5889
- if (sessionDayOf(point.time.getTime()) === sessionDayOf(prev.time.getTime())) {
5890
- continue;
5891
- }
5892
- const x = chartLeft + (index - xStart) / xSpan * chartWidth;
5893
- ctx.beginPath();
5894
- ctx.moveTo(crisp(x), crisp(chartTop));
5895
- ctx.lineTo(crisp(x), crisp(fullChartBottom));
5896
- ctx.stroke();
6468
+ const session = compiledSession;
6469
+ const dayOf = (ms) => session ? sessionInfoAt(session, ms).sessionDay : zoneClock.dayKey(ms);
6470
+ ctx.save();
6471
+ ctx.globalAlpha = clamp(grid.sessionSeparatorOpacity, 0, 1);
6472
+ ctx.strokeStyle = grid.sessionSeparatorColor;
6473
+ ctx.lineWidth = 1;
6474
+ for (let index = first; index <= endIndex; index += 1) {
6475
+ const point = data[index];
6476
+ const prev = data[index - 1];
6477
+ if (!point || !prev) continue;
6478
+ if (dayOf(point.time.getTime()) === dayOf(prev.time.getTime())) {
6479
+ continue;
5897
6480
  }
5898
- ctx.restore();
6481
+ const x = chartLeft + (index - xStart) / xSpan * chartWidth;
6482
+ ctx.beginPath();
6483
+ ctx.moveTo(crisp(x), crisp(chartTop));
6484
+ ctx.lineTo(crisp(x), crisp(fullChartBottom));
6485
+ ctx.stroke();
5899
6486
  }
6487
+ ctx.restore();
5900
6488
  }
5901
6489
  const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
5902
6490
  const useSmoothedCandle = tickerOpts.smoothing && smoothedTickerPrice !== null;
@@ -6477,6 +7065,35 @@ function createChart(element, options = {}) {
6477
7065
  ctx.textAlign = prevAlign;
6478
7066
  ctx.textBaseline = prevBaseline;
6479
7067
  }
7068
+ if (barMarks.length > 0 && data.length > 0) {
7069
+ const prevFont = ctx.font;
7070
+ const prevAlign = ctx.textAlign;
7071
+ const prevBaseline = ctx.textBaseline;
7072
+ ctx.textAlign = "center";
7073
+ ctx.textBaseline = "middle";
7074
+ for (const mark of barMarks) {
7075
+ const ms = typeof mark.time === "number" ? mark.time : Date.parse(String(mark.time));
7076
+ if (!Number.isFinite(ms)) continue;
7077
+ const index = findNearestIndexForTimeMs(ms);
7078
+ if (index === null) continue;
7079
+ const bar = data[index];
7080
+ if (!bar) continue;
7081
+ const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
7082
+ if (x < chartLeft - 20 || x > chartRight + 20) continue;
7083
+ const radius = Math.max(4, Number(mark.size) || 7);
7084
+ const above = mark.position === "above";
7085
+ const y = Number.isFinite(mark.price) ? yFromPrice(Number(mark.price)) : above ? yFromPrice(bar.h) - radius - 8 : yFromPrice(bar.l) + radius + 8;
7086
+ if (y < chartTop - radius || y > chartBottom + radius) continue;
7087
+ const key = mark.id ?? `bar:${index}:${mark.label ?? ""}`;
7088
+ const hovered = hoveredMarkKey === `bar|${key}`;
7089
+ const color = mark.color ?? "#f7a600";
7090
+ drawMarkBadge(x, y, hovered ? radius + 1.5 : radius, color, mark.shape ?? "circle", mark.label ?? "", mark.textColor ?? "#101114");
7091
+ markRegions.push({ kind: "bar", mark, id: key, x, y, radius: radius + 3 });
7092
+ }
7093
+ ctx.font = prevFont;
7094
+ ctx.textAlign = prevAlign;
7095
+ ctx.textBaseline = prevBaseline;
7096
+ }
6480
7097
  ctx.restore();
6481
7098
  const positionAxisGutter = Math.max(0, width - chartRight);
6482
7099
  if (positionAxisGutter > 0) {
@@ -6987,6 +7604,9 @@ function createChart(element, options = {}) {
6987
7604
  for (const priceLine of priceLines) {
6988
7605
  drawPriceLine(priceLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
6989
7606
  }
7607
+ for (const alert of alerts) {
7608
+ drawAlertLine(alert, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
7609
+ }
6990
7610
  for (const orderLine of orderLines) {
6991
7611
  drawOrderLine(orderLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
6992
7612
  }
@@ -7096,11 +7716,36 @@ function createChart(element, options = {}) {
7096
7716
  for (const tick of timeTicks) {
7097
7717
  const tickTime = getTimeForIndex(tick.index);
7098
7718
  if (!tickTime) continue;
7099
- 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 });
7719
+ const tickMs = tickTime.getTime();
7720
+ const timeLabel = tick.weight >= 9 ? zoneClock.formatYear(tickMs) : tick.weight === 8 ? zoneClock.formatMonth(tickMs) : tick.weight === 7 ? zoneClock.formatDayMonth(tickMs) : formatClock(tickTime);
7100
7721
  drawText(timeLabel, tick.x, (fullChartBottom + height) / 2, "center", "middle", xAxis.textColor);
7101
7722
  }
7102
7723
  ctx.font = prevFont;
7103
7724
  }
7725
+ if (timescaleMarks.length > 0 && data.length > 0) {
7726
+ const radius = 6;
7727
+ const markY = fullChartBottom - radius - 2;
7728
+ for (const mark of timescaleMarks) {
7729
+ const ms = typeof mark.time === "number" ? mark.time : Date.parse(String(mark.time));
7730
+ if (!Number.isFinite(ms)) continue;
7731
+ const index = findNearestIndexForTimeMs(ms);
7732
+ if (index === null) continue;
7733
+ const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
7734
+ if (x < chartLeft - 10 || x > chartRight + 10) continue;
7735
+ const key = mark.id ?? `ts:${index}:${mark.label ?? ""}`;
7736
+ const hovered = hoveredMarkKey === `timescale|${key}`;
7737
+ drawMarkBadge(
7738
+ x,
7739
+ markY,
7740
+ hovered ? radius + 1.5 : radius,
7741
+ mark.color ?? "#5b8def",
7742
+ mark.shape ?? "circle",
7743
+ mark.label ?? "",
7744
+ mark.textColor ?? "#0b1220"
7745
+ );
7746
+ markRegions.push({ kind: "timescale", mark, id: key, x, y: markY, radius: radius + 3 });
7747
+ }
7748
+ }
7104
7749
  if (labels.visible && labels.showCountdownToBarClose && lastPoint) {
7105
7750
  const stepMs = getTimeStepMs();
7106
7751
  const nowMs = Date.now() + (Number(mergedOptions.clockOffsetMs) || 0);
@@ -7726,6 +8371,22 @@ function createChart(element, options = {}) {
7726
8371
  (region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
7727
8372
  );
7728
8373
  };
8374
+ const getAlertRegionAt = (x, y) => {
8375
+ const hits = alertRegions.filter(
8376
+ (region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
8377
+ );
8378
+ if (hits.length === 0) return null;
8379
+ return hits.find((region) => region.part === "remove") ?? hits[0];
8380
+ };
8381
+ const getMarkAt = (x, y) => {
8382
+ const tolerance = touchInputActive ? Math.max(1, mergedOptions.touch?.hitToleranceScale ?? 2.2) : 1;
8383
+ for (let index = markRegions.length - 1; index >= 0; index -= 1) {
8384
+ const region = markRegions[index];
8385
+ const reach = region.radius * tolerance;
8386
+ if (Math.abs(x - region.x) <= reach && Math.abs(y - region.y) <= reach) return region;
8387
+ }
8388
+ return null;
8389
+ };
7729
8390
  const getCrosshairPriceActionRegion = (x, y) => {
7730
8391
  if (!crosshairPriceActionRegion) {
7731
8392
  return null;
@@ -8885,7 +9546,10 @@ function createChart(element, options = {}) {
8885
9546
  pointerDownInfo = null;
8886
9547
  orderDragState = null;
8887
9548
  actionDragState = null;
8888
- drawingDragState = null;
9549
+ if (drawingDragState) {
9550
+ drawingDragState = null;
9551
+ commitHistory();
9552
+ }
8889
9553
  canvas.style.cursor = "default";
8890
9554
  setCrosshairPoint(null);
8891
9555
  };
@@ -8980,6 +9644,20 @@ function createChart(element, options = {}) {
8980
9644
  });
8981
9645
  return;
8982
9646
  }
9647
+ const markHit = getMarkAt(point.x, point.y);
9648
+ if (markHit && markClickHandler) {
9649
+ const rect = canvas.getBoundingClientRect();
9650
+ setCrosshairPoint(null);
9651
+ markClickHandler({
9652
+ kind: markHit.kind,
9653
+ mark: markHit.mark,
9654
+ x: markHit.x,
9655
+ y: markHit.y,
9656
+ clientX: rect.left + markHit.x,
9657
+ clientY: rect.top + markHit.y
9658
+ });
9659
+ return;
9660
+ }
8983
9661
  const orderRegion = getOrderActionRegion(point.x, point.y);
8984
9662
  if (orderRegion) {
8985
9663
  if (orderRegion.draggable) {
@@ -9018,6 +9696,28 @@ function createChart(element, options = {}) {
9018
9696
  setCrosshairPoint(null);
9019
9697
  return;
9020
9698
  }
9699
+ const alertHit = getAlertRegionAt(point.x, point.y);
9700
+ if (alertHit) {
9701
+ const target = alerts.find((alert) => alert.id === alertHit.id);
9702
+ if (target) {
9703
+ setCrosshairPoint(null);
9704
+ if (alertHit.part === "remove") {
9705
+ const removed = serializeAlert(target);
9706
+ removeAlert(target.id);
9707
+ alertActionHandler?.({ alert: removed, action: "remove" });
9708
+ return;
9709
+ }
9710
+ if (target.draggable) {
9711
+ activePointerId = event.pointerId;
9712
+ alertDragState = { id: target.id, startPrice: target.price, lastPrice: target.price, moved: false };
9713
+ capturePointer(event.pointerId);
9714
+ canvas.style.cursor = "ns-resize";
9715
+ return;
9716
+ }
9717
+ alertActionHandler?.({ alert: serializeAlert(target), action: "click", price: target.price });
9718
+ return;
9719
+ }
9720
+ }
9021
9721
  const paneButtonHit = getPaneButtonHit(point.x, point.y);
9022
9722
  if (paneButtonHit) {
9023
9723
  setCrosshairPoint(null);
@@ -9260,6 +9960,24 @@ function createChart(element, options = {}) {
9260
9960
  setCrosshairPoint(null);
9261
9961
  return;
9262
9962
  }
9963
+ if (alertDragState) {
9964
+ if (activePointerId !== null && event.pointerId !== activePointerId) {
9965
+ return;
9966
+ }
9967
+ const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
9968
+ if (nextPrice !== alertDragState.lastPrice) {
9969
+ alertDragState.lastPrice = nextPrice;
9970
+ alertDragState.moved = true;
9971
+ const target = alerts.find((alert) => alert.id === alertDragState?.id);
9972
+ if (target) {
9973
+ target.price = nextPrice;
9974
+ target.lastPrice = data[data.length - 1]?.c ?? null;
9975
+ alertActionHandler?.({ alert: serializeAlert(target), action: "move", price: nextPrice, dragging: true });
9976
+ }
9977
+ scheduleDraw();
9978
+ }
9979
+ return;
9980
+ }
9263
9981
  if (orderDragState) {
9264
9982
  if (activePointerId !== null && event.pointerId !== activePointerId) {
9265
9983
  return;
@@ -9326,12 +10044,49 @@ function createChart(element, options = {}) {
9326
10044
  setCrosshairPoint(null);
9327
10045
  return;
9328
10046
  }
10047
+ const markHover = getMarkAt(point.x, point.y);
10048
+ const nextMarkKey = markHover ? `${markHover.kind}|${markHover.id}` : null;
10049
+ if (nextMarkKey !== hoveredMarkKey) {
10050
+ hoveredMarkKey = nextMarkKey;
10051
+ if (markHover) {
10052
+ const rect = canvas.getBoundingClientRect();
10053
+ markHoverHandler?.({
10054
+ kind: markHover.kind,
10055
+ mark: markHover.mark,
10056
+ x: markHover.x,
10057
+ y: markHover.y,
10058
+ clientX: rect.left + markHover.x,
10059
+ clientY: rect.top + markHover.y
10060
+ });
10061
+ } else {
10062
+ markHoverHandler?.(null);
10063
+ }
10064
+ scheduleDraw();
10065
+ }
10066
+ if (markHover) {
10067
+ canvas.title = markHover.mark.text ?? markHover.mark.label ?? "";
10068
+ canvas.style.cursor = "pointer";
10069
+ setCrosshairPoint(null);
10070
+ return;
10071
+ }
9329
10072
  const orderDragRegion = getOrderDragRegion(point.x, point.y);
9330
10073
  if (orderDragRegion) {
9331
10074
  canvas.style.cursor = "ns-resize";
9332
10075
  setCrosshairPoint(null);
9333
10076
  return;
9334
10077
  }
10078
+ const alertHover = getAlertRegionAt(point.x, point.y);
10079
+ const nextAlertId = alertHover?.id ?? null;
10080
+ if (nextAlertId !== hoveredAlertId) {
10081
+ hoveredAlertId = nextAlertId;
10082
+ scheduleDraw();
10083
+ }
10084
+ if (alertHover) {
10085
+ canvas.title = alertHover.part === "remove" ? "Remove alert" : "Drag to move alert";
10086
+ canvas.style.cursor = alertHover.part === "remove" ? "pointer" : "ns-resize";
10087
+ setCrosshairPoint(null);
10088
+ return;
10089
+ }
9335
10090
  const hoveredPane = getPaneAt(point.x, point.y);
9336
10091
  const nextHoveredPaneId = hoveredPane?.id ?? null;
9337
10092
  if (nextHoveredPaneId !== hoveredPaneId) {
@@ -9489,6 +10244,21 @@ function createChart(element, options = {}) {
9489
10244
  scheduleDraw();
9490
10245
  return;
9491
10246
  }
10247
+ if (alertDragState) {
10248
+ const target = alerts.find((alert) => alert.id === alertDragState?.id);
10249
+ if (target && alertDragState.moved) {
10250
+ alertActionHandler?.({
10251
+ alert: serializeAlert(target),
10252
+ action: "move",
10253
+ price: alertDragState.lastPrice,
10254
+ dragging: false
10255
+ });
10256
+ } else if (target) {
10257
+ alertActionHandler?.({ alert: serializeAlert(target), action: "click", price: target.price });
10258
+ }
10259
+ alertDragState = null;
10260
+ canvas.style.cursor = "default";
10261
+ }
9492
10262
  if (orderDragState) {
9493
10263
  const moved = orderDragState.lastPrice !== orderDragState.startPrice;
9494
10264
  const finalLine = orderLines.find((line) => line.id === orderDragState?.orderId);
@@ -9535,9 +10305,14 @@ function createChart(element, options = {}) {
9535
10305
  });
9536
10306
  paneDividerDrag = null;
9537
10307
  }
9538
- if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null)) {
10308
+ if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null || hoveredAlertId !== null || hoveredMarkKey !== null)) {
9539
10309
  hoveredPaneId = null;
9540
10310
  hoveredPaneButton = null;
10311
+ hoveredAlertId = null;
10312
+ if (hoveredMarkKey !== null) {
10313
+ hoveredMarkKey = null;
10314
+ markHoverHandler?.(null);
10315
+ }
9541
10316
  if (canvas.title) {
9542
10317
  canvas.title = "";
9543
10318
  }
@@ -9713,9 +10488,6 @@ function createChart(element, options = {}) {
9713
10488
  if (keyboard.enabled === false) {
9714
10489
  return;
9715
10490
  }
9716
- if (event.altKey || event.ctrlKey || event.metaKey) {
9717
- return;
9718
- }
9719
10491
  const panStep = Math.max(1, keyboard.panBars ?? 1) * (event.shiftKey ? 10 : 1);
9720
10492
  const emit = (action, drawing) => {
9721
10493
  keyboardShortcutHandler?.({
@@ -9725,6 +10497,37 @@ function createChart(element, options = {}) {
9725
10497
  ...drawing ? { drawing } : {}
9726
10498
  });
9727
10499
  };
10500
+ if ((event.ctrlKey || event.metaKey) && !event.altKey) {
10501
+ const key = event.key.toLowerCase();
10502
+ if (keyboard.undoRedo !== false && (key === "z" || key === "y")) {
10503
+ const wantsRedo = key === "y" || event.shiftKey;
10504
+ const applied = wantsRedo ? redo() : undo();
10505
+ if (applied) {
10506
+ event.preventDefault();
10507
+ emit(wantsRedo ? "redo" : "undo");
10508
+ }
10509
+ return;
10510
+ }
10511
+ }
10512
+ if (event.altKey || event.ctrlKey || event.metaKey) {
10513
+ return;
10514
+ }
10515
+ if (replay && keyboard.replay !== false) {
10516
+ if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
10517
+ const direction = event.key === "ArrowRight" ? 1 : -1;
10518
+ replayStep(direction * (event.shiftKey ? 10 : 1));
10519
+ emit(direction > 0 ? "replay-step-forward" : "replay-step-back");
10520
+ event.preventDefault();
10521
+ return;
10522
+ }
10523
+ if (event.key === " " || event.key === "Spacebar") {
10524
+ if (replay.playing) replayPause();
10525
+ else replayPlay();
10526
+ emit("replay-toggle-play");
10527
+ event.preventDefault();
10528
+ return;
10529
+ }
10530
+ }
9728
10531
  switch (event.key) {
9729
10532
  case "Delete":
9730
10533
  case "Backspace": {
@@ -9870,6 +10673,20 @@ function createChart(element, options = {}) {
9870
10673
  doubleClickEnabled = mergedOptions.doubleClickEnabled;
9871
10674
  doubleClickAction = mergedOptions.doubleClickAction;
9872
10675
  setIndicatorLiveThrottleMs(mergedOptions.indicatorUpdate?.liveThrottleMs ?? 80);
10676
+ if (nextOptions.timezone !== void 0) {
10677
+ timezoneSetting = nextOptions.timezone || "local";
10678
+ zoneClock = createZoneClock(timezoneSetting);
10679
+ }
10680
+ if (nextOptions.timeFormat !== void 0) {
10681
+ timeFormat = nextOptions.timeFormat === "12h" ? "12h" : "24h";
10682
+ }
10683
+ if (nextOptions.session !== void 0) {
10684
+ sessionOptions = { ...sessionOptions, ...nextOptions.session ?? {} };
10685
+ compiledSession = compileSession(sessionOptions.spec);
10686
+ }
10687
+ if (nextOptions.alerts !== void 0) {
10688
+ setAlerts(nextOptions.alerts ?? []);
10689
+ }
9873
10690
  applyAccessibilityAttributes();
9874
10691
  const isTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
9875
10692
  if (!isTickerSmoothingEnabled) {
@@ -9930,7 +10747,7 @@ function createChart(element, options = {}) {
9930
10747
  resetRightMarginCache();
9931
10748
  scheduleDraw();
9932
10749
  };
9933
- const setData = (nextData) => {
10750
+ const setDataInternal = (nextData) => {
9934
10751
  const hadData = data.length > 0;
9935
10752
  const previousCount = data.length;
9936
10753
  const previousCenterRounded = hadData ? Math.round(xCenter) : 0;
@@ -9972,9 +10789,10 @@ function createChart(element, options = {}) {
9972
10789
  if (lastPoint) {
9973
10790
  pushSmoothedTicker(lastPoint.c, lastPoint.v);
9974
10791
  }
10792
+ evaluateAlertsFromLatestBar();
9975
10793
  scheduleDraw();
9976
10794
  };
9977
- const upsertBar = (point) => {
10795
+ const upsertBarInternal = (point) => {
9978
10796
  const time = new Date(point.t);
9979
10797
  const timeMs = time.getTime();
9980
10798
  const open = Number(point.o);
@@ -9995,7 +10813,7 @@ function createChart(element, options = {}) {
9995
10813
  };
9996
10814
  const last = data[data.length - 1];
9997
10815
  if (!last) {
9998
- setData([point]);
10816
+ setDataInternal([point]);
9999
10817
  return;
10000
10818
  }
10001
10819
  const lastMs = last.time.getTime();
@@ -10017,8 +10835,167 @@ function createChart(element, options = {}) {
10017
10835
  reanchorDrawingsToData();
10018
10836
  }
10019
10837
  pushSmoothedTicker(parsed.c, parsed.v);
10838
+ evaluateAlerts(parsed.c, timeMs, data.length - 1);
10020
10839
  scheduleDraw();
10021
10840
  };
10841
+ const toRawPoint = (point) => ({
10842
+ t: point.time.toISOString(),
10843
+ o: point.o,
10844
+ h: point.h,
10845
+ l: point.l,
10846
+ c: point.c,
10847
+ ...point.v === void 0 ? {} : { v: point.v }
10848
+ });
10849
+ const getReplayState = () => {
10850
+ if (!replay) {
10851
+ return {
10852
+ active: false,
10853
+ playing: false,
10854
+ speed: 1,
10855
+ index: Math.max(0, data.length - 1),
10856
+ total: data.length,
10857
+ atEnd: true,
10858
+ timeMs: data.length > 0 ? data[data.length - 1].time.getTime() : null
10859
+ };
10860
+ }
10861
+ const total = replay.fullRaw.length;
10862
+ const revealed = replay.fullRaw[replay.cursor];
10863
+ const revealedMs = revealed ? Date.parse(String(revealed.t)) : Number.NaN;
10864
+ return {
10865
+ active: true,
10866
+ playing: replay.playing,
10867
+ speed: replay.speed,
10868
+ index: replay.cursor,
10869
+ total,
10870
+ atEnd: replay.cursor >= total - 1,
10871
+ timeMs: Number.isFinite(revealedMs) ? revealedMs : null
10872
+ };
10873
+ };
10874
+ const emitReplayState = () => {
10875
+ replayStateHandler?.(getReplayState());
10876
+ };
10877
+ const applyReplaySlice = () => {
10878
+ if (!replay) return;
10879
+ setDataInternal(replay.fullRaw.slice(0, replay.cursor + 1));
10880
+ };
10881
+ const clearReplayTimer = () => {
10882
+ if (replay?.timerId) {
10883
+ clearTimeout(replay.timerId);
10884
+ replay.timerId = null;
10885
+ }
10886
+ };
10887
+ const scheduleReplayTick = () => {
10888
+ if (!replay || !replay.playing) return;
10889
+ clearReplayTimer();
10890
+ const intervalMs = Math.max(16, 1e3 / Math.max(0.05, replay.speed));
10891
+ replay.timerId = setTimeout(() => {
10892
+ if (!replay || !replay.playing) return;
10893
+ if (replay.cursor >= replay.fullRaw.length - 1) {
10894
+ replay.playing = false;
10895
+ clearReplayTimer();
10896
+ emitReplayState();
10897
+ return;
10898
+ }
10899
+ replay.cursor += 1;
10900
+ applyReplaySlice();
10901
+ emitReplayState();
10902
+ scheduleReplayTick();
10903
+ }, intervalMs);
10904
+ };
10905
+ const startReplay = (startOptions = {}) => {
10906
+ const source = replay ? replay.fullRaw : data.map((point) => toRawPoint(point));
10907
+ if (source.length === 0) return;
10908
+ clearReplayTimer();
10909
+ let cursor;
10910
+ if (Number.isFinite(startOptions.fromTimeMs)) {
10911
+ const target = Number(startOptions.fromTimeMs);
10912
+ const resolved = findNearestIndexForTimeMs(target);
10913
+ cursor = resolved ?? Math.floor(source.length * 0.7);
10914
+ } else if (Number.isFinite(startOptions.fromIndex)) {
10915
+ cursor = Math.floor(Number(startOptions.fromIndex));
10916
+ } else {
10917
+ cursor = Math.floor(source.length * 0.7);
10918
+ }
10919
+ cursor = clamp(cursor, 0, source.length - 1);
10920
+ const speed = Math.max(0.05, Number(startOptions.speed) || replay?.speed || 1);
10921
+ replay = { fullRaw: source, cursor, playing: false, speed, timerId: null };
10922
+ applyReplaySlice();
10923
+ setFollowingLatest(true);
10924
+ if (startOptions.play) {
10925
+ replay.playing = true;
10926
+ scheduleReplayTick();
10927
+ }
10928
+ emitReplayState();
10929
+ };
10930
+ const stopReplay = () => {
10931
+ if (!replay) return;
10932
+ clearReplayTimer();
10933
+ const full = replay.fullRaw;
10934
+ replay = null;
10935
+ setDataInternal(full);
10936
+ setFollowingLatest(true);
10937
+ emitReplayState();
10938
+ };
10939
+ const replayStep = (bars = 1) => {
10940
+ if (!replay) return;
10941
+ const step = Math.trunc(bars) || 1;
10942
+ replaySeek(replay.cursor + step);
10943
+ };
10944
+ const replaySeek = (index) => {
10945
+ if (!replay || !Number.isFinite(index)) return;
10946
+ const next = clamp(Math.round(index), 0, replay.fullRaw.length - 1);
10947
+ if (next === replay.cursor) return;
10948
+ replay.cursor = next;
10949
+ applyReplaySlice();
10950
+ emitReplayState();
10951
+ };
10952
+ const replayPlay = (speed) => {
10953
+ if (!replay) return;
10954
+ if (Number.isFinite(speed)) replay.speed = Math.max(0.05, Number(speed));
10955
+ if (replay.cursor >= replay.fullRaw.length - 1) return;
10956
+ replay.playing = true;
10957
+ scheduleReplayTick();
10958
+ emitReplayState();
10959
+ };
10960
+ const replayPause = () => {
10961
+ if (!replay || !replay.playing) return;
10962
+ replay.playing = false;
10963
+ clearReplayTimer();
10964
+ emitReplayState();
10965
+ };
10966
+ const setReplaySpeed = (speed) => {
10967
+ if (!replay || !Number.isFinite(speed)) return;
10968
+ replay.speed = clamp(Number(speed), 0.05, 100);
10969
+ if (replay.playing) scheduleReplayTick();
10970
+ emitReplayState();
10971
+ };
10972
+ const onReplayStateChange = (handler) => {
10973
+ replayStateHandler = handler;
10974
+ };
10975
+ const setData = (nextData) => {
10976
+ if (replay) {
10977
+ replay.fullRaw = nextData.slice();
10978
+ replay.cursor = clamp(replay.cursor, 0, Math.max(0, replay.fullRaw.length - 1));
10979
+ applyReplaySlice();
10980
+ emitReplayState();
10981
+ return;
10982
+ }
10983
+ setDataInternal(nextData);
10984
+ };
10985
+ const upsertBar = (point) => {
10986
+ if (replay) {
10987
+ const timeMs = Date.parse(String(point.t));
10988
+ if (!Number.isFinite(timeMs)) return;
10989
+ const buffer = replay.fullRaw;
10990
+ const lastRaw = buffer[buffer.length - 1];
10991
+ const lastMs = lastRaw ? Date.parse(String(lastRaw.t)) : Number.NaN;
10992
+ if (Number.isFinite(lastMs) && timeMs === lastMs) buffer[buffer.length - 1] = point;
10993
+ else if (!Number.isFinite(lastMs) || timeMs > lastMs) buffer.push(point);
10994
+ emitReplayState();
10995
+ return;
10996
+ }
10997
+ upsertBarInternal(point);
10998
+ };
10022
10999
  const setPriceLines = (lines) => {
10023
11000
  priceLines = lines.map((line, index) => ({
10024
11001
  ...line,
@@ -10207,6 +11184,96 @@ function createChart(element, options = {}) {
10207
11184
  tradeMarkers = Array.isArray(markers) ? markers.slice() : [];
10208
11185
  scheduleDraw();
10209
11186
  };
11187
+ const setBarMarks = (marks) => {
11188
+ barMarks = Array.isArray(marks) ? marks.slice() : [];
11189
+ scheduleDraw();
11190
+ };
11191
+ const getBarMarks = () => barMarks.slice();
11192
+ const setTimescaleMarks = (marks) => {
11193
+ timescaleMarks = Array.isArray(marks) ? marks.slice() : [];
11194
+ scheduleDraw();
11195
+ };
11196
+ const getTimescaleMarks = () => timescaleMarks.slice();
11197
+ const onMarkClick = (handler) => {
11198
+ markClickHandler = handler;
11199
+ };
11200
+ const onMarkHover = (handler) => {
11201
+ markHoverHandler = handler;
11202
+ };
11203
+ const setAlerts = (nextAlerts) => {
11204
+ alerts = (Array.isArray(nextAlerts) ? nextAlerts : []).map((alert) => normalizeAlert(alert));
11205
+ const last = data[data.length - 1];
11206
+ if (last) {
11207
+ for (const alert of alerts) alert.lastPrice = last.c;
11208
+ }
11209
+ resetRightMarginCache();
11210
+ scheduleDraw();
11211
+ };
11212
+ const addAlert = (alert) => {
11213
+ const next = normalizeAlert(alert);
11214
+ const last = data[data.length - 1];
11215
+ if (last) next.lastPrice = last.c;
11216
+ alerts.push(next);
11217
+ resetRightMarginCache();
11218
+ scheduleDraw();
11219
+ return next.id;
11220
+ };
11221
+ const updateAlert = (id, patch) => {
11222
+ const target = alerts.find((alert) => alert.id === id);
11223
+ if (!target) return;
11224
+ if (Number.isFinite(patch.price)) target.price = Number(patch.price);
11225
+ if (patch.condition) target.condition = patch.condition;
11226
+ if (patch.label !== void 0) target.label = patch.label;
11227
+ if (patch.color !== void 0) target.color = patch.color;
11228
+ if (patch.active !== void 0) target.active = patch.active;
11229
+ if (patch.once !== void 0) target.once = patch.once;
11230
+ if (patch.draggable !== void 0) target.draggable = patch.draggable;
11231
+ if (patch.triggered !== void 0) {
11232
+ target.triggered = patch.triggered;
11233
+ if (!patch.triggered) target.lastPrice = data[data.length - 1]?.c ?? null;
11234
+ }
11235
+ resetRightMarginCache();
11236
+ scheduleDraw();
11237
+ };
11238
+ const removeAlert = (id) => {
11239
+ alerts = alerts.filter((alert) => alert.id !== id);
11240
+ if (hoveredAlertId === id) hoveredAlertId = null;
11241
+ resetRightMarginCache();
11242
+ scheduleDraw();
11243
+ };
11244
+ const getAlerts = () => alerts.map((alert) => serializeAlert(alert));
11245
+ const onAlertTrigger = (handler) => {
11246
+ alertTriggerHandler = handler;
11247
+ };
11248
+ const onAlertAction = (handler) => {
11249
+ alertActionHandler = handler;
11250
+ };
11251
+ const setTimezone = (timezone) => {
11252
+ timezoneSetting = timezone || "local";
11253
+ zoneClock = createZoneClock(timezoneSetting);
11254
+ scheduleDraw();
11255
+ };
11256
+ const getTimezone = () => timezoneSetting;
11257
+ const setTimeFormat = (format) => {
11258
+ timeFormat = format === "12h" ? "12h" : "24h";
11259
+ resetRightMarginCache();
11260
+ scheduleDraw();
11261
+ };
11262
+ const getTimeFormat = () => timeFormat;
11263
+ const setSession = (session) => {
11264
+ if (session === null) {
11265
+ sessionOptions = { ...sessionOptions, spec: null };
11266
+ } else if (typeof session === "string") {
11267
+ sessionOptions = { ...sessionOptions, spec: session };
11268
+ } else if ("segments" in session || "timezone" in session || "days" in session) {
11269
+ sessionOptions = { ...sessionOptions, spec: session };
11270
+ } else {
11271
+ sessionOptions = { ...sessionOptions, ...session };
11272
+ }
11273
+ compiledSession = compileSession(sessionOptions.spec);
11274
+ scheduleDraw();
11275
+ };
11276
+ const getSession = () => ({ ...sessionOptions });
10210
11277
  const setMagnetMode = (mode) => {
10211
11278
  magnetMode = mode;
10212
11279
  };
@@ -10226,7 +11293,13 @@ function createChart(element, options = {}) {
10226
11293
  drawings = dedupeDrawingIds(nextDrawings.map((drawing) => normalizeDrawingState(drawing)));
10227
11294
  draftDrawing = null;
10228
11295
  reanchorDrawingsToData();
10229
- emitDrawingsChange();
11296
+ historySuspended += 1;
11297
+ try {
11298
+ emitDrawingsChange();
11299
+ } finally {
11300
+ historySuspended -= 1;
11301
+ }
11302
+ resetHistoryBaseline();
10230
11303
  scheduleDraw();
10231
11304
  };
10232
11305
  const addDrawing = (drawing) => {
@@ -10311,7 +11384,10 @@ function createChart(element, options = {}) {
10311
11384
  indicators: getIndicators(),
10312
11385
  magnetMode: getMagnetMode(),
10313
11386
  priceScale: getPriceScale(),
10314
- drawingDefaults
11387
+ drawingDefaults,
11388
+ alerts: getAlerts(),
11389
+ timezone: getTimezone(),
11390
+ timeFormat: getTimeFormat()
10315
11391
  };
10316
11392
  };
10317
11393
  const loadState = (state) => {
@@ -10340,9 +11416,19 @@ function createChart(element, options = {}) {
10340
11416
  }
10341
11417
  }
10342
11418
  }
11419
+ if (Array.isArray(state.alerts)) {
11420
+ setAlerts(state.alerts);
11421
+ }
11422
+ if (typeof state.timezone === "string") {
11423
+ setTimezone(state.timezone);
11424
+ }
11425
+ if (state.timeFormat === "12h" || state.timeFormat === "24h") {
11426
+ setTimeFormat(state.timeFormat);
11427
+ }
10343
11428
  if (state.viewport && typeof state.viewport === "object") {
10344
11429
  setViewport(state.viewport);
10345
11430
  }
11431
+ clearHistory();
10346
11432
  };
10347
11433
  const takeScreenshot = (options2 = {}) => {
10348
11434
  return canvas.toDataURL(options2.type ?? "image/png", options2.quality);
@@ -10350,6 +11436,8 @@ function createChart(element, options = {}) {
10350
11436
  const destroy = () => {
10351
11437
  cancelViewportTween();
10352
11438
  cancelLongPressTimer();
11439
+ clearReplayTimer();
11440
+ replay = null;
10353
11441
  datafeedUnsubscribe?.();
10354
11442
  datafeedUnsubscribe = null;
10355
11443
  datafeed = null;
@@ -10383,6 +11471,10 @@ function createChart(element, options = {}) {
10383
11471
  }
10384
11472
  element.innerHTML = "";
10385
11473
  };
11474
+ if (Array.isArray(options.alerts) && options.alerts.length > 0) {
11475
+ setAlerts(options.alerts);
11476
+ }
11477
+ resetHistoryBaseline();
10386
11478
  draw();
10387
11479
  armCountdownHeartbeat();
10388
11480
  return {
@@ -10466,6 +11558,43 @@ function createChart(element, options = {}) {
10466
11558
  saveState,
10467
11559
  loadState,
10468
11560
  takeScreenshot,
11561
+ setTimezone,
11562
+ getTimezone,
11563
+ setTimeFormat,
11564
+ getTimeFormat,
11565
+ setSession,
11566
+ getSession,
11567
+ undo,
11568
+ redo,
11569
+ canUndo: () => historyPast.length > 0,
11570
+ canRedo: () => historyFuture.length > 0,
11571
+ getUndoRedoState,
11572
+ onUndoRedoStateChange: (handler) => {
11573
+ undoRedoStateHandler = handler;
11574
+ },
11575
+ clearHistory,
11576
+ startReplay,
11577
+ stopReplay,
11578
+ replayStep,
11579
+ replaySeek,
11580
+ replayPlay,
11581
+ replayPause,
11582
+ setReplaySpeed,
11583
+ getReplayState,
11584
+ onReplayStateChange,
11585
+ setAlerts,
11586
+ addAlert,
11587
+ updateAlert,
11588
+ removeAlert,
11589
+ getAlerts,
11590
+ onAlertTrigger,
11591
+ onAlertAction,
11592
+ setBarMarks,
11593
+ getBarMarks,
11594
+ setTimescaleMarks,
11595
+ getTimescaleMarks,
11596
+ onMarkClick,
11597
+ onMarkHover,
10469
11598
  resize,
10470
11599
  destroy
10471
11600
  };
@@ -10476,8 +11605,10 @@ export {
10476
11605
  FIB_DEFAULT_PALETTE,
10477
11606
  POSITION_DEFAULT_COLORS,
10478
11607
  SCRIPT_SOURCE_INPUT_VALUES,
11608
+ SESSION_PRESETS,
10479
11609
  compileScriptIndicator,
10480
11610
  createChart,
10481
11611
  extractScriptInputs,
11612
+ isValidTimeZone,
10482
11613
  validateScriptSource
10483
11614
  };