hyperprop-charting-library 0.1.141 → 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.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;
@@ -2625,44 +2644,6 @@ var compileScriptIndicator = (definition) => {
2625
2644
 
2626
2645
  // src/themes.ts
2627
2646
  var CHART_THEMES = {
2628
- dark: {
2629
- name: "dark",
2630
- background: "#0f0f0f",
2631
- upColor: "#089981",
2632
- downColor: "#f23645",
2633
- lineColor: "#2962ff",
2634
- gridColor: "#3c3c3c",
2635
- gridOpacity: 0.5,
2636
- axisTextColor: "#dbdbdb",
2637
- axisLineColor: "#424242",
2638
- crosshairColor: "#9598a1",
2639
- crosshairLabelBackground: "#3c3c3c",
2640
- crosshairLabelText: "#dbdbdb",
2641
- crosshairLabelBorder: "#5d5d5d",
2642
- watermarkColor: "rgba(219,219,219,0.06)",
2643
- indicatorTextColor: "#b2b5be",
2644
- labelTextColor: "#ffffff",
2645
- labelBackground: "#1a1a1a"
2646
- },
2647
- light: {
2648
- name: "light",
2649
- background: "#ffffff",
2650
- upColor: "#089981",
2651
- downColor: "#f23645",
2652
- lineColor: "#2962ff",
2653
- gridColor: "#d5d5d5",
2654
- gridOpacity: 0.9,
2655
- axisTextColor: "#0f0f0f",
2656
- axisLineColor: "#c3c3c3",
2657
- crosshairColor: "#787b86",
2658
- crosshairLabelBackground: "#4b4b4b",
2659
- crosshairLabelText: "#ffffff",
2660
- crosshairLabelBorder: "#878787",
2661
- watermarkColor: "rgba(15,15,15,0.06)",
2662
- indicatorTextColor: "#4b4b4b",
2663
- labelTextColor: "#ffffff",
2664
- labelBackground: "#f0f3fa"
2665
- },
2666
2647
  midnight: {
2667
2648
  name: "midnight",
2668
2649
  background: "#171717",
@@ -2682,6 +2663,63 @@ var CHART_THEMES = {
2682
2663
  labelTextColor: "#dbeafe",
2683
2664
  labelBackground: "#0b1220"
2684
2665
  },
2666
+ light: {
2667
+ name: "light",
2668
+ background: "#ffffff",
2669
+ upColor: "#22ab94",
2670
+ downColor: "#f23645",
2671
+ lineColor: "#2563eb",
2672
+ gridColor: "#e0e3eb",
2673
+ gridOpacity: 0.9,
2674
+ axisTextColor: "#1f2430",
2675
+ axisLineColor: "#c8ccd6",
2676
+ crosshairColor: "#6b7280",
2677
+ crosshairLabelBackground: "#2a2e39",
2678
+ crosshairLabelText: "#ffffff",
2679
+ crosshairLabelBorder: "#6b7280",
2680
+ watermarkColor: "rgba(19,23,34,0.06)",
2681
+ indicatorTextColor: "#4b5563",
2682
+ labelTextColor: "#ffffff",
2683
+ labelBackground: "#f1f3f8"
2684
+ },
2685
+ brand: {
2686
+ name: "brand",
2687
+ background: "#0b1220",
2688
+ upColor: "#22ab94",
2689
+ downColor: "#f23645",
2690
+ lineColor: "#253cff",
2691
+ gridColor: "#1b2740",
2692
+ gridOpacity: 0.6,
2693
+ axisTextColor: "#c3cee8",
2694
+ axisLineColor: "#34405c",
2695
+ crosshairColor: "#6e85ff",
2696
+ crosshairLabelBackground: "#253cff",
2697
+ crosshairLabelText: "#ffffff",
2698
+ crosshairLabelBorder: "#6e85ff",
2699
+ watermarkColor: "rgba(37,60,255,0.08)",
2700
+ indicatorTextColor: "#a9b8dc",
2701
+ labelTextColor: "#ffffff",
2702
+ labelBackground: "#101a2e"
2703
+ },
2704
+ dark: {
2705
+ name: "dark",
2706
+ background: "#0f0f0f",
2707
+ upColor: "#089981",
2708
+ downColor: "#f23645",
2709
+ lineColor: "#2962ff",
2710
+ gridColor: "#3c3c3c",
2711
+ gridOpacity: 0.5,
2712
+ axisTextColor: "#dbdbdb",
2713
+ axisLineColor: "#424242",
2714
+ crosshairColor: "#9598a1",
2715
+ crosshairLabelBackground: "#3c3c3c",
2716
+ crosshairLabelText: "#dbdbdb",
2717
+ crosshairLabelBorder: "#5d5d5d",
2718
+ watermarkColor: "rgba(219,219,219,0.06)",
2719
+ indicatorTextColor: "#b2b5be",
2720
+ labelTextColor: "#ffffff",
2721
+ labelBackground: "#1a1a1a"
2722
+ },
2685
2723
  "high-contrast": {
2686
2724
  name: "high-contrast",
2687
2725
  background: "#000000",
@@ -2703,7 +2741,7 @@ var CHART_THEMES = {
2703
2741
  }
2704
2742
  };
2705
2743
  var CHART_THEME_NAMES = Object.keys(CHART_THEMES);
2706
- var resolveTheme = (theme) => typeof theme === "string" ? CHART_THEMES[theme] ?? CHART_THEMES.dark : theme;
2744
+ var resolveTheme = (theme) => typeof theme === "string" ? CHART_THEMES[theme] ?? CHART_THEMES.midnight : theme;
2707
2745
 
2708
2746
  // src/options.ts
2709
2747
  var DEFAULT_GRID_OPTIONS = {
@@ -2905,7 +2943,23 @@ var DEFAULT_OPTIONS = {
2905
2943
  accessibility: { label: "Price chart", description: "", focusable: true },
2906
2944
  downsampling: { enabled: true, thresholdPx: 1.5 },
2907
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
+ },
2908
2960
  datafeedOptions: { prefetchThresholdBars: 150, cooldownMs: 1200, chunkBars: 1500 },
2961
+ animation: { viewportTransitions: true, durationMs: 260 },
2962
+ indicatorUpdate: { liveThrottleMs: 80 },
2909
2963
  crosshair: DEFAULT_CROSSHAIR_OPTIONS,
2910
2964
  grid: DEFAULT_GRID_OPTIONS,
2911
2965
  watermark: DEFAULT_WATERMARK_OPTIONS,
@@ -2964,6 +3018,18 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
2964
3018
  ...baseOptions.touch,
2965
3019
  ...options.touch ?? {}
2966
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
+ },
2967
3033
  datafeedOptions: {
2968
3034
  ...baseOptions.datafeedOptions,
2969
3035
  ...options.datafeedOptions ?? {}
@@ -3223,6 +3289,7 @@ function createChart(element, options = {}) {
3223
3289
  let crosshairPoint = null;
3224
3290
  let doubleClickEnabled = mergedOptions.doubleClickEnabled;
3225
3291
  let doubleClickAction = mergedOptions.doubleClickAction;
3292
+ setIndicatorLiveThrottleMs(mergedOptions.indicatorUpdate?.liveThrottleMs ?? 80);
3226
3293
  let noOverlappingLineLabels = DEFAULT_LABELS_OPTIONS.noOverlapping;
3227
3294
  let rightAxisLabelSlots = [];
3228
3295
  let plotLabelSlots = [];
@@ -3464,21 +3531,69 @@ function createChart(element, options = {}) {
3464
3531
  Math.min(highCenter, count + maxPanBars)
3465
3532
  );
3466
3533
  };
3467
- const fitXViewport = () => {
3468
- const count = data.length;
3469
- if (count === 0) {
3470
- xCenter = 0;
3471
- xSpan = 60;
3472
- 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;
3473
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 };
3474
3586
  const maxSpan = Math.min(maxVisibleBars, Math.max(minVisibleBars, count + maxPanBars * 2));
3475
- const requestedVisibleBars = clamp(Math.floor(mergedOptions.initialVisibleBars), minVisibleBars, maxSpan);
3476
- xSpan = requestedVisibleBars;
3477
- if (mergedOptions.initialViewport === "center") {
3478
- xCenter = count / 2;
3479
- } else {
3480
- xCenter = count - xSpan / 2 + rightEdgePaddingBars;
3481
- }
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;
3482
3597
  clampXViewport();
3483
3598
  };
3484
3599
  const getYBounds = () => {
@@ -6875,6 +6990,73 @@ function createChart(element, options = {}) {
6875
6990
  for (const orderLine of orderLines) {
6876
6991
  drawOrderLine(orderLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
6877
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
+ }
6878
7060
  if (labels.visible && (labels.showIndicatorNames || labels.showIndicatorValues)) {
6879
7061
  const isLegendInputValue = (value) => {
6880
7062
  if (typeof value === "number" || typeof value === "boolean") {
@@ -6903,7 +7085,7 @@ function createChart(element, options = {}) {
6903
7085
  const isRight = position === "top-right" || position === "bottom-right";
6904
7086
  const isBottom = position === "bottom-left" || position === "bottom-right";
6905
7087
  const legendX = isRight ? chartRight - offsetX : chartLeft + offsetX;
6906
- const legendY = isBottom ? chartBottom - offsetY : chartTop + offsetY;
7088
+ const legendY = isBottom ? chartBottom - offsetY : Math.max(chartTop + offsetY, dataLineBottom);
6907
7089
  drawText(legendText, legendX, legendY, isRight ? "right" : "left", isBottom ? "bottom" : "top", labels.indicatorTextColor);
6908
7090
  ctx.font = prevFont;
6909
7091
  }
@@ -7444,27 +7626,24 @@ function createChart(element, options = {}) {
7444
7626
  scheduleDraw();
7445
7627
  };
7446
7628
  const fitContent = () => {
7447
- fitXViewport();
7448
- updateFollowLatest(true);
7449
- emitViewportChange();
7450
- scheduleDraw();
7629
+ const target = getFitXTarget();
7630
+ animateViewportTo(target.center, target.span, () => updateFollowLatest(true));
7451
7631
  };
7452
7632
  const resetViewport = () => {
7453
7633
  cancelKineticPan();
7454
- fitXViewport();
7634
+ const target = getFitXTarget();
7455
7635
  resetYViewport();
7456
- updateFollowLatest(true);
7457
- emitViewportChange();
7458
- scheduleDraw();
7636
+ animateViewportTo(target.center, target.span, () => updateFollowLatest(true));
7459
7637
  };
7460
7638
  const isFollowingLatest = () => followLatest;
7461
7639
  const setFollowingLatest = (follow) => {
7462
7640
  if (follow && data.length > 0) {
7463
7641
  cancelKineticPan();
7464
- xCenter = data.length - xSpan / 2 + rightEdgePaddingBars;
7465
- clampXViewport();
7466
- updateFollowLatest(true);
7467
- scheduleDraw();
7642
+ animateViewportTo(
7643
+ data.length - xSpan / 2 + rightEdgePaddingBars,
7644
+ xSpan,
7645
+ () => updateFollowLatest(true)
7646
+ );
7468
7647
  } else {
7469
7648
  updateFollowLatest(follow);
7470
7649
  }
@@ -8747,6 +8926,7 @@ function createChart(element, options = {}) {
8747
8926
  canvas.focus({ preventScroll: true });
8748
8927
  }
8749
8928
  cancelKineticPan();
8929
+ cancelViewportTween();
8750
8930
  panVelocitySamples.length = 0;
8751
8931
  magnetModifierActive = event.metaKey || event.ctrlKey;
8752
8932
  shiftKeyActive = event.shiftKey;
@@ -9411,6 +9591,7 @@ function createChart(element, options = {}) {
9411
9591
  return;
9412
9592
  }
9413
9593
  cancelKineticPan();
9594
+ cancelViewportTween();
9414
9595
  const point = getCanvasPoint(event);
9415
9596
  const region = getHitRegion(point.x, point.y);
9416
9597
  if (region === "outside") {
@@ -9688,6 +9869,7 @@ function createChart(element, options = {}) {
9688
9869
  resetRightMarginCache();
9689
9870
  doubleClickEnabled = mergedOptions.doubleClickEnabled;
9690
9871
  doubleClickAction = mergedOptions.doubleClickAction;
9872
+ setIndicatorLiveThrottleMs(mergedOptions.indicatorUpdate?.liveThrottleMs ?? 80);
9691
9873
  applyAccessibilityAttributes();
9692
9874
  const isTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
9693
9875
  if (!isTickerSmoothingEnabled) {
@@ -10166,6 +10348,7 @@ function createChart(element, options = {}) {
10166
10348
  return canvas.toDataURL(options2.type ?? "image/png", options2.quality);
10167
10349
  };
10168
10350
  const destroy = () => {
10351
+ cancelViewportTween();
10169
10352
  cancelLongPressTimer();
10170
10353
  datafeedUnsubscribe?.();
10171
10354
  datafeedUnsubscribe = null;
package/docs/API.md CHANGED
@@ -568,6 +568,7 @@ an up measurement, red for down.
568
568
  - `cancelDrawing(): boolean` — abort an in-progress multi-click drawing (between the first click and the final point, e.g. a half-drawn trendline/ray/fib). Returns `true` if a draft was cancelled. Wire it to Escape.
569
569
  - Hold **Shift** while drawing or dragging an endpoint of a `trendline`/`ray`/`arrow` to constrain it to the nearest 0/45/90° angle (e.g. a perfectly horizontal line).
570
570
  - `setMagnetMode(mode): void` / `getMagnetMode()` — magnet/snap for all drawing tools. `"none"` off, `"weak"` snaps to a candle's OHLC only when the cursor is near a value, `"strong"` always snaps to the nearest OHLC of the bar under the cursor. Holding Cmd/Ctrl while drawing/dragging forces strong snapping regardless of the mode.
571
+ - `setTheme(theme: ChartThemeName | ChartTheme): void` / `getTheme(): string | null` — apply a whole palette at once; see "Themes" below
571
572
  - `setDatafeed(datafeed: ChartDatafeed | null): Promise<void>` — attach a pull-based data source with lazy history; see "Datafeed" below
572
573
  - `onLongPress(handler: ((event: ChartLongPressEvent) => void) | null): void` — touch press-and-hold inspection
573
574
  - `setCompareSeries(series: CompareSeriesOptions[]): void` / `addCompareSeries(series)` / `removeCompareSeries(id)` / `getCompareSeries()` — overlay other instruments on the main pane; see "Compare overlays" below
@@ -613,6 +614,97 @@ link.click();
613
614
 
614
615
  ---
615
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
+
675
+ ## Themes
676
+
677
+ A theme is the chart's whole palette as one object, so hosts stop setting a
678
+ dozen colors individually and re-pushing them on every light/dark toggle.
679
+
680
+ ```ts
681
+ import { CHART_THEMES, createChart } from "hyperprop-charting-library";
682
+
683
+ chart.setTheme("light"); // built-in preset
684
+ chart.setTheme({ ...CHART_THEMES.dark, upColor: "#00c853" }); // tweak one
685
+ chart.getTheme(); // "light" | null when hand-set
686
+ ```
687
+
688
+ Five presets ship in `CHART_THEMES`:
689
+
690
+ | Preset | Surface | Candles | For |
691
+ | --- | --- | --- | --- |
692
+ | `midnight` | `#171717` | `#22ab94` / `#f23645` | the default dark look |
693
+ | `light` | `#ffffff` | `#22ab94` / `#f23645` | same candles on a white surface |
694
+ | `brand` | `#0b1220` | `#22ab94` / `#f23645` | Hyperprop blue accents (`#253cff`) |
695
+ | `dark` | `#0f0f0f` | `#089981` / `#f23645` | neutral near-black, no tint |
696
+ | `high-contrast` | `#000000` | `#00e5a0` / `#ff4d5e` | projectors, low vision |
697
+
698
+ `setTheme` covers background, candles, the line/area series, grid (color and
699
+ opacity), both axes, the crosshair and its labels, the watermark, and
700
+ indicator legend text. It deliberately does **not** touch order-line colors or
701
+ drawing defaults — those are trading state, not chrome, so switching looks
702
+ never changes what your orders look like mid-session.
703
+
704
+ `getTheme()` returns the active preset name, or `null` once any themed color
705
+ is set by hand. Unrelated `updateOptions` calls (label toggles, sizing, ticker
706
+ line) don't clear it, so it stays a reliable "am I on a preset" signal.
707
+
616
708
  ## Datafeed (optional, pull-based)
617
709
 
618
710
  By default the host pushes bars in with `setData`/`upsertBar`. Attach a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.141",
3
+ "version": "0.1.143",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",