hyperprop-charting-library 0.1.138 → 0.1.139

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.
@@ -2821,6 +2821,7 @@ var DEFAULT_OPTIONS = {
2821
2821
  doubleClickAction: "reset",
2822
2822
  keyboard: { enabled: true, panBars: 1, deleteSelectedDrawing: true },
2823
2823
  accessibility: { label: "Price chart", description: "", focusable: true },
2824
+ downsampling: { enabled: true, thresholdPx: 1.5 },
2824
2825
  crosshair: DEFAULT_CROSSHAIR_OPTIONS,
2825
2826
  grid: DEFAULT_GRID_OPTIONS,
2826
2827
  watermark: DEFAULT_WATERMARK_OPTIONS,
@@ -2871,6 +2872,10 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
2871
2872
  ...baseOptions.accessibility,
2872
2873
  ...options.accessibility ?? {}
2873
2874
  },
2875
+ downsampling: {
2876
+ ...baseOptions.downsampling,
2877
+ ...options.downsampling ?? {}
2878
+ },
2874
2879
  crosshair: {
2875
2880
  ...baseOptions.crosshair,
2876
2881
  ...options.crosshair ?? {}
@@ -3746,6 +3751,64 @@ function createChart(element, options = {}) {
3746
3751
  heikinAshiFingerprint = fingerprint;
3747
3752
  return result;
3748
3753
  };
3754
+ let compareSeriesStates = [];
3755
+ const prepareCompareSeries = (options2) => {
3756
+ const rows = [];
3757
+ for (const point of options2.data ?? []) {
3758
+ const t = new Date(point.t).getTime();
3759
+ const c = Number(point.c);
3760
+ if (Number.isFinite(t) && Number.isFinite(c)) rows.push({ t, c });
3761
+ }
3762
+ rows.sort((a, b) => a.t - b.t);
3763
+ return {
3764
+ options: options2,
3765
+ times: rows.map((row) => row.t),
3766
+ closes: rows.map((row) => row.c),
3767
+ aligned: null,
3768
+ alignedFingerprint: ""
3769
+ };
3770
+ };
3771
+ const mainDataFingerprint = () => {
3772
+ const last = data[data.length - 1];
3773
+ return last ? `${data.length}|${data[0].time.getTime()}|${last.time.getTime()}` : "empty";
3774
+ };
3775
+ const getAlignedCompareValues = (state) => {
3776
+ const fingerprint = `${mainDataFingerprint()}|${state.times.length}|${state.times[state.times.length - 1] ?? 0}`;
3777
+ if (state.aligned && state.alignedFingerprint === fingerprint) {
3778
+ return state.aligned;
3779
+ }
3780
+ const aligned = new Float64Array(data.length).fill(Number.NaN);
3781
+ let cursor = 0;
3782
+ let lastValue = Number.NaN;
3783
+ for (let i = 0; i < data.length; i += 1) {
3784
+ const barTime = data[i].time.getTime();
3785
+ while (cursor < state.times.length && state.times[cursor] <= barTime) {
3786
+ lastValue = state.closes[cursor];
3787
+ cursor += 1;
3788
+ }
3789
+ aligned[i] = lastValue;
3790
+ }
3791
+ state.aligned = aligned;
3792
+ state.alignedFingerprint = fingerprint;
3793
+ return aligned;
3794
+ };
3795
+ const visibleCompareSeries = () => compareSeriesStates.filter((state) => state.options.visible !== false && state.times.length > 0);
3796
+ const setCompareSeries = (series) => {
3797
+ compareSeriesStates = (series ?? []).filter((entry) => entry && entry.id).map(prepareCompareSeries);
3798
+ scheduleDraw({ updateAutoScale: true });
3799
+ };
3800
+ const addCompareSeries = (series) => {
3801
+ if (!series?.id) return;
3802
+ compareSeriesStates = [...compareSeriesStates.filter((state) => state.options.id !== series.id), prepareCompareSeries(series)];
3803
+ scheduleDraw({ updateAutoScale: true });
3804
+ };
3805
+ const removeCompareSeries = (id) => {
3806
+ const next = compareSeriesStates.filter((state) => state.options.id !== id);
3807
+ if (next.length === compareSeriesStates.length) return;
3808
+ compareSeriesStates = next;
3809
+ scheduleDraw({ updateAutoScale: true });
3810
+ };
3811
+ const getCompareSeries = () => compareSeriesStates.map((state) => state.options);
3749
3812
  const formatHoverTimeLabel = (time, mode) => {
3750
3813
  if (mode === "time") {
3751
3814
  return time.toLocaleTimeString(void 0, {
@@ -4351,6 +4414,31 @@ function createChart(element, options = {}) {
4351
4414
  const chartType = mergedOptions.chartType;
4352
4415
  const seriesData = chartType === "heikin-ashi" ? getHeikinAshiData() : data;
4353
4416
  const isLineStyleChart = chartType === "line" || chartType === "area" || chartType === "baseline";
4417
+ const comparePlans = [];
4418
+ for (const state of visibleCompareSeries()) {
4419
+ const aligned = getAlignedCompareValues(state);
4420
+ let anchorIndex = -1;
4421
+ for (let index = startIndex; index <= endIndex; index += 1) {
4422
+ if (Number.isFinite(aligned[index] ?? Number.NaN)) {
4423
+ anchorIndex = index;
4424
+ break;
4425
+ }
4426
+ }
4427
+ if (anchorIndex < 0) continue;
4428
+ const baseCompare = aligned[anchorIndex];
4429
+ const baseMain = seriesData[anchorIndex]?.c ?? 0;
4430
+ const usePercent = (state.options.scale ?? "percent") !== "price";
4431
+ const toPrice = usePercent && baseCompare > 0 ? (value) => baseMain * value / baseCompare : (value) => value;
4432
+ let lastNativeValue = Number.NaN;
4433
+ for (let index = endIndex; index >= startIndex; index -= 1) {
4434
+ const value = aligned[index];
4435
+ if (value !== void 0 && Number.isFinite(value)) {
4436
+ lastNativeValue = value;
4437
+ break;
4438
+ }
4439
+ }
4440
+ comparePlans.push({ state, aligned, toPrice, usePercent, baseCompare, lastNativeValue });
4441
+ }
4354
4442
  const skipLatestIndex = mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1 ? data.length - 1 : -1;
4355
4443
  const scanCandleRange = (from, to) => {
4356
4444
  let min = Number.POSITIVE_INFINITY;
@@ -4396,6 +4484,23 @@ function createChart(element, options = {}) {
4396
4484
  maxPrice = Math.max(maxPrice, weightedMax);
4397
4485
  }
4398
4486
  }
4487
+ for (const plan of comparePlans) {
4488
+ if (plan.state.options.includeInAutoScale === false) continue;
4489
+ let compareMin = Number.POSITIVE_INFINITY;
4490
+ let compareMax = Number.NEGATIVE_INFINITY;
4491
+ for (let index = startIndex; index <= endIndex; index += 1) {
4492
+ const value = plan.aligned[index];
4493
+ if (value === void 0 || !Number.isFinite(value)) continue;
4494
+ const price = plan.toPrice(value);
4495
+ if (!Number.isFinite(price)) continue;
4496
+ if (price < compareMin) compareMin = price;
4497
+ if (price > compareMax) compareMax = price;
4498
+ }
4499
+ if (compareMin <= compareMax) {
4500
+ minPrice = Math.min(minPrice, compareMin);
4501
+ maxPrice = Math.max(maxPrice, compareMax);
4502
+ }
4503
+ }
4399
4504
  const priceRange = maxPrice - minPrice || 1;
4400
4505
  const autoMin = minPrice - priceRange * 0.08;
4401
4506
  const autoMax = maxPrice + priceRange * 0.08;
@@ -5507,7 +5612,106 @@ function createChart(element, options = {}) {
5507
5612
  }
5508
5613
  return data[index]?.v;
5509
5614
  };
5510
- if (isLineStyleChart) {
5615
+ const downsamplingOptions = mergedOptions.downsampling ?? DEFAULT_OPTIONS.downsampling;
5616
+ const downsampleThresholdPx = Math.max(0.2, downsamplingOptions?.thresholdPx ?? 1.5);
5617
+ const shouldDownsample = (downsamplingOptions?.enabled ?? true) && candleSpacing < downsampleThresholdPx && endIndex - startIndex > 2;
5618
+ const seriesColumns = [];
5619
+ if (shouldDownsample) {
5620
+ let current = null;
5621
+ let currentColumn = Number.NaN;
5622
+ for (let index = startIndex; index <= endIndex; index += 1) {
5623
+ const point = seriesData[index];
5624
+ if (!point) continue;
5625
+ const column = Math.round(chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth);
5626
+ if (!current || column !== currentColumn) {
5627
+ currentColumn = column;
5628
+ current = {
5629
+ x: column,
5630
+ open: point.o,
5631
+ high: point.h,
5632
+ low: point.l,
5633
+ close: point.c,
5634
+ minClose: point.c,
5635
+ maxClose: point.c,
5636
+ minCloseIndex: index,
5637
+ maxCloseIndex: index
5638
+ };
5639
+ seriesColumns.push(current);
5640
+ continue;
5641
+ }
5642
+ if (point.h > current.high) current.high = point.h;
5643
+ if (point.l < current.low) current.low = point.l;
5644
+ current.close = point.c;
5645
+ if (point.c < current.minClose) {
5646
+ current.minClose = point.c;
5647
+ current.minCloseIndex = index;
5648
+ }
5649
+ if (point.c > current.maxClose) {
5650
+ current.maxClose = point.c;
5651
+ current.maxCloseIndex = index;
5652
+ }
5653
+ }
5654
+ }
5655
+ if (shouldDownsample && !isLineStyleChart) {
5656
+ const upColumns = new Path2D();
5657
+ const downColumns = new Path2D();
5658
+ for (const column of seriesColumns) {
5659
+ const path = column.close >= column.open ? upColumns : downColumns;
5660
+ const x = column.x + 0.5;
5661
+ const top = crisp(yFromPrice(column.high));
5662
+ const bottom = crisp(yFromPrice(column.low));
5663
+ path.moveTo(x, top);
5664
+ path.lineTo(x, bottom === top ? bottom + 1 : bottom);
5665
+ }
5666
+ ctx.lineWidth = 1;
5667
+ ctx.strokeStyle = mergedOptions.upColor;
5668
+ ctx.stroke(upColumns);
5669
+ ctx.strokeStyle = mergedOptions.downColor;
5670
+ ctx.stroke(downColumns);
5671
+ } else if (shouldDownsample && isLineStyleChart) {
5672
+ const seriesLineWidth = Math.max(1, mergedOptions.lineWidth);
5673
+ const linePath = new Path2D();
5674
+ let started = false;
5675
+ let firstX = 0;
5676
+ let lastX = 0;
5677
+ for (const column of seriesColumns) {
5678
+ const lowFirst = column.minCloseIndex <= column.maxCloseIndex;
5679
+ const first = lowFirst ? column.minClose : column.maxClose;
5680
+ const second = lowFirst ? column.maxClose : column.minClose;
5681
+ const x = column.x;
5682
+ if (!started) {
5683
+ linePath.moveTo(x, yFromPrice(first));
5684
+ firstX = x;
5685
+ started = true;
5686
+ } else {
5687
+ linePath.lineTo(x, yFromPrice(first));
5688
+ }
5689
+ if (second !== first) {
5690
+ linePath.lineTo(x, yFromPrice(second));
5691
+ }
5692
+ lastX = x;
5693
+ }
5694
+ if (started) {
5695
+ if (chartType === "area" || chartType === "baseline") {
5696
+ const baseY = chartType === "baseline" ? yFromPrice(mergedOptions.baselinePrice ?? (yMin + yMax) / 2) : chartBottom;
5697
+ const fillPath = new Path2D(linePath);
5698
+ fillPath.lineTo(lastX, baseY);
5699
+ fillPath.lineTo(firstX, baseY);
5700
+ fillPath.closePath();
5701
+ ctx.save();
5702
+ ctx.globalAlpha = Math.max(0, Math.min(1, mergedOptions.areaFillOpacity));
5703
+ ctx.fillStyle = chartType === "baseline" ? mergedOptions.upColor : mergedOptions.lineColor;
5704
+ ctx.fill(fillPath);
5705
+ ctx.restore();
5706
+ }
5707
+ ctx.save();
5708
+ ctx.strokeStyle = chartType === "baseline" ? mergedOptions.upColor : mergedOptions.lineColor;
5709
+ ctx.lineWidth = seriesLineWidth;
5710
+ ctx.lineJoin = "round";
5711
+ ctx.stroke(linePath);
5712
+ ctx.restore();
5713
+ }
5714
+ } else if (isLineStyleChart) {
5511
5715
  const seriesLineWidth = Math.max(1, mergedOptions.lineWidth);
5512
5716
  const linePath = new Path2D();
5513
5717
  let started = false;
@@ -5673,6 +5877,105 @@ function createChart(element, options = {}) {
5673
5877
  ctx.stroke(downHollowBodies);
5674
5878
  }
5675
5879
  }
5880
+ const compareAxisTags = [];
5881
+ for (const plan of comparePlans) {
5882
+ const color = plan.state.options.color ?? "#f59e0b";
5883
+ const lineWidth = Math.max(1, plan.state.options.lineWidth ?? 1.5);
5884
+ const path = new Path2D();
5885
+ let started = false;
5886
+ let firstX = 0;
5887
+ let lastX = 0;
5888
+ let lastY = 0;
5889
+ let lastPrice = Number.NaN;
5890
+ const emit = (x, y) => {
5891
+ if (!started) {
5892
+ path.moveTo(x, y);
5893
+ firstX = x;
5894
+ started = true;
5895
+ } else {
5896
+ path.lineTo(x, y);
5897
+ }
5898
+ lastX = x;
5899
+ lastY = y;
5900
+ };
5901
+ const xForIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
5902
+ if (shouldDownsample) {
5903
+ let column = Number.NaN;
5904
+ let minValue = 0;
5905
+ let maxValue = 0;
5906
+ let minIndex = 0;
5907
+ let maxIndex = 0;
5908
+ let hasColumn = false;
5909
+ const flushColumn = () => {
5910
+ if (!hasColumn) return;
5911
+ const lowFirst = minIndex <= maxIndex;
5912
+ const first = plan.toPrice(lowFirst ? minValue : maxValue);
5913
+ const second = plan.toPrice(lowFirst ? maxValue : minValue);
5914
+ if (Number.isFinite(first)) emit(column, yFromPrice(first));
5915
+ if (Number.isFinite(second) && second !== first) emit(column, yFromPrice(second));
5916
+ lastPrice = plan.toPrice(maxIndex >= minIndex ? maxValue : minValue);
5917
+ };
5918
+ for (let index = startIndex; index <= endIndex; index += 1) {
5919
+ const value = plan.aligned[index];
5920
+ if (value === void 0 || !Number.isFinite(value)) continue;
5921
+ const nextColumn = Math.round(xForIndex(index));
5922
+ if (!hasColumn || nextColumn !== column) {
5923
+ flushColumn();
5924
+ column = nextColumn;
5925
+ minValue = value;
5926
+ maxValue = value;
5927
+ minIndex = index;
5928
+ maxIndex = index;
5929
+ hasColumn = true;
5930
+ continue;
5931
+ }
5932
+ if (value < minValue) {
5933
+ minValue = value;
5934
+ minIndex = index;
5935
+ }
5936
+ if (value > maxValue) {
5937
+ maxValue = value;
5938
+ maxIndex = index;
5939
+ }
5940
+ }
5941
+ flushColumn();
5942
+ } else {
5943
+ for (let index = startIndex; index <= endIndex; index += 1) {
5944
+ const value = plan.aligned[index];
5945
+ if (value === void 0 || !Number.isFinite(value)) continue;
5946
+ const price = plan.toPrice(value);
5947
+ if (!Number.isFinite(price)) continue;
5948
+ lastPrice = price;
5949
+ emit(xForIndex(index), yFromPrice(price));
5950
+ }
5951
+ }
5952
+ if (!started) continue;
5953
+ if (plan.state.options.style === "area") {
5954
+ const fillPath = new Path2D(path);
5955
+ fillPath.lineTo(lastX, chartBottom);
5956
+ fillPath.lineTo(firstX, chartBottom);
5957
+ fillPath.closePath();
5958
+ ctx.save();
5959
+ ctx.globalAlpha = Math.max(0, Math.min(1, mergedOptions.areaFillOpacity)) * 0.6;
5960
+ ctx.fillStyle = color;
5961
+ ctx.fill(fillPath);
5962
+ ctx.restore();
5963
+ }
5964
+ ctx.save();
5965
+ ctx.strokeStyle = color;
5966
+ ctx.lineWidth = lineWidth;
5967
+ ctx.lineJoin = "round";
5968
+ ctx.stroke(path);
5969
+ ctx.restore();
5970
+ if (Number.isFinite(lastPrice)) {
5971
+ const label = plan.state.options.label ?? plan.state.options.id;
5972
+ const nativeLast = plan.lastNativeValue;
5973
+ const usePercentTag = plan.usePercent && plan.baseCompare > 0 && Number.isFinite(nativeLast);
5974
+ const changePct = usePercentTag ? (nativeLast / plan.baseCompare - 1) * 100 : 0;
5975
+ const shortText = usePercentTag ? `${changePct >= 0 ? "+" : ""}${changePct.toFixed(2)}%` : formatPrice(nativeLast);
5976
+ compareAxisTags.push({ y: lastY, text: `${label} ${shortText}`, shortText, color });
5977
+ }
5978
+ }
5676
5979
  const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
5677
5980
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
5678
5981
  ).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
@@ -6447,6 +6750,35 @@ function createChart(element, options = {}) {
6447
6750
  drawText(countdownText, chartRight + 6, (fullChartBottom + height) / 2, "left", "middle", xAxis.textColor);
6448
6751
  ctx.font = prevFont;
6449
6752
  }
6753
+ if (compareAxisTags.length > 0 && width - chartRight > 0) {
6754
+ const prevFont = ctx.font;
6755
+ const tagFontSize = Math.max(9, (resolvedAxis.fontSize ?? 12) - 2);
6756
+ ctx.font = `${tagFontSize}px ${mergedOptions.fontFamily}`;
6757
+ ctx.textBaseline = "middle";
6758
+ ctx.textAlign = "left";
6759
+ const placed = [];
6760
+ const tagHeight = tagFontSize + 6;
6761
+ const gutterWidth = width - chartRight - 4;
6762
+ for (const tag of [...compareAxisTags].sort((a, b) => a.y - b.y)) {
6763
+ const text = measureTextWidth(tag.text) + 10 <= gutterWidth ? tag.text : tag.shortText;
6764
+ const tagWidth = Math.min(gutterWidth, measureTextWidth(text) + 10);
6765
+ if (tagWidth <= 0) continue;
6766
+ let tagY = clamp(tag.y - tagHeight / 2, chartTop, chartBottom - tagHeight);
6767
+ for (const slot of placed) {
6768
+ if (tagY < slot.bottom + 2 && tagY + tagHeight > slot.top - 2) {
6769
+ tagY = slot.bottom + 2;
6770
+ }
6771
+ }
6772
+ placed.push({ top: tagY, bottom: tagY + tagHeight });
6773
+ ctx.save();
6774
+ ctx.fillStyle = tag.color;
6775
+ fillRoundedRect(chartRight + 2, Math.round(tagY), Math.round(tagWidth), tagHeight, 3);
6776
+ ctx.fillStyle = labelTextColorOn(tag.color, "#ffffff");
6777
+ ctx.fillText(text, chartRight + 7, Math.round(tagY + tagHeight / 2));
6778
+ ctx.restore();
6779
+ }
6780
+ ctx.font = prevFont;
6781
+ }
6450
6782
  if (baseCtx) {
6451
6783
  if (baseCanvas.width !== canvas.width || baseCanvas.height !== canvas.height) {
6452
6784
  baseCanvas.width = canvas.width;
@@ -9548,6 +9880,10 @@ function createChart(element, options = {}) {
9548
9880
  getMagnetMode,
9549
9881
  setPriceScale,
9550
9882
  getPriceScale,
9883
+ setCompareSeries,
9884
+ addCompareSeries,
9885
+ removeCompareSeries,
9886
+ getCompareSeries,
9551
9887
  cancelDrawing,
9552
9888
  setTradeMarkers,
9553
9889
  setSelectedDrawing,