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.
@@ -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;