hyperprop-charting-library 0.1.142 → 0.1.143

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.d.cts CHANGED
@@ -83,6 +83,9 @@ interface ChartOptions {
83
83
  downsampling?: DownsamplingOptions;
84
84
  touch?: TouchOptions;
85
85
  datafeedOptions?: DatafeedOptions;
86
+ dataLine?: DataLineOptions;
87
+ animation?: AnimationOptions;
88
+ indicatorUpdate?: IndicatorUpdateOptions;
86
89
  crosshair?: CrosshairOptions;
87
90
  grid?: GridOptions;
88
91
  watermark?: WatermarkOptions;
@@ -636,6 +639,67 @@ interface ChartTheme {
636
639
  labelBackground: string;
637
640
  }
638
641
  type ChartThemeName = "midnight" | "light" | "brand" | "dark" | "high-contrast";
642
+ /**
643
+ * The symbol/OHLC line drawn inside the top-left of the plot, like
644
+ * TradingView's data line. Values follow the crosshair while hovering and fall
645
+ * back to the latest bar otherwise. Opt-in, because hosts that already render
646
+ * their own DOM overlay shouldn't suddenly get two.
647
+ */
648
+ interface DataLineOptions {
649
+ /** Default false. */
650
+ visible?: boolean;
651
+ /** e.g. "NQU6" — drawn first, in the emphasis color. */
652
+ symbol?: string;
653
+ /** e.g. "1h". */
654
+ interval?: string;
655
+ /** e.g. "CME". */
656
+ exchange?: string;
657
+ /** O/H/L/C values for the hovered (or latest) bar. Default true. */
658
+ showOhlc?: boolean;
659
+ /** Absolute and percent change of that bar, colored by direction. Default true. */
660
+ showChange?: boolean;
661
+ /** Bar volume. Default false. */
662
+ showVolume?: boolean;
663
+ /**
664
+ * Extra chips after the values — session, currency, a "Closed" badge. Each
665
+ * renders as `label value` with an optional color and pill background.
666
+ */
667
+ details?: Array<{
668
+ label?: string;
669
+ value: string;
670
+ color?: string;
671
+ background?: string;
672
+ }>;
673
+ /** Colored dot before the symbol, e.g. green when the market is open. */
674
+ statusColor?: string;
675
+ fontSize?: number;
676
+ /** Symbol color. Defaults to the axis text color. */
677
+ symbolColor?: string;
678
+ /** Everything after the symbol. Defaults to the indicator legend color. */
679
+ textColor?: string;
680
+ }
681
+ interface IndicatorUpdateOptions {
682
+ /**
683
+ * While only the forming bar is ticking, recompute indicator series at most
684
+ * this often (ms) rather than on every frame. Values are always exact — this
685
+ * changes how often the live bar's output refreshes, not how it's computed,
686
+ * and anything structural (a bar closing, history loading, inputs changing)
687
+ * recomputes immediately. Default 80; set 0 to recompute every frame.
688
+ */
689
+ liveThrottleMs?: number;
690
+ }
691
+ interface AnimationOptions {
692
+ /**
693
+ * Ease the big viewport jumps — reset, fit-all, scroll-to-realtime — instead
694
+ * of teleporting, so it stays obvious which way the chart moved. Continuous
695
+ * gestures (drag, wheel, pinch) are never animated; they already follow the
696
+ * hand. Default true, and always skipped when the OS asks for reduced
697
+ * motion.
698
+ */
699
+ viewportTransitions?: boolean;
700
+ /** Transition length in ms. Default 260. */
701
+ durationMs?: number;
702
+ }
639
703
  /** Touch ergonomics. Fingers are blunter than a mouse cursor. */
640
704
  interface TouchOptions {
641
705
  /**
@@ -1078,4 +1142,4 @@ declare const CHART_THEME_NAMES: ChartThemeName[];
1078
1142
 
1079
1143
  declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
1080
1144
 
1081
- export { type AccessibilityOptions, type AxisOptions, type BuiltInIndicatorInfo, CHART_THEMES, CHART_THEME_NAMES, type ChartClickEvent, type ChartContextMenuEvent, type ChartContextMenuRegion, type ChartDatafeed, type ChartInstance, type ChartKeyboardAction, type ChartKeyboardShortcutEvent, type ChartLongPressEvent, type ChartOptions, type ChartSavedState, type ChartTheme, type ChartThemeName, type ChartType, type CompareSeriesOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type DatafeedBarsRequest, type DatafeedOptions, type DownsamplingOptions, type DrawingBounds, type DrawingDefaults, type DrawingHoverEvent, type DrawingObjectOptions, type DrawingPoint, type DrawingSelectEvent, type DrawingSelectionChangeEvent, type DrawingToolType, FIB_DEFAULT_PALETTE, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPaneActionEvent, type IndicatorPaneAxisOptions, type IndicatorPaneGuideLine, type IndicatorPaneHeightChangeEvent, type IndicatorPaneRenderInfo, type IndicatorPaneValue, type IndicatorPaneValueLabel, type IndicatorPlugin, type IndicatorRenderContext, type KeyboardOptions, type LabelsOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, POSITION_DEFAULT_COLORS, type PriceLineOptions, type PriceScaleMode, type PriceScaleOptions, SCRIPT_SOURCE_INPUT_VALUES, type ScriptComputeResult, type ScriptIndicatorDefinition, type ScriptIndicatorInputDef, type ScriptInputHelper, type ScriptInputOptions, type ScriptPlotSpec, type TickerLineOptions, type TouchOptions, type TradeMarkerOptions, type ViewportState, type WatermarkOptions, compileScriptIndicator, createChart, extractScriptInputs, validateScriptSource };
1145
+ export { type AccessibilityOptions, type AnimationOptions, type AxisOptions, type BuiltInIndicatorInfo, CHART_THEMES, CHART_THEME_NAMES, type ChartClickEvent, type ChartContextMenuEvent, type ChartContextMenuRegion, type ChartDatafeed, type ChartInstance, type ChartKeyboardAction, type ChartKeyboardShortcutEvent, type ChartLongPressEvent, type ChartOptions, type ChartSavedState, type ChartTheme, type ChartThemeName, type ChartType, type CompareSeriesOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type DataLineOptions, type DatafeedBarsRequest, type DatafeedOptions, type DownsamplingOptions, type DrawingBounds, type DrawingDefaults, type DrawingHoverEvent, type DrawingObjectOptions, type DrawingPoint, type DrawingSelectEvent, type DrawingSelectionChangeEvent, type DrawingToolType, FIB_DEFAULT_PALETTE, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPaneActionEvent, type IndicatorPaneAxisOptions, type IndicatorPaneGuideLine, type IndicatorPaneHeightChangeEvent, type IndicatorPaneRenderInfo, type IndicatorPaneValue, type IndicatorPaneValueLabel, type IndicatorPlugin, type IndicatorRenderContext, type IndicatorUpdateOptions, type KeyboardOptions, type LabelsOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, POSITION_DEFAULT_COLORS, type PriceLineOptions, type PriceScaleMode, type PriceScaleOptions, SCRIPT_SOURCE_INPUT_VALUES, type ScriptComputeResult, type ScriptIndicatorDefinition, type ScriptIndicatorInputDef, type ScriptInputHelper, type ScriptInputOptions, type ScriptPlotSpec, type TickerLineOptions, type TouchOptions, type TradeMarkerOptions, type ViewportState, type WatermarkOptions, compileScriptIndicator, createChart, extractScriptInputs, validateScriptSource };
package/dist/index.d.ts CHANGED
@@ -83,6 +83,9 @@ interface ChartOptions {
83
83
  downsampling?: DownsamplingOptions;
84
84
  touch?: TouchOptions;
85
85
  datafeedOptions?: DatafeedOptions;
86
+ dataLine?: DataLineOptions;
87
+ animation?: AnimationOptions;
88
+ indicatorUpdate?: IndicatorUpdateOptions;
86
89
  crosshair?: CrosshairOptions;
87
90
  grid?: GridOptions;
88
91
  watermark?: WatermarkOptions;
@@ -636,6 +639,67 @@ interface ChartTheme {
636
639
  labelBackground: string;
637
640
  }
638
641
  type ChartThemeName = "midnight" | "light" | "brand" | "dark" | "high-contrast";
642
+ /**
643
+ * The symbol/OHLC line drawn inside the top-left of the plot, like
644
+ * TradingView's data line. Values follow the crosshair while hovering and fall
645
+ * back to the latest bar otherwise. Opt-in, because hosts that already render
646
+ * their own DOM overlay shouldn't suddenly get two.
647
+ */
648
+ interface DataLineOptions {
649
+ /** Default false. */
650
+ visible?: boolean;
651
+ /** e.g. "NQU6" — drawn first, in the emphasis color. */
652
+ symbol?: string;
653
+ /** e.g. "1h". */
654
+ interval?: string;
655
+ /** e.g. "CME". */
656
+ exchange?: string;
657
+ /** O/H/L/C values for the hovered (or latest) bar. Default true. */
658
+ showOhlc?: boolean;
659
+ /** Absolute and percent change of that bar, colored by direction. Default true. */
660
+ showChange?: boolean;
661
+ /** Bar volume. Default false. */
662
+ showVolume?: boolean;
663
+ /**
664
+ * Extra chips after the values — session, currency, a "Closed" badge. Each
665
+ * renders as `label value` with an optional color and pill background.
666
+ */
667
+ details?: Array<{
668
+ label?: string;
669
+ value: string;
670
+ color?: string;
671
+ background?: string;
672
+ }>;
673
+ /** Colored dot before the symbol, e.g. green when the market is open. */
674
+ statusColor?: string;
675
+ fontSize?: number;
676
+ /** Symbol color. Defaults to the axis text color. */
677
+ symbolColor?: string;
678
+ /** Everything after the symbol. Defaults to the indicator legend color. */
679
+ textColor?: string;
680
+ }
681
+ interface IndicatorUpdateOptions {
682
+ /**
683
+ * While only the forming bar is ticking, recompute indicator series at most
684
+ * this often (ms) rather than on every frame. Values are always exact — this
685
+ * changes how often the live bar's output refreshes, not how it's computed,
686
+ * and anything structural (a bar closing, history loading, inputs changing)
687
+ * recomputes immediately. Default 80; set 0 to recompute every frame.
688
+ */
689
+ liveThrottleMs?: number;
690
+ }
691
+ interface AnimationOptions {
692
+ /**
693
+ * Ease the big viewport jumps — reset, fit-all, scroll-to-realtime — instead
694
+ * of teleporting, so it stays obvious which way the chart moved. Continuous
695
+ * gestures (drag, wheel, pinch) are never animated; they already follow the
696
+ * hand. Default true, and always skipped when the OS asks for reduced
697
+ * motion.
698
+ */
699
+ viewportTransitions?: boolean;
700
+ /** Transition length in ms. Default 260. */
701
+ durationMs?: number;
702
+ }
639
703
  /** Touch ergonomics. Fingers are blunter than a mouse cursor. */
640
704
  interface TouchOptions {
641
705
  /**
@@ -1078,4 +1142,4 @@ declare const CHART_THEME_NAMES: ChartThemeName[];
1078
1142
 
1079
1143
  declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
1080
1144
 
1081
- export { type AccessibilityOptions, type AxisOptions, type BuiltInIndicatorInfo, CHART_THEMES, CHART_THEME_NAMES, type ChartClickEvent, type ChartContextMenuEvent, type ChartContextMenuRegion, type ChartDatafeed, type ChartInstance, type ChartKeyboardAction, type ChartKeyboardShortcutEvent, type ChartLongPressEvent, type ChartOptions, type ChartSavedState, type ChartTheme, type ChartThemeName, type ChartType, type CompareSeriesOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type DatafeedBarsRequest, type DatafeedOptions, type DownsamplingOptions, type DrawingBounds, type DrawingDefaults, type DrawingHoverEvent, type DrawingObjectOptions, type DrawingPoint, type DrawingSelectEvent, type DrawingSelectionChangeEvent, type DrawingToolType, FIB_DEFAULT_PALETTE, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPaneActionEvent, type IndicatorPaneAxisOptions, type IndicatorPaneGuideLine, type IndicatorPaneHeightChangeEvent, type IndicatorPaneRenderInfo, type IndicatorPaneValue, type IndicatorPaneValueLabel, type IndicatorPlugin, type IndicatorRenderContext, type KeyboardOptions, type LabelsOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, POSITION_DEFAULT_COLORS, type PriceLineOptions, type PriceScaleMode, type PriceScaleOptions, SCRIPT_SOURCE_INPUT_VALUES, type ScriptComputeResult, type ScriptIndicatorDefinition, type ScriptIndicatorInputDef, type ScriptInputHelper, type ScriptInputOptions, type ScriptPlotSpec, type TickerLineOptions, type TouchOptions, type TradeMarkerOptions, type ViewportState, type WatermarkOptions, compileScriptIndicator, createChart, extractScriptInputs, validateScriptSource };
1145
+ export { type AccessibilityOptions, type AnimationOptions, type AxisOptions, type BuiltInIndicatorInfo, CHART_THEMES, CHART_THEME_NAMES, type ChartClickEvent, type ChartContextMenuEvent, type ChartContextMenuRegion, type ChartDatafeed, type ChartInstance, type ChartKeyboardAction, type ChartKeyboardShortcutEvent, type ChartLongPressEvent, type ChartOptions, type ChartSavedState, type ChartTheme, type ChartThemeName, type ChartType, type CompareSeriesOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type DataLineOptions, type DatafeedBarsRequest, type DatafeedOptions, type DownsamplingOptions, type DrawingBounds, type DrawingDefaults, type DrawingHoverEvent, type DrawingObjectOptions, type DrawingPoint, type DrawingSelectEvent, type DrawingSelectionChangeEvent, type DrawingToolType, FIB_DEFAULT_PALETTE, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPaneActionEvent, type IndicatorPaneAxisOptions, type IndicatorPaneGuideLine, type IndicatorPaneHeightChangeEvent, type IndicatorPaneRenderInfo, type IndicatorPaneValue, type IndicatorPaneValueLabel, type IndicatorPlugin, type IndicatorRenderContext, type IndicatorUpdateOptions, type KeyboardOptions, type LabelsOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, POSITION_DEFAULT_COLORS, type PriceLineOptions, type PriceScaleMode, type PriceScaleOptions, SCRIPT_SOURCE_INPUT_VALUES, type ScriptComputeResult, type ScriptIndicatorDefinition, type ScriptIndicatorInputDef, type ScriptInputHelper, type ScriptInputOptions, type ScriptPlotSpec, type TickerLineOptions, type TouchOptions, type TradeMarkerOptions, type ViewportState, type WatermarkOptions, compileScriptIndicator, createChart, extractScriptInputs, validateScriptSource };
package/dist/index.js CHANGED
@@ -335,26 +335,45 @@ var getSeriesFingerprint = (data) => {
335
335
  var withCachedSeries = (key, data, compute) => {
336
336
  const fingerprint = getSeriesFingerprint(data);
337
337
  const existing = builtInSeriesCache.get(key);
338
- if (existing && existing.fingerprint === fingerprint) {
339
- return existing.values;
338
+ const shape = getSeriesShape(data);
339
+ if (existing) {
340
+ const unchanged = existing.fingerprint === fingerprint;
341
+ const liveTickWithinThrottle = !unchanged && indicatorLiveThrottleMs > 0 && existing.shape === shape && Date.now() - existing.computedAt < indicatorLiveThrottleMs;
342
+ if (unchanged || liveTickWithinThrottle) {
343
+ return existing.values;
344
+ }
340
345
  }
341
346
  const values = compute();
342
- builtInSeriesCache.set(key, { fingerprint, values });
347
+ builtInSeriesCache.set(key, { fingerprint, values, shape, computedAt: Date.now() });
343
348
  return values;
344
349
  };
345
350
  var builtInComputationCache = /* @__PURE__ */ new Map();
351
+ var indicatorLiveThrottleMs = 80;
352
+ var setIndicatorLiveThrottleMs = (ms) => {
353
+ indicatorLiveThrottleMs = Math.max(0, ms);
354
+ };
355
+ var getSeriesShape = (data) => {
356
+ const length = data.length;
357
+ const last = length > 0 ? data[length - 1] : void 0;
358
+ return last ? `${length}|${last.time.getTime()}` : "empty";
359
+ };
346
360
  var COMPUTATION_CACHE_MAX_ENTRIES = 128;
347
361
  var withCachedComputation = (key, data, compute) => {
348
362
  const fingerprint = getSeriesFingerprint(data);
349
363
  const existing = builtInComputationCache.get(key);
350
- if (existing && existing.fingerprint === fingerprint) {
351
- builtInComputationCache.delete(key);
352
- builtInComputationCache.set(key, existing);
353
- return existing.value;
364
+ const shape = getSeriesShape(data);
365
+ if (existing) {
366
+ const unchanged = existing.fingerprint === fingerprint;
367
+ const liveTickWithinThrottle = !unchanged && indicatorLiveThrottleMs > 0 && existing.shape === shape && Date.now() - existing.computedAt < indicatorLiveThrottleMs;
368
+ if (unchanged || liveTickWithinThrottle) {
369
+ builtInComputationCache.delete(key);
370
+ builtInComputationCache.set(key, existing);
371
+ return existing.value;
372
+ }
354
373
  }
355
374
  const value = compute();
356
375
  builtInComputationCache.delete(key);
357
- builtInComputationCache.set(key, { fingerprint, value });
376
+ builtInComputationCache.set(key, { fingerprint, value, shape, computedAt: Date.now() });
358
377
  while (builtInComputationCache.size > COMPUTATION_CACHE_MAX_ENTRIES) {
359
378
  const oldest = builtInComputationCache.keys().next().value;
360
379
  if (oldest === void 0) break;
@@ -2924,7 +2943,23 @@ var DEFAULT_OPTIONS = {
2924
2943
  accessibility: { label: "Price chart", description: "", focusable: true },
2925
2944
  downsampling: { enabled: true, thresholdPx: 1.5 },
2926
2945
  touch: { hitToleranceScale: 2.2, longPressTooltip: true, longPressMs: 400 },
2946
+ dataLine: {
2947
+ visible: false,
2948
+ symbol: "",
2949
+ interval: "",
2950
+ exchange: "",
2951
+ showOhlc: true,
2952
+ showChange: true,
2953
+ showVolume: false,
2954
+ details: [],
2955
+ statusColor: "",
2956
+ fontSize: 0,
2957
+ symbolColor: "",
2958
+ textColor: ""
2959
+ },
2927
2960
  datafeedOptions: { prefetchThresholdBars: 150, cooldownMs: 1200, chunkBars: 1500 },
2961
+ animation: { viewportTransitions: true, durationMs: 260 },
2962
+ indicatorUpdate: { liveThrottleMs: 80 },
2928
2963
  crosshair: DEFAULT_CROSSHAIR_OPTIONS,
2929
2964
  grid: DEFAULT_GRID_OPTIONS,
2930
2965
  watermark: DEFAULT_WATERMARK_OPTIONS,
@@ -2983,6 +3018,18 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
2983
3018
  ...baseOptions.touch,
2984
3019
  ...options.touch ?? {}
2985
3020
  },
3021
+ dataLine: {
3022
+ ...baseOptions.dataLine,
3023
+ ...options.dataLine ?? {}
3024
+ },
3025
+ animation: {
3026
+ ...baseOptions.animation,
3027
+ ...options.animation ?? {}
3028
+ },
3029
+ indicatorUpdate: {
3030
+ ...baseOptions.indicatorUpdate,
3031
+ ...options.indicatorUpdate ?? {}
3032
+ },
2986
3033
  datafeedOptions: {
2987
3034
  ...baseOptions.datafeedOptions,
2988
3035
  ...options.datafeedOptions ?? {}
@@ -3242,6 +3289,7 @@ function createChart(element, options = {}) {
3242
3289
  let crosshairPoint = null;
3243
3290
  let doubleClickEnabled = mergedOptions.doubleClickEnabled;
3244
3291
  let doubleClickAction = mergedOptions.doubleClickAction;
3292
+ setIndicatorLiveThrottleMs(mergedOptions.indicatorUpdate?.liveThrottleMs ?? 80);
3245
3293
  let noOverlappingLineLabels = DEFAULT_LABELS_OPTIONS.noOverlapping;
3246
3294
  let rightAxisLabelSlots = [];
3247
3295
  let plotLabelSlots = [];
@@ -3483,21 +3531,69 @@ function createChart(element, options = {}) {
3483
3531
  Math.min(highCenter, count + maxPanBars)
3484
3532
  );
3485
3533
  };
3486
- const fitXViewport = () => {
3487
- const count = data.length;
3488
- if (count === 0) {
3489
- xCenter = 0;
3490
- xSpan = 60;
3491
- return;
3534
+ let viewportTweenRaf = null;
3535
+ const cancelViewportTween = () => {
3536
+ if (viewportTweenRaf !== null) {
3537
+ cancelAnimationFrame(viewportTweenRaf);
3538
+ viewportTweenRaf = null;
3539
+ }
3540
+ };
3541
+ const prefersReducedMotion = () => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
3542
+ const animateViewportTo = (targetCenter, targetSpan, onComplete) => {
3543
+ const animation = mergedOptions.animation ?? DEFAULT_OPTIONS.animation;
3544
+ const duration = Math.max(0, animation?.durationMs ?? 260);
3545
+ const startCenter = xCenter;
3546
+ const startSpan = xSpan;
3547
+ const centerDelta = targetCenter - startCenter;
3548
+ const spanRatio = startSpan > 0 ? targetSpan / startSpan : 1;
3549
+ const negligible = Math.abs(centerDelta) < 0.5 && Math.abs(spanRatio - 1) < 0.01;
3550
+ if (animation?.viewportTransitions === false || duration === 0 || negligible || prefersReducedMotion() || typeof requestAnimationFrame !== "function") {
3551
+ xCenter = targetCenter;
3552
+ xSpan = targetSpan;
3553
+ clampXViewport();
3554
+ onComplete?.();
3555
+ emitViewportChange();
3556
+ scheduleDraw();
3557
+ return false;
3492
3558
  }
3559
+ cancelViewportTween();
3560
+ const startedAt = performance.now();
3561
+ const step = (now) => {
3562
+ const t = clamp((now - startedAt) / duration, 0, 1);
3563
+ const eased = 1 - Math.pow(1 - t, 3);
3564
+ xCenter = startCenter + centerDelta * eased;
3565
+ xSpan = startSpan * Math.pow(spanRatio, eased);
3566
+ clampXViewport();
3567
+ scheduleDraw();
3568
+ if (t < 1) {
3569
+ viewportTweenRaf = requestAnimationFrame(step);
3570
+ return;
3571
+ }
3572
+ viewportTweenRaf = null;
3573
+ xCenter = targetCenter;
3574
+ xSpan = targetSpan;
3575
+ clampXViewport();
3576
+ onComplete?.();
3577
+ emitViewportChange();
3578
+ scheduleDraw();
3579
+ };
3580
+ viewportTweenRaf = requestAnimationFrame(step);
3581
+ return true;
3582
+ };
3583
+ const getFitXTarget = () => {
3584
+ const count = data.length;
3585
+ if (count === 0) return { center: 0, span: 60 };
3493
3586
  const maxSpan = Math.min(maxVisibleBars, Math.max(minVisibleBars, count + maxPanBars * 2));
3494
- const requestedVisibleBars = clamp(Math.floor(mergedOptions.initialVisibleBars), minVisibleBars, maxSpan);
3495
- xSpan = requestedVisibleBars;
3496
- if (mergedOptions.initialViewport === "center") {
3497
- xCenter = count / 2;
3498
- } else {
3499
- xCenter = count - xSpan / 2 + rightEdgePaddingBars;
3500
- }
3587
+ const span = clamp(Math.floor(mergedOptions.initialVisibleBars), minVisibleBars, maxSpan);
3588
+ return {
3589
+ center: mergedOptions.initialViewport === "center" ? count / 2 : count - span / 2 + rightEdgePaddingBars,
3590
+ span
3591
+ };
3592
+ };
3593
+ const fitXViewport = () => {
3594
+ const target = getFitXTarget();
3595
+ xSpan = target.span;
3596
+ xCenter = target.center;
3501
3597
  clampXViewport();
3502
3598
  };
3503
3599
  const getYBounds = () => {
@@ -6894,6 +6990,73 @@ function createChart(element, options = {}) {
6894
6990
  for (const orderLine of orderLines) {
6895
6991
  drawOrderLine(orderLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
6896
6992
  }
6993
+ let dataLineBottom = chartTop;
6994
+ const dataLine = mergedOptions.dataLine;
6995
+ if (dataLine?.visible && data.length > 0) {
6996
+ const hoverIndex = crosshairPoint && getHitRegion(crosshairPoint.x, crosshairPoint.y) === "plot" ? indexFromCanvasX(crosshairPoint.x) : null;
6997
+ const barIndex = hoverIndex !== null && hoverIndex >= 0 && hoverIndex < data.length ? hoverIndex : data.length - 1;
6998
+ const bar = data[barIndex];
6999
+ if (bar) {
7000
+ const prevFont = ctx.font;
7001
+ const fontSize = Math.max(9, dataLine.fontSize || axis.fontSize);
7002
+ ctx.font = `${fontSize}px ${mergedOptions.fontFamily}`;
7003
+ ctx.textBaseline = "top";
7004
+ ctx.textAlign = "left";
7005
+ const symbolColor = dataLine.symbolColor || axis.textColor;
7006
+ const textColor = dataLine.textColor || labels.indicatorTextColor;
7007
+ const changeAbs = bar.c - bar.o;
7008
+ const changeColor = changeAbs >= 0 ? mergedOptions.upColor : mergedOptions.downColor;
7009
+ const rowY = chartTop + 6;
7010
+ let cursorX = chartLeft + 10;
7011
+ if (dataLine.statusColor) {
7012
+ const dotRadius = Math.max(2.5, fontSize / 4);
7013
+ ctx.fillStyle = dataLine.statusColor;
7014
+ ctx.beginPath();
7015
+ ctx.arc(cursorX + dotRadius, rowY + fontSize / 2, dotRadius, 0, Math.PI * 2);
7016
+ ctx.fill();
7017
+ cursorX += dotRadius * 2 + 6;
7018
+ }
7019
+ const headline = [dataLine.symbol, dataLine.interval, dataLine.exchange].filter((part) => part && part.length > 0).join(" \xB7 ");
7020
+ if (headline) {
7021
+ ctx.fillStyle = symbolColor;
7022
+ ctx.fillText(headline, cursorX, rowY);
7023
+ cursorX += measureTextWidth(headline) + 10;
7024
+ }
7025
+ if (dataLine.showOhlc !== false) {
7026
+ const ohlc = `O${formatPrice(bar.o)} H${formatPrice(bar.h)} L${formatPrice(bar.l)} C${formatPrice(bar.c)}`;
7027
+ ctx.fillStyle = changeColor;
7028
+ ctx.fillText(ohlc, cursorX, rowY);
7029
+ cursorX += measureTextWidth(ohlc) + 8;
7030
+ }
7031
+ if (dataLine.showChange !== false) {
7032
+ const pct = bar.o !== 0 ? changeAbs / bar.o * 100 : 0;
7033
+ const changeText = `${changeAbs >= 0 ? "+" : ""}${formatPrice(changeAbs)} (${changeAbs >= 0 ? "+" : ""}${pct.toFixed(2)}%)`;
7034
+ ctx.fillStyle = changeColor;
7035
+ ctx.fillText(changeText, cursorX, rowY);
7036
+ cursorX += measureTextWidth(changeText) + 10;
7037
+ }
7038
+ if (dataLine.showVolume && bar.v !== void 0) {
7039
+ const volumeText = `Vol ${bar.v >= 1e6 ? `${(bar.v / 1e6).toFixed(2)}M` : bar.v >= 1e3 ? `${(bar.v / 1e3).toFixed(1)}K` : String(Math.round(bar.v))}`;
7040
+ ctx.fillStyle = textColor;
7041
+ ctx.fillText(volumeText, cursorX, rowY);
7042
+ cursorX += measureTextWidth(volumeText) + 10;
7043
+ }
7044
+ for (const detail of dataLine.details ?? []) {
7045
+ const text = detail.label ? `${detail.label} ${detail.value}` : detail.value;
7046
+ const width2 = measureTextWidth(text);
7047
+ if (cursorX + width2 > chartRight - 4) break;
7048
+ if (detail.background) {
7049
+ ctx.fillStyle = detail.background;
7050
+ fillRoundedRect(cursorX - 4, rowY - 2, width2 + 8, fontSize + 4, 3);
7051
+ }
7052
+ ctx.fillStyle = detail.color || textColor;
7053
+ ctx.fillText(text, cursorX, rowY);
7054
+ cursorX += width2 + 12;
7055
+ }
7056
+ dataLineBottom = rowY + fontSize + 4;
7057
+ ctx.font = prevFont;
7058
+ }
7059
+ }
6897
7060
  if (labels.visible && (labels.showIndicatorNames || labels.showIndicatorValues)) {
6898
7061
  const isLegendInputValue = (value) => {
6899
7062
  if (typeof value === "number" || typeof value === "boolean") {
@@ -6922,7 +7085,7 @@ function createChart(element, options = {}) {
6922
7085
  const isRight = position === "top-right" || position === "bottom-right";
6923
7086
  const isBottom = position === "bottom-left" || position === "bottom-right";
6924
7087
  const legendX = isRight ? chartRight - offsetX : chartLeft + offsetX;
6925
- const legendY = isBottom ? chartBottom - offsetY : chartTop + offsetY;
7088
+ const legendY = isBottom ? chartBottom - offsetY : Math.max(chartTop + offsetY, dataLineBottom);
6926
7089
  drawText(legendText, legendX, legendY, isRight ? "right" : "left", isBottom ? "bottom" : "top", labels.indicatorTextColor);
6927
7090
  ctx.font = prevFont;
6928
7091
  }
@@ -7463,27 +7626,24 @@ function createChart(element, options = {}) {
7463
7626
  scheduleDraw();
7464
7627
  };
7465
7628
  const fitContent = () => {
7466
- fitXViewport();
7467
- updateFollowLatest(true);
7468
- emitViewportChange();
7469
- scheduleDraw();
7629
+ const target = getFitXTarget();
7630
+ animateViewportTo(target.center, target.span, () => updateFollowLatest(true));
7470
7631
  };
7471
7632
  const resetViewport = () => {
7472
7633
  cancelKineticPan();
7473
- fitXViewport();
7634
+ const target = getFitXTarget();
7474
7635
  resetYViewport();
7475
- updateFollowLatest(true);
7476
- emitViewportChange();
7477
- scheduleDraw();
7636
+ animateViewportTo(target.center, target.span, () => updateFollowLatest(true));
7478
7637
  };
7479
7638
  const isFollowingLatest = () => followLatest;
7480
7639
  const setFollowingLatest = (follow) => {
7481
7640
  if (follow && data.length > 0) {
7482
7641
  cancelKineticPan();
7483
- xCenter = data.length - xSpan / 2 + rightEdgePaddingBars;
7484
- clampXViewport();
7485
- updateFollowLatest(true);
7486
- scheduleDraw();
7642
+ animateViewportTo(
7643
+ data.length - xSpan / 2 + rightEdgePaddingBars,
7644
+ xSpan,
7645
+ () => updateFollowLatest(true)
7646
+ );
7487
7647
  } else {
7488
7648
  updateFollowLatest(follow);
7489
7649
  }
@@ -8766,6 +8926,7 @@ function createChart(element, options = {}) {
8766
8926
  canvas.focus({ preventScroll: true });
8767
8927
  }
8768
8928
  cancelKineticPan();
8929
+ cancelViewportTween();
8769
8930
  panVelocitySamples.length = 0;
8770
8931
  magnetModifierActive = event.metaKey || event.ctrlKey;
8771
8932
  shiftKeyActive = event.shiftKey;
@@ -9430,6 +9591,7 @@ function createChart(element, options = {}) {
9430
9591
  return;
9431
9592
  }
9432
9593
  cancelKineticPan();
9594
+ cancelViewportTween();
9433
9595
  const point = getCanvasPoint(event);
9434
9596
  const region = getHitRegion(point.x, point.y);
9435
9597
  if (region === "outside") {
@@ -9707,6 +9869,7 @@ function createChart(element, options = {}) {
9707
9869
  resetRightMarginCache();
9708
9870
  doubleClickEnabled = mergedOptions.doubleClickEnabled;
9709
9871
  doubleClickAction = mergedOptions.doubleClickAction;
9872
+ setIndicatorLiveThrottleMs(mergedOptions.indicatorUpdate?.liveThrottleMs ?? 80);
9710
9873
  applyAccessibilityAttributes();
9711
9874
  const isTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
9712
9875
  if (!isTickerSmoothingEnabled) {
@@ -10185,6 +10348,7 @@ function createChart(element, options = {}) {
10185
10348
  return canvas.toDataURL(options2.type ?? "image/png", options2.quality);
10186
10349
  };
10187
10350
  const destroy = () => {
10351
+ cancelViewportTween();
10188
10352
  cancelLongPressTimer();
10189
10353
  datafeedUnsubscribe?.();
10190
10354
  datafeedUnsubscribe = null;
package/docs/API.md CHANGED
@@ -614,6 +614,64 @@ link.click();
614
614
 
615
615
  ---
616
616
 
617
+ ## In-chart data line
618
+
619
+ The symbol/OHLC line TradingView draws in the top-left of the plot. Hosts have
620
+ been building this in DOM and re-rendering it on every crosshair move; drawn
621
+ in-canvas it just follows the frame, inherits the theme's colors, and costs
622
+ nothing per pointer move.
623
+
624
+ ```ts
625
+ createChart(el, {
626
+ dataLine: {
627
+ visible: true,
628
+ symbol: "NQU6",
629
+ interval: "1h",
630
+ exchange: "CME",
631
+ showVolume: true,
632
+ statusColor: marketOpen ? "#22c55e" : "#ef4444",
633
+ details: [
634
+ { label: "Session", value: "RTH" },
635
+ { value: "Closed", color: "#fff", background: "rgba(242,54,69,0.85)" },
636
+ ],
637
+ },
638
+ });
639
+ ```
640
+
641
+ Values track the hovered bar and fall back to the latest one, the change is
642
+ colored by direction, and the indicator legend moves down to sit under it.
643
+ Detail chips are dropped rather than clipped when the row runs out of width.
644
+ Off by default so hosts with their own overlay don't suddenly render two.
645
+
646
+ ## Viewport transitions
647
+
648
+ The discrete jumps — `resetViewport()`, `fitContent()`,
649
+ `setFollowingLatest(true)` and the `Home`/`End`/`R` shortcuts — ease into place
650
+ over `animation.durationMs` (default 260) instead of teleporting, so it stays
651
+ obvious which way the chart moved. Zoom interpolates geometrically, so each
652
+ frame changes scale by the same ratio rather than appearing to accelerate.
653
+
654
+ Continuous gestures (drag, wheel, pinch) are never animated — they already
655
+ track the input — and any pointer interaction cancels a transition in flight.
656
+ The whole thing is skipped when the OS asks for reduced motion, or with
657
+ `animation: { viewportTransitions: false }`.
658
+
659
+ ## Indicator recomputation
660
+
661
+ Indicator series are memoised per (indicator, inputs, series fingerprint). A
662
+ live tick changes the fingerprint, which used to mean every indicator
663
+ recomputed its whole series on every animation frame — measured at 50k bars
664
+ with 10 indicators, that was ~11ms of the frame budget and dropped the chart to
665
+ 51fps.
666
+
667
+ While *only the forming bar* is ticking, recomputation is now throttled to
668
+ `indicatorUpdate.liveThrottleMs` (default 80ms). The values are always exact —
669
+ this changes how often the live bar's output refreshes, never how it is
670
+ computed — and anything structural (a bar closing, history prepending, inputs
671
+ or length changing) recomputes immediately, so closed bars are never stale.
672
+ Same benchmark after: 8.4ms and 118fps. Set `liveThrottleMs: 0` to recompute on
673
+ every frame.
674
+
617
675
  ## Themes
618
676
 
619
677
  A theme is the chart's whole palette as one object, so hosts stop setting a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.142",
3
+ "version": "0.1.143",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",