hyperprop-charting-library 0.1.143 → 0.1.144

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -25,9 +25,11 @@ __export(index_exports, {
25
25
  FIB_DEFAULT_PALETTE: () => FIB_DEFAULT_PALETTE,
26
26
  POSITION_DEFAULT_COLORS: () => POSITION_DEFAULT_COLORS,
27
27
  SCRIPT_SOURCE_INPUT_VALUES: () => SCRIPT_SOURCE_INPUT_VALUES,
28
+ SESSION_PRESETS: () => SESSION_PRESETS,
28
29
  compileScriptIndicator: () => compileScriptIndicator,
29
30
  createChart: () => createChart,
30
31
  extractScriptInputs: () => extractScriptInputs,
32
+ isValidTimeZone: () => isValidTimeZone,
31
33
  validateScriptSource: () => validateScriptSource
32
34
  });
33
35
  module.exports = __toCommonJS(index_exports);
@@ -2777,6 +2779,237 @@ var CHART_THEMES = {
2777
2779
  var CHART_THEME_NAMES = Object.keys(CHART_THEMES);
2778
2780
  var resolveTheme = (theme) => typeof theme === "string" ? CHART_THEMES[theme] ?? CHART_THEMES.midnight : theme;
2779
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
+
2780
3013
  // src/options.ts
2781
3014
  var DEFAULT_GRID_OPTIONS = {
2782
3015
  color: "#2b2f38",
@@ -2994,9 +3227,19 @@ var DEFAULT_OPTIONS = {
2994
3227
  datafeedOptions: { prefetchThresholdBars: 150, cooldownMs: 1200, chunkBars: 1500 },
2995
3228
  animation: { viewportTransitions: true, durationMs: 260 },
2996
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 },
2997
3239
  crosshair: DEFAULT_CROSSHAIR_OPTIONS,
2998
3240
  grid: DEFAULT_GRID_OPTIONS,
2999
3241
  watermark: DEFAULT_WATERMARK_OPTIONS,
3242
+ alerts: [],
3000
3243
  priceLines: [],
3001
3244
  orderLines: [],
3002
3245
  tickerLine: {
@@ -3064,6 +3307,14 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
3064
3307
  ...baseOptions.indicatorUpdate,
3065
3308
  ...options.indicatorUpdate ?? {}
3066
3309
  },
3310
+ session: {
3311
+ ...baseOptions.session,
3312
+ ...options.session ?? {}
3313
+ },
3314
+ history: {
3315
+ ...baseOptions.history,
3316
+ ...options.history ?? {}
3317
+ },
3067
3318
  datafeedOptions: {
3068
3319
  ...baseOptions.datafeedOptions,
3069
3320
  ...options.datafeedOptions ?? {}
@@ -3227,6 +3478,45 @@ function createChart(element, options = {}) {
3227
3478
  ...drawing.label === void 0 ? {} : { label: drawing.label }
3228
3479
  });
3229
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 = "";
3230
3520
  let indicators = (options.indicators ?? []).map((indicator) => normalizeIndicatorState(indicator));
3231
3521
  let drawings = dedupeDrawingIds(
3232
3522
  (options.drawings ?? []).map((drawing) => normalizeDrawingState(drawing))
@@ -3275,9 +3565,168 @@ function createChart(element, options = {}) {
3275
3565
  let selectedDrawingId = null;
3276
3566
  const drawingToolDefaults = /* @__PURE__ */ new Map();
3277
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
+ };
3278
3664
  const emitDrawingsChange = () => {
3665
+ commitHistory();
3279
3666
  drawingsChangeHandler?.(drawings.map((drawing) => serializeDrawing(drawing)));
3280
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
+ };
3281
3730
  const orderWidgetWidthById = /* @__PURE__ */ new Map();
3282
3731
  const orderPriceTagWidthById = /* @__PURE__ */ new Map();
3283
3732
  let xCenter = 0;
@@ -3450,6 +3899,9 @@ function createChart(element, options = {}) {
3450
3899
  let overlayLayout = null;
3451
3900
  const margin = { top: 16, right: 72, bottom: 26, left: 12 };
3452
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;
3453
3905
  const minVisibleBars = Math.max(1, Math.floor(mergedOptions.minVisibleBars));
3454
3906
  const maxVisibleBars = Math.max(minVisibleBars, Math.floor(mergedOptions.maxVisibleBars));
3455
3907
  const maxPanBars = Math.max(0, Math.floor(mergedOptions.maxPanBars));
@@ -3777,6 +4229,7 @@ function createChart(element, options = {}) {
3777
4229
  };
3778
4230
  const resetRightMarginCache = () => {
3779
4231
  cachedRightMargin = margin.right;
4232
+ rightMarginShrinkPendingSince = null;
3780
4233
  };
3781
4234
  const parseData = (nextData) => {
3782
4235
  const dedupedByTime = /* @__PURE__ */ new Map();
@@ -4139,40 +4592,13 @@ function createChart(element, options = {}) {
4139
4592
  datafeedUnsubscribe = typeof unsubscribe === "function" ? unsubscribe : null;
4140
4593
  };
4141
4594
  const formatHoverTimeLabel = (time, mode) => {
4142
- if (mode === "time") {
4143
- return time.toLocaleTimeString(void 0, {
4144
- hour: "2-digit",
4145
- minute: "2-digit",
4146
- hour12: false
4147
- });
4148
- }
4149
- if (mode === "datetime") {
4150
- return time.toLocaleString(void 0, {
4151
- month: "short",
4152
- day: "numeric",
4153
- hour: "2-digit",
4154
- minute: "2-digit",
4155
- hour12: false
4156
- });
4157
- }
4158
- if (mode === "date") {
4159
- return time.toLocaleDateString(void 0, {
4160
- month: "short",
4161
- day: "numeric"
4162
- });
4163
- }
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);
4164
4599
  const stepMs = getTimeStepMs();
4165
- if (stepMs < 24 * 60 * 60 * 1e3) {
4166
- return time.toLocaleTimeString(void 0, {
4167
- hour: "2-digit",
4168
- minute: "2-digit",
4169
- hour12: false
4170
- });
4171
- }
4172
- return time.toLocaleDateString(void 0, {
4173
- month: "short",
4174
- day: "numeric"
4175
- });
4600
+ if (stepMs < 24 * 60 * 60 * 1e3) return formatClock(time);
4601
+ return zoneClock.formatDayMonth(ms);
4176
4602
  };
4177
4603
  const drawText = (text, x, y, align = "left", baseline = "alphabetic", color = mergedOptions.axis?.textColor ?? mergedOptions.axisColor) => {
4178
4604
  ctx.fillStyle = color;
@@ -4269,6 +4695,86 @@ function createChart(element, options = {}) {
4269
4695
  labelTextColorOn(mergedLine.labelBackgroundColor, mergedLine.labelTextColor)
4270
4696
  );
4271
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
+ };
4272
4778
  const drawOrderLine = (line, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
4273
4779
  const mergedLine = { ...DEFAULT_ORDER_LINE_OPTIONS, ...line };
4274
4780
  const renderPrice = mergedLine.behavior === "follow" && Number.isFinite(mergedLine.followPrice) ? mergedLine.followPrice : mergedLine.price;
@@ -4553,6 +5059,37 @@ function createChart(element, options = {}) {
4553
5059
  ctx.closePath();
4554
5060
  ctx.stroke();
4555
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
+ };
4556
5093
  const setCrosshairPoint = (point) => {
4557
5094
  const current = crosshairPoint;
4558
5095
  if (point === null && current === null) {
@@ -4600,6 +5137,8 @@ function createChart(element, options = {}) {
4600
5137
  const shouldUpdateAutoScale = options2.updateAutoScale ?? true;
4601
5138
  orderActionRegions = [];
4602
5139
  orderDragRegions = [];
5140
+ alertRegions = [];
5141
+ markRegions = [];
4603
5142
  crosshairPriceActionRegion = null;
4604
5143
  const pixelRatio = getPixelRatio();
4605
5144
  canvas.style.width = `${width}px`;
@@ -4682,7 +5221,23 @@ function createChart(element, options = {}) {
4682
5221
  }
4683
5222
  const maxRightMargin = Math.max(margin.right, width - margin.left - 160);
4684
5223
  const nextRightMargin = Math.min(maxRightMargin, Math.max(margin.right, Math.ceil(required + 1)));
4685
- 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
+ }
4686
5241
  return Math.min(maxRightMargin, cachedRightMargin);
4687
5242
  };
4688
5243
  const rightMargin = estimateRightMargin();
@@ -5855,12 +6410,14 @@ function createChart(element, options = {}) {
5855
6410
  if (!prevTime) {
5856
6411
  weight = 7;
5857
6412
  } else {
5858
- if (prevTime.getFullYear() !== time.getFullYear()) weight = 9;
5859
- else if (prevTime.getMonth() !== time.getMonth()) weight = 8;
5860
- 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;
5861
6418
  else {
5862
- const prevMinutes = prevTime.getHours() * 60 + prevTime.getMinutes();
5863
- const curMinutes = time.getHours() * 60 + time.getMinutes();
6419
+ const prevMinutes = prev.hour * 60 + prev.minute;
6420
+ const curMinutes = current.hour * 60 + current.minute;
5864
6421
  const crossed = (span) => Math.floor(prevMinutes / span) !== Math.floor(curMinutes / span);
5865
6422
  if (crossed(360)) weight = 6;
5866
6423
  else if (crossed(60)) weight = 5;
@@ -5906,31 +6463,64 @@ function createChart(element, options = {}) {
5906
6463
  ctx.stroke();
5907
6464
  ctx.restore();
5908
6465
  }
5909
- 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) {
5910
6503
  const first = Math.max(1, startIndex);
5911
- 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;
5912
- if (barDeltaMs > 0 && barDeltaMs < 0.9 * 864e5) {
5913
- const SESSION_ANCHOR_MS = 22 * 36e5;
5914
- const sessionDayOf = (ms) => Math.floor((ms - SESSION_ANCHOR_MS) / 864e5);
5915
- ctx.save();
5916
- ctx.globalAlpha = clamp(grid.sessionSeparatorOpacity, 0, 1);
5917
- ctx.strokeStyle = grid.sessionSeparatorColor;
5918
- ctx.lineWidth = 1;
5919
- for (let index = first; index <= endIndex; index += 1) {
5920
- const point = data[index];
5921
- const prev = data[index - 1];
5922
- if (!point || !prev) continue;
5923
- if (sessionDayOf(point.time.getTime()) === sessionDayOf(prev.time.getTime())) {
5924
- continue;
5925
- }
5926
- const x = chartLeft + (index - xStart) / xSpan * chartWidth;
5927
- ctx.beginPath();
5928
- ctx.moveTo(crisp(x), crisp(chartTop));
5929
- ctx.lineTo(crisp(x), crisp(fullChartBottom));
5930
- 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;
5931
6516
  }
5932
- 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();
5933
6522
  }
6523
+ ctx.restore();
5934
6524
  }
5935
6525
  const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
5936
6526
  const useSmoothedCandle = tickerOpts.smoothing && smoothedTickerPrice !== null;
@@ -6511,6 +7101,35 @@ function createChart(element, options = {}) {
6511
7101
  ctx.textAlign = prevAlign;
6512
7102
  ctx.textBaseline = prevBaseline;
6513
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
+ }
6514
7133
  ctx.restore();
6515
7134
  const positionAxisGutter = Math.max(0, width - chartRight);
6516
7135
  if (positionAxisGutter > 0) {
@@ -7021,6 +7640,9 @@ function createChart(element, options = {}) {
7021
7640
  for (const priceLine of priceLines) {
7022
7641
  drawPriceLine(priceLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
7023
7642
  }
7643
+ for (const alert of alerts) {
7644
+ drawAlertLine(alert, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
7645
+ }
7024
7646
  for (const orderLine of orderLines) {
7025
7647
  drawOrderLine(orderLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
7026
7648
  }
@@ -7130,11 +7752,36 @@ function createChart(element, options = {}) {
7130
7752
  for (const tick of timeTicks) {
7131
7753
  const tickTime = getTimeForIndex(tick.index);
7132
7754
  if (!tickTime) continue;
7133
- 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);
7134
7757
  drawText(timeLabel, tick.x, (fullChartBottom + height) / 2, "center", "middle", xAxis.textColor);
7135
7758
  }
7136
7759
  ctx.font = prevFont;
7137
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
+ }
7138
7785
  if (labels.visible && labels.showCountdownToBarClose && lastPoint) {
7139
7786
  const stepMs = getTimeStepMs();
7140
7787
  const nowMs = Date.now() + (Number(mergedOptions.clockOffsetMs) || 0);
@@ -7760,6 +8407,22 @@ function createChart(element, options = {}) {
7760
8407
  (region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
7761
8408
  );
7762
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
+ };
7763
8426
  const getCrosshairPriceActionRegion = (x, y) => {
7764
8427
  if (!crosshairPriceActionRegion) {
7765
8428
  return null;
@@ -8919,7 +9582,10 @@ function createChart(element, options = {}) {
8919
9582
  pointerDownInfo = null;
8920
9583
  orderDragState = null;
8921
9584
  actionDragState = null;
8922
- drawingDragState = null;
9585
+ if (drawingDragState) {
9586
+ drawingDragState = null;
9587
+ commitHistory();
9588
+ }
8923
9589
  canvas.style.cursor = "default";
8924
9590
  setCrosshairPoint(null);
8925
9591
  };
@@ -9014,6 +9680,20 @@ function createChart(element, options = {}) {
9014
9680
  });
9015
9681
  return;
9016
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
+ }
9017
9697
  const orderRegion = getOrderActionRegion(point.x, point.y);
9018
9698
  if (orderRegion) {
9019
9699
  if (orderRegion.draggable) {
@@ -9052,6 +9732,28 @@ function createChart(element, options = {}) {
9052
9732
  setCrosshairPoint(null);
9053
9733
  return;
9054
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
+ }
9055
9757
  const paneButtonHit = getPaneButtonHit(point.x, point.y);
9056
9758
  if (paneButtonHit) {
9057
9759
  setCrosshairPoint(null);
@@ -9294,6 +9996,24 @@ function createChart(element, options = {}) {
9294
9996
  setCrosshairPoint(null);
9295
9997
  return;
9296
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
+ }
9297
10017
  if (orderDragState) {
9298
10018
  if (activePointerId !== null && event.pointerId !== activePointerId) {
9299
10019
  return;
@@ -9360,12 +10080,49 @@ function createChart(element, options = {}) {
9360
10080
  setCrosshairPoint(null);
9361
10081
  return;
9362
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
+ }
9363
10108
  const orderDragRegion = getOrderDragRegion(point.x, point.y);
9364
10109
  if (orderDragRegion) {
9365
10110
  canvas.style.cursor = "ns-resize";
9366
10111
  setCrosshairPoint(null);
9367
10112
  return;
9368
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
+ }
9369
10126
  const hoveredPane = getPaneAt(point.x, point.y);
9370
10127
  const nextHoveredPaneId = hoveredPane?.id ?? null;
9371
10128
  if (nextHoveredPaneId !== hoveredPaneId) {
@@ -9523,6 +10280,21 @@ function createChart(element, options = {}) {
9523
10280
  scheduleDraw();
9524
10281
  return;
9525
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
+ }
9526
10298
  if (orderDragState) {
9527
10299
  const moved = orderDragState.lastPrice !== orderDragState.startPrice;
9528
10300
  const finalLine = orderLines.find((line) => line.id === orderDragState?.orderId);
@@ -9569,9 +10341,14 @@ function createChart(element, options = {}) {
9569
10341
  });
9570
10342
  paneDividerDrag = null;
9571
10343
  }
9572
- if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null)) {
10344
+ if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null || hoveredAlertId !== null || hoveredMarkKey !== null)) {
9573
10345
  hoveredPaneId = null;
9574
10346
  hoveredPaneButton = null;
10347
+ hoveredAlertId = null;
10348
+ if (hoveredMarkKey !== null) {
10349
+ hoveredMarkKey = null;
10350
+ markHoverHandler?.(null);
10351
+ }
9575
10352
  if (canvas.title) {
9576
10353
  canvas.title = "";
9577
10354
  }
@@ -9747,9 +10524,6 @@ function createChart(element, options = {}) {
9747
10524
  if (keyboard.enabled === false) {
9748
10525
  return;
9749
10526
  }
9750
- if (event.altKey || event.ctrlKey || event.metaKey) {
9751
- return;
9752
- }
9753
10527
  const panStep = Math.max(1, keyboard.panBars ?? 1) * (event.shiftKey ? 10 : 1);
9754
10528
  const emit = (action, drawing) => {
9755
10529
  keyboardShortcutHandler?.({
@@ -9759,6 +10533,21 @@ function createChart(element, options = {}) {
9759
10533
  ...drawing ? { drawing } : {}
9760
10534
  });
9761
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
+ }
9762
10551
  switch (event.key) {
9763
10552
  case "Delete":
9764
10553
  case "Backspace": {
@@ -9904,6 +10693,20 @@ function createChart(element, options = {}) {
9904
10693
  doubleClickEnabled = mergedOptions.doubleClickEnabled;
9905
10694
  doubleClickAction = mergedOptions.doubleClickAction;
9906
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
+ }
9907
10710
  applyAccessibilityAttributes();
9908
10711
  const isTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
9909
10712
  if (!isTickerSmoothingEnabled) {
@@ -9964,7 +10767,7 @@ function createChart(element, options = {}) {
9964
10767
  resetRightMarginCache();
9965
10768
  scheduleDraw();
9966
10769
  };
9967
- const setData = (nextData) => {
10770
+ const setDataInternal = (nextData) => {
9968
10771
  const hadData = data.length > 0;
9969
10772
  const previousCount = data.length;
9970
10773
  const previousCenterRounded = hadData ? Math.round(xCenter) : 0;
@@ -10006,9 +10809,10 @@ function createChart(element, options = {}) {
10006
10809
  if (lastPoint) {
10007
10810
  pushSmoothedTicker(lastPoint.c, lastPoint.v);
10008
10811
  }
10812
+ evaluateAlertsFromLatestBar();
10009
10813
  scheduleDraw();
10010
10814
  };
10011
- const upsertBar = (point) => {
10815
+ const upsertBarInternal = (point) => {
10012
10816
  const time = new Date(point.t);
10013
10817
  const timeMs = time.getTime();
10014
10818
  const open = Number(point.o);
@@ -10029,7 +10833,7 @@ function createChart(element, options = {}) {
10029
10833
  };
10030
10834
  const last = data[data.length - 1];
10031
10835
  if (!last) {
10032
- setData([point]);
10836
+ setDataInternal([point]);
10033
10837
  return;
10034
10838
  }
10035
10839
  const lastMs = last.time.getTime();
@@ -10051,8 +10855,163 @@ function createChart(element, options = {}) {
10051
10855
  reanchorDrawingsToData();
10052
10856
  }
10053
10857
  pushSmoothedTicker(parsed.c, parsed.v);
10858
+ evaluateAlerts(parsed.c, timeMs, data.length - 1);
10054
10859
  scheduleDraw();
10055
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
+ };
10056
11015
  const setPriceLines = (lines) => {
10057
11016
  priceLines = lines.map((line, index) => ({
10058
11017
  ...line,
@@ -10241,6 +11200,96 @@ function createChart(element, options = {}) {
10241
11200
  tradeMarkers = Array.isArray(markers) ? markers.slice() : [];
10242
11201
  scheduleDraw();
10243
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 });
10244
11293
  const setMagnetMode = (mode) => {
10245
11294
  magnetMode = mode;
10246
11295
  };
@@ -10260,7 +11309,13 @@ function createChart(element, options = {}) {
10260
11309
  drawings = dedupeDrawingIds(nextDrawings.map((drawing) => normalizeDrawingState(drawing)));
10261
11310
  draftDrawing = null;
10262
11311
  reanchorDrawingsToData();
10263
- emitDrawingsChange();
11312
+ historySuspended += 1;
11313
+ try {
11314
+ emitDrawingsChange();
11315
+ } finally {
11316
+ historySuspended -= 1;
11317
+ }
11318
+ resetHistoryBaseline();
10264
11319
  scheduleDraw();
10265
11320
  };
10266
11321
  const addDrawing = (drawing) => {
@@ -10345,7 +11400,10 @@ function createChart(element, options = {}) {
10345
11400
  indicators: getIndicators(),
10346
11401
  magnetMode: getMagnetMode(),
10347
11402
  priceScale: getPriceScale(),
10348
- drawingDefaults
11403
+ drawingDefaults,
11404
+ alerts: getAlerts(),
11405
+ timezone: getTimezone(),
11406
+ timeFormat: getTimeFormat()
10349
11407
  };
10350
11408
  };
10351
11409
  const loadState = (state) => {
@@ -10374,9 +11432,19 @@ function createChart(element, options = {}) {
10374
11432
  }
10375
11433
  }
10376
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
+ }
10377
11444
  if (state.viewport && typeof state.viewport === "object") {
10378
11445
  setViewport(state.viewport);
10379
11446
  }
11447
+ clearHistory();
10380
11448
  };
10381
11449
  const takeScreenshot = (options2 = {}) => {
10382
11450
  return canvas.toDataURL(options2.type ?? "image/png", options2.quality);
@@ -10384,6 +11452,8 @@ function createChart(element, options = {}) {
10384
11452
  const destroy = () => {
10385
11453
  cancelViewportTween();
10386
11454
  cancelLongPressTimer();
11455
+ clearReplayTimer();
11456
+ replay = null;
10387
11457
  datafeedUnsubscribe?.();
10388
11458
  datafeedUnsubscribe = null;
10389
11459
  datafeed = null;
@@ -10417,6 +11487,10 @@ function createChart(element, options = {}) {
10417
11487
  }
10418
11488
  element.innerHTML = "";
10419
11489
  };
11490
+ if (Array.isArray(options.alerts) && options.alerts.length > 0) {
11491
+ setAlerts(options.alerts);
11492
+ }
11493
+ resetHistoryBaseline();
10420
11494
  draw();
10421
11495
  armCountdownHeartbeat();
10422
11496
  return {
@@ -10500,6 +11574,42 @@ function createChart(element, options = {}) {
10500
11574
  saveState,
10501
11575
  loadState,
10502
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,
10503
11613
  resize,
10504
11614
  destroy
10505
11615
  };
@@ -10511,8 +11621,10 @@ function createChart(element, options = {}) {
10511
11621
  FIB_DEFAULT_PALETTE,
10512
11622
  POSITION_DEFAULT_COLORS,
10513
11623
  SCRIPT_SOURCE_INPUT_VALUES,
11624
+ SESSION_PRESETS,
10514
11625
  compileScriptIndicator,
10515
11626
  createChart,
10516
11627
  extractScriptInputs,
11628
+ isValidTimeZone,
10517
11629
  validateScriptSource
10518
11630
  });