hyperprop-charting-library 0.1.33 → 0.1.35
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/hyperprop-charting-library.cjs +143 -9
- package/dist/hyperprop-charting-library.d.ts +18 -1
- package/dist/hyperprop-charting-library.js +143 -9
- package/dist/index.cjs +143 -9
- package/dist/index.d.cts +18 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.js +143 -9
- package/package.json +1 -1
|
@@ -821,10 +821,21 @@ function createChart(element, options = {}) {
|
|
|
821
821
|
let xSpan = 60;
|
|
822
822
|
let followLatest = true;
|
|
823
823
|
let followingLatestChangeHandler = null;
|
|
824
|
+
let viewportChangeHandler = null;
|
|
825
|
+
let viewportEmitScheduled = false;
|
|
826
|
+
const emitViewportChange = () => {
|
|
827
|
+
if (!viewportChangeHandler || viewportEmitScheduled) return;
|
|
828
|
+
viewportEmitScheduled = true;
|
|
829
|
+
queueMicrotask(() => {
|
|
830
|
+
viewportEmitScheduled = false;
|
|
831
|
+
viewportChangeHandler?.(getViewport());
|
|
832
|
+
});
|
|
833
|
+
};
|
|
824
834
|
const updateFollowLatest = (next) => {
|
|
825
835
|
if (followLatest === next) return;
|
|
826
836
|
followLatest = next;
|
|
827
837
|
followingLatestChangeHandler?.(next);
|
|
838
|
+
emitViewportChange();
|
|
828
839
|
};
|
|
829
840
|
let yMinOverride = null;
|
|
830
841
|
let yMaxOverride = null;
|
|
@@ -841,6 +852,41 @@ function createChart(element, options = {}) {
|
|
|
841
852
|
let crosshairPoint = null;
|
|
842
853
|
let doubleClickEnabled = mergedOptions.doubleClickEnabled;
|
|
843
854
|
let doubleClickAction = mergedOptions.doubleClickAction;
|
|
855
|
+
let smoothedTickerPrice = null;
|
|
856
|
+
let tickerPriceTarget = null;
|
|
857
|
+
let smoothingRafId = null;
|
|
858
|
+
const tickerSmoothingLoop = () => {
|
|
859
|
+
smoothingRafId = null;
|
|
860
|
+
if (smoothedTickerPrice === null || tickerPriceTarget === null) return;
|
|
861
|
+
const diff = tickerPriceTarget - smoothedTickerPrice;
|
|
862
|
+
if (Math.abs(diff) < 1e-9) {
|
|
863
|
+
smoothedTickerPrice = tickerPriceTarget;
|
|
864
|
+
draw();
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
868
|
+
const speed = clamp(tickerOpts.smoothingSpeed ?? 8, 1, 60);
|
|
869
|
+
const dt = 1 / 60;
|
|
870
|
+
const lerp = 1 - Math.exp(-speed * dt);
|
|
871
|
+
smoothedTickerPrice += diff * lerp;
|
|
872
|
+
draw();
|
|
873
|
+
smoothingRafId = requestAnimationFrame(tickerSmoothingLoop);
|
|
874
|
+
};
|
|
875
|
+
const pushSmoothedPrice = (target) => {
|
|
876
|
+
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
877
|
+
if (!tickerOpts.smoothing) {
|
|
878
|
+
smoothedTickerPrice = null;
|
|
879
|
+
tickerPriceTarget = null;
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
tickerPriceTarget = target;
|
|
883
|
+
if (smoothedTickerPrice === null) {
|
|
884
|
+
smoothedTickerPrice = target;
|
|
885
|
+
}
|
|
886
|
+
if (smoothingRafId === null) {
|
|
887
|
+
smoothingRafId = requestAnimationFrame(tickerSmoothingLoop);
|
|
888
|
+
}
|
|
889
|
+
};
|
|
844
890
|
const canvas = document.createElement("canvas");
|
|
845
891
|
const ctx = canvas.getContext("2d");
|
|
846
892
|
if (!ctx) {
|
|
@@ -1801,28 +1847,37 @@ function createChart(element, options = {}) {
|
|
|
1801
1847
|
}
|
|
1802
1848
|
ctx.restore();
|
|
1803
1849
|
}
|
|
1850
|
+
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
1851
|
+
const useSmoothedCandle = tickerOpts.smoothing && smoothedTickerPrice !== null;
|
|
1852
|
+
const lastDataIndex = data.length - 1;
|
|
1804
1853
|
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
1805
1854
|
const point = data[index];
|
|
1806
1855
|
if (!point) {
|
|
1807
1856
|
continue;
|
|
1808
1857
|
}
|
|
1858
|
+
const isLastCandle = useSmoothedCandle && index === lastDataIndex;
|
|
1859
|
+
const displayClose = isLastCandle ? smoothedTickerPrice : point.c;
|
|
1860
|
+
const displayHigh = isLastCandle ? Math.max(point.h, smoothedTickerPrice) : point.h;
|
|
1861
|
+
const displayLow = isLastCandle ? Math.min(point.l, smoothedTickerPrice) : point.l;
|
|
1809
1862
|
const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
1810
1863
|
const openY = yFromPrice(point.o);
|
|
1811
|
-
const closeY = yFromPrice(
|
|
1812
|
-
const highY = yFromPrice(
|
|
1813
|
-
const lowY = yFromPrice(
|
|
1814
|
-
const direction = getCandleDirectionByIndex(index);
|
|
1864
|
+
const closeY = yFromPrice(displayClose);
|
|
1865
|
+
const highY = yFromPrice(displayHigh);
|
|
1866
|
+
const lowY = yFromPrice(displayLow);
|
|
1867
|
+
const direction = isLastCandle ? displayClose >= point.o ? "up" : "down" : getCandleDirectionByIndex(index);
|
|
1815
1868
|
const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
|
|
1869
|
+
const roundedCenterX = Math.round(centerX);
|
|
1816
1870
|
ctx.strokeStyle = candleColor;
|
|
1817
1871
|
ctx.lineWidth = candleWickWidth;
|
|
1818
1872
|
ctx.beginPath();
|
|
1819
|
-
ctx.moveTo(
|
|
1820
|
-
ctx.lineTo(
|
|
1873
|
+
ctx.moveTo(roundedCenterX + 0.5, crisp(highY));
|
|
1874
|
+
ctx.lineTo(roundedCenterX + 0.5, crisp(lowY));
|
|
1821
1875
|
ctx.stroke();
|
|
1876
|
+
const bodyLeft = roundedCenterX - Math.floor(bodyWidth / 2);
|
|
1822
1877
|
const bodyTop = Math.min(openY, closeY);
|
|
1823
1878
|
const bodyHeight = Math.max(1, Math.abs(closeY - openY));
|
|
1824
1879
|
ctx.fillStyle = candleColor;
|
|
1825
|
-
ctx.fillRect(
|
|
1880
|
+
ctx.fillRect(bodyLeft, Math.round(bodyTop), bodyWidth, Math.max(1, Math.round(bodyHeight)));
|
|
1826
1881
|
}
|
|
1827
1882
|
const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
1828
1883
|
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
|
|
@@ -1956,10 +2011,10 @@ function createChart(element, options = {}) {
|
|
|
1956
2011
|
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
1957
2012
|
const lastPoint = data[data.length - 1];
|
|
1958
2013
|
if ((ticker.visible ?? true) && lastPoint) {
|
|
1959
|
-
const tickerPrice = lastPoint.c;
|
|
2014
|
+
const tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
|
|
1960
2015
|
const tickerY = yFromPrice(tickerPrice);
|
|
1961
2016
|
const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
|
|
1962
|
-
const lastDirection = getCandleDirectionByIndex(data.length - 1);
|
|
2017
|
+
const lastDirection = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice >= lastPoint.o ? "up" : "down" : getCandleDirectionByIndex(data.length - 1);
|
|
1963
2018
|
const tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
|
|
1964
2019
|
const tickerThickness = Math.max(1, ticker.thickness ?? 1);
|
|
1965
2020
|
const tickerStyle = ticker.style ?? "solid";
|
|
@@ -2158,6 +2213,7 @@ function createChart(element, options = {}) {
|
|
|
2158
2213
|
xCenter = nextStart + nextSpan / 2;
|
|
2159
2214
|
clampXViewport();
|
|
2160
2215
|
updateFollowLatest(false);
|
|
2216
|
+
emitViewportChange();
|
|
2161
2217
|
draw();
|
|
2162
2218
|
};
|
|
2163
2219
|
const zoomXToLatest = (factor) => {
|
|
@@ -2172,6 +2228,7 @@ function createChart(element, options = {}) {
|
|
|
2172
2228
|
xSpan = nextSpan;
|
|
2173
2229
|
xCenter = nextStart + nextSpan / 2;
|
|
2174
2230
|
clampXViewport();
|
|
2231
|
+
emitViewportChange();
|
|
2175
2232
|
draw();
|
|
2176
2233
|
};
|
|
2177
2234
|
const zoomY = (factor, anchorY) => {
|
|
@@ -2192,6 +2249,7 @@ function createChart(element, options = {}) {
|
|
|
2192
2249
|
const clamped = clampYRange(nextMin, nextMax);
|
|
2193
2250
|
yMinOverride = clamped.min;
|
|
2194
2251
|
yMaxOverride = clamped.max;
|
|
2252
|
+
emitViewportChange();
|
|
2195
2253
|
draw();
|
|
2196
2254
|
};
|
|
2197
2255
|
const pan = (deltaX, deltaY, allowX, allowY) => {
|
|
@@ -2215,6 +2273,9 @@ function createChart(element, options = {}) {
|
|
|
2215
2273
|
yMinOverride = clamped.min;
|
|
2216
2274
|
yMaxOverride = clamped.max;
|
|
2217
2275
|
}
|
|
2276
|
+
if (allowX || allowY) {
|
|
2277
|
+
emitViewportChange();
|
|
2278
|
+
}
|
|
2218
2279
|
draw();
|
|
2219
2280
|
};
|
|
2220
2281
|
const resetYViewport = () => {
|
|
@@ -2254,6 +2315,7 @@ function createChart(element, options = {}) {
|
|
|
2254
2315
|
xCenter += bars;
|
|
2255
2316
|
clampXViewport();
|
|
2256
2317
|
updateFollowLatest(false);
|
|
2318
|
+
emitViewportChange();
|
|
2257
2319
|
draw();
|
|
2258
2320
|
};
|
|
2259
2321
|
const panY = (priceDelta) => {
|
|
@@ -2265,17 +2327,20 @@ function createChart(element, options = {}) {
|
|
|
2265
2327
|
const clamped = clampYRange(currentMin + priceDelta, currentMax + priceDelta);
|
|
2266
2328
|
yMinOverride = clamped.min;
|
|
2267
2329
|
yMaxOverride = clamped.max;
|
|
2330
|
+
emitViewportChange();
|
|
2268
2331
|
draw();
|
|
2269
2332
|
};
|
|
2270
2333
|
const fitContent = () => {
|
|
2271
2334
|
fitXViewport();
|
|
2272
2335
|
updateFollowLatest(true);
|
|
2336
|
+
emitViewportChange();
|
|
2273
2337
|
draw();
|
|
2274
2338
|
};
|
|
2275
2339
|
const resetViewport = () => {
|
|
2276
2340
|
fitXViewport();
|
|
2277
2341
|
resetYViewport();
|
|
2278
2342
|
updateFollowLatest(true);
|
|
2343
|
+
emitViewportChange();
|
|
2279
2344
|
draw();
|
|
2280
2345
|
};
|
|
2281
2346
|
const isFollowingLatest = () => followLatest;
|
|
@@ -2292,6 +2357,64 @@ function createChart(element, options = {}) {
|
|
|
2292
2357
|
const onFollowingLatestChange = (handler) => {
|
|
2293
2358
|
followingLatestChangeHandler = handler;
|
|
2294
2359
|
};
|
|
2360
|
+
const getViewport = () => {
|
|
2361
|
+
let centerTimeMs = null;
|
|
2362
|
+
if (data.length > 0) {
|
|
2363
|
+
const centerRounded = Math.round(xCenter);
|
|
2364
|
+
if (centerRounded >= 0 && centerRounded < data.length) {
|
|
2365
|
+
centerTimeMs = data[centerRounded]?.time.getTime() ?? null;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
return {
|
|
2369
|
+
xSpan,
|
|
2370
|
+
followingLatest: followLatest,
|
|
2371
|
+
centerTimeMs,
|
|
2372
|
+
yMin: yMinOverride,
|
|
2373
|
+
yMax: yMaxOverride
|
|
2374
|
+
};
|
|
2375
|
+
};
|
|
2376
|
+
const setViewport = (viewport) => {
|
|
2377
|
+
let changed = false;
|
|
2378
|
+
if (typeof viewport.xSpan === "number" && Number.isFinite(viewport.xSpan)) {
|
|
2379
|
+
const minSpan = minVisibleBars;
|
|
2380
|
+
const maxSpan = Math.min(maxVisibleBars, Math.max(minSpan, data.length + maxPanBars * 2));
|
|
2381
|
+
const nextSpan = clamp(viewport.xSpan, minSpan, maxSpan);
|
|
2382
|
+
if (nextSpan !== xSpan) {
|
|
2383
|
+
xSpan = nextSpan;
|
|
2384
|
+
changed = true;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
if (typeof viewport.centerTimeMs === "number" && data.length > 0) {
|
|
2388
|
+
const nextCenter = findNearestIndexForTimeMs(viewport.centerTimeMs);
|
|
2389
|
+
if (nextCenter !== null) {
|
|
2390
|
+
xCenter = nextCenter;
|
|
2391
|
+
changed = true;
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
if (viewport.yMin !== void 0) {
|
|
2395
|
+
yMinOverride = viewport.yMin;
|
|
2396
|
+
changed = true;
|
|
2397
|
+
}
|
|
2398
|
+
if (viewport.yMax !== void 0) {
|
|
2399
|
+
yMaxOverride = viewport.yMax;
|
|
2400
|
+
changed = true;
|
|
2401
|
+
}
|
|
2402
|
+
if (typeof viewport.followingLatest === "boolean") {
|
|
2403
|
+
if (followLatest !== viewport.followingLatest) {
|
|
2404
|
+
followLatest = viewport.followingLatest;
|
|
2405
|
+
followingLatestChangeHandler?.(followLatest);
|
|
2406
|
+
changed = true;
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
if (changed) {
|
|
2410
|
+
clampXViewport();
|
|
2411
|
+
draw();
|
|
2412
|
+
emitViewportChange();
|
|
2413
|
+
}
|
|
2414
|
+
};
|
|
2415
|
+
const onViewportChange = (handler) => {
|
|
2416
|
+
viewportChangeHandler = handler;
|
|
2417
|
+
};
|
|
2295
2418
|
const getCanvasPoint = (event) => {
|
|
2296
2419
|
const rect = canvas.getBoundingClientRect();
|
|
2297
2420
|
return {
|
|
@@ -2723,6 +2846,10 @@ function createChart(element, options = {}) {
|
|
|
2723
2846
|
fitXViewport();
|
|
2724
2847
|
}
|
|
2725
2848
|
}
|
|
2849
|
+
const lastClose = data.length > 0 ? data[data.length - 1].c : null;
|
|
2850
|
+
if (lastClose !== null) {
|
|
2851
|
+
pushSmoothedPrice(lastClose);
|
|
2852
|
+
}
|
|
2726
2853
|
draw();
|
|
2727
2854
|
};
|
|
2728
2855
|
const setPriceLines = (lines) => {
|
|
@@ -2873,6 +3000,10 @@ function createChart(element, options = {}) {
|
|
|
2873
3000
|
draw();
|
|
2874
3001
|
};
|
|
2875
3002
|
const destroy = () => {
|
|
3003
|
+
if (smoothingRafId !== null) {
|
|
3004
|
+
cancelAnimationFrame(smoothingRafId);
|
|
3005
|
+
smoothingRafId = null;
|
|
3006
|
+
}
|
|
2876
3007
|
canvas.removeEventListener("pointerdown", onPointerDown);
|
|
2877
3008
|
canvas.removeEventListener("pointermove", onPointerMove);
|
|
2878
3009
|
canvas.removeEventListener("pointerup", endPointerDrag);
|
|
@@ -2907,6 +3038,9 @@ function createChart(element, options = {}) {
|
|
|
2907
3038
|
isFollowingLatest,
|
|
2908
3039
|
setFollowingLatest,
|
|
2909
3040
|
onFollowingLatestChange,
|
|
3041
|
+
getViewport,
|
|
3042
|
+
setViewport,
|
|
3043
|
+
onViewportChange,
|
|
2910
3044
|
setDoubleClickEnabled,
|
|
2911
3045
|
setDoubleClickAction,
|
|
2912
3046
|
registerIndicator,
|
|
@@ -259,6 +259,8 @@ interface TickerLineOptions {
|
|
|
259
259
|
labelBackgroundColor?: string;
|
|
260
260
|
labelTextColor?: string;
|
|
261
261
|
labelBorderRadius?: number;
|
|
262
|
+
smoothing?: boolean;
|
|
263
|
+
smoothingSpeed?: number;
|
|
262
264
|
}
|
|
263
265
|
interface ChartInstance {
|
|
264
266
|
setData: (data: OhlcDataPoint[]) => void;
|
|
@@ -284,6 +286,9 @@ interface ChartInstance {
|
|
|
284
286
|
isFollowingLatest: () => boolean;
|
|
285
287
|
setFollowingLatest: (follow: boolean) => void;
|
|
286
288
|
onFollowingLatestChange: (handler: ((following: boolean) => void) | null) => void;
|
|
289
|
+
getViewport: () => ViewportState;
|
|
290
|
+
setViewport: (viewport: Partial<ViewportState>) => void;
|
|
291
|
+
onViewportChange: (handler: ((viewport: ViewportState) => void) | null) => void;
|
|
287
292
|
setDoubleClickEnabled: (enabled: boolean) => void;
|
|
288
293
|
setDoubleClickAction: (action: "reset" | "placeLimitOrder") => void;
|
|
289
294
|
registerIndicator: (plugin: IndicatorPlugin<any>) => void;
|
|
@@ -305,6 +310,18 @@ interface OhlcDataPoint {
|
|
|
305
310
|
c: number;
|
|
306
311
|
v?: number;
|
|
307
312
|
}
|
|
313
|
+
interface ViewportState {
|
|
314
|
+
/** Number of bars visible horizontally. */
|
|
315
|
+
xSpan: number;
|
|
316
|
+
/** Whether the chart is auto-following the latest candle. */
|
|
317
|
+
followingLatest: boolean;
|
|
318
|
+
/** Timestamp (ms) at the horizontal center of the viewport. null when data is empty. */
|
|
319
|
+
centerTimeMs: number | null;
|
|
320
|
+
/** Manual Y-axis min override (null = auto-scale). */
|
|
321
|
+
yMin: number | null;
|
|
322
|
+
/** Manual Y-axis max override (null = auto-scale). */
|
|
323
|
+
yMax: number | null;
|
|
324
|
+
}
|
|
308
325
|
declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
|
|
309
326
|
|
|
310
|
-
export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPlugin, type IndicatorRenderContext, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };
|
|
327
|
+
export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPlugin, type IndicatorRenderContext, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type ViewportState, type WatermarkOptions, createChart };
|
|
@@ -797,10 +797,21 @@ function createChart(element, options = {}) {
|
|
|
797
797
|
let xSpan = 60;
|
|
798
798
|
let followLatest = true;
|
|
799
799
|
let followingLatestChangeHandler = null;
|
|
800
|
+
let viewportChangeHandler = null;
|
|
801
|
+
let viewportEmitScheduled = false;
|
|
802
|
+
const emitViewportChange = () => {
|
|
803
|
+
if (!viewportChangeHandler || viewportEmitScheduled) return;
|
|
804
|
+
viewportEmitScheduled = true;
|
|
805
|
+
queueMicrotask(() => {
|
|
806
|
+
viewportEmitScheduled = false;
|
|
807
|
+
viewportChangeHandler?.(getViewport());
|
|
808
|
+
});
|
|
809
|
+
};
|
|
800
810
|
const updateFollowLatest = (next) => {
|
|
801
811
|
if (followLatest === next) return;
|
|
802
812
|
followLatest = next;
|
|
803
813
|
followingLatestChangeHandler?.(next);
|
|
814
|
+
emitViewportChange();
|
|
804
815
|
};
|
|
805
816
|
let yMinOverride = null;
|
|
806
817
|
let yMaxOverride = null;
|
|
@@ -817,6 +828,41 @@ function createChart(element, options = {}) {
|
|
|
817
828
|
let crosshairPoint = null;
|
|
818
829
|
let doubleClickEnabled = mergedOptions.doubleClickEnabled;
|
|
819
830
|
let doubleClickAction = mergedOptions.doubleClickAction;
|
|
831
|
+
let smoothedTickerPrice = null;
|
|
832
|
+
let tickerPriceTarget = null;
|
|
833
|
+
let smoothingRafId = null;
|
|
834
|
+
const tickerSmoothingLoop = () => {
|
|
835
|
+
smoothingRafId = null;
|
|
836
|
+
if (smoothedTickerPrice === null || tickerPriceTarget === null) return;
|
|
837
|
+
const diff = tickerPriceTarget - smoothedTickerPrice;
|
|
838
|
+
if (Math.abs(diff) < 1e-9) {
|
|
839
|
+
smoothedTickerPrice = tickerPriceTarget;
|
|
840
|
+
draw();
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
844
|
+
const speed = clamp(tickerOpts.smoothingSpeed ?? 8, 1, 60);
|
|
845
|
+
const dt = 1 / 60;
|
|
846
|
+
const lerp = 1 - Math.exp(-speed * dt);
|
|
847
|
+
smoothedTickerPrice += diff * lerp;
|
|
848
|
+
draw();
|
|
849
|
+
smoothingRafId = requestAnimationFrame(tickerSmoothingLoop);
|
|
850
|
+
};
|
|
851
|
+
const pushSmoothedPrice = (target) => {
|
|
852
|
+
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
853
|
+
if (!tickerOpts.smoothing) {
|
|
854
|
+
smoothedTickerPrice = null;
|
|
855
|
+
tickerPriceTarget = null;
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
tickerPriceTarget = target;
|
|
859
|
+
if (smoothedTickerPrice === null) {
|
|
860
|
+
smoothedTickerPrice = target;
|
|
861
|
+
}
|
|
862
|
+
if (smoothingRafId === null) {
|
|
863
|
+
smoothingRafId = requestAnimationFrame(tickerSmoothingLoop);
|
|
864
|
+
}
|
|
865
|
+
};
|
|
820
866
|
const canvas = document.createElement("canvas");
|
|
821
867
|
const ctx = canvas.getContext("2d");
|
|
822
868
|
if (!ctx) {
|
|
@@ -1777,28 +1823,37 @@ function createChart(element, options = {}) {
|
|
|
1777
1823
|
}
|
|
1778
1824
|
ctx.restore();
|
|
1779
1825
|
}
|
|
1826
|
+
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
1827
|
+
const useSmoothedCandle = tickerOpts.smoothing && smoothedTickerPrice !== null;
|
|
1828
|
+
const lastDataIndex = data.length - 1;
|
|
1780
1829
|
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
1781
1830
|
const point = data[index];
|
|
1782
1831
|
if (!point) {
|
|
1783
1832
|
continue;
|
|
1784
1833
|
}
|
|
1834
|
+
const isLastCandle = useSmoothedCandle && index === lastDataIndex;
|
|
1835
|
+
const displayClose = isLastCandle ? smoothedTickerPrice : point.c;
|
|
1836
|
+
const displayHigh = isLastCandle ? Math.max(point.h, smoothedTickerPrice) : point.h;
|
|
1837
|
+
const displayLow = isLastCandle ? Math.min(point.l, smoothedTickerPrice) : point.l;
|
|
1785
1838
|
const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
1786
1839
|
const openY = yFromPrice(point.o);
|
|
1787
|
-
const closeY = yFromPrice(
|
|
1788
|
-
const highY = yFromPrice(
|
|
1789
|
-
const lowY = yFromPrice(
|
|
1790
|
-
const direction = getCandleDirectionByIndex(index);
|
|
1840
|
+
const closeY = yFromPrice(displayClose);
|
|
1841
|
+
const highY = yFromPrice(displayHigh);
|
|
1842
|
+
const lowY = yFromPrice(displayLow);
|
|
1843
|
+
const direction = isLastCandle ? displayClose >= point.o ? "up" : "down" : getCandleDirectionByIndex(index);
|
|
1791
1844
|
const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
|
|
1845
|
+
const roundedCenterX = Math.round(centerX);
|
|
1792
1846
|
ctx.strokeStyle = candleColor;
|
|
1793
1847
|
ctx.lineWidth = candleWickWidth;
|
|
1794
1848
|
ctx.beginPath();
|
|
1795
|
-
ctx.moveTo(
|
|
1796
|
-
ctx.lineTo(
|
|
1849
|
+
ctx.moveTo(roundedCenterX + 0.5, crisp(highY));
|
|
1850
|
+
ctx.lineTo(roundedCenterX + 0.5, crisp(lowY));
|
|
1797
1851
|
ctx.stroke();
|
|
1852
|
+
const bodyLeft = roundedCenterX - Math.floor(bodyWidth / 2);
|
|
1798
1853
|
const bodyTop = Math.min(openY, closeY);
|
|
1799
1854
|
const bodyHeight = Math.max(1, Math.abs(closeY - openY));
|
|
1800
1855
|
ctx.fillStyle = candleColor;
|
|
1801
|
-
ctx.fillRect(
|
|
1856
|
+
ctx.fillRect(bodyLeft, Math.round(bodyTop), bodyWidth, Math.max(1, Math.round(bodyHeight)));
|
|
1802
1857
|
}
|
|
1803
1858
|
const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
1804
1859
|
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
|
|
@@ -1932,10 +1987,10 @@ function createChart(element, options = {}) {
|
|
|
1932
1987
|
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
1933
1988
|
const lastPoint = data[data.length - 1];
|
|
1934
1989
|
if ((ticker.visible ?? true) && lastPoint) {
|
|
1935
|
-
const tickerPrice = lastPoint.c;
|
|
1990
|
+
const tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
|
|
1936
1991
|
const tickerY = yFromPrice(tickerPrice);
|
|
1937
1992
|
const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
|
|
1938
|
-
const lastDirection = getCandleDirectionByIndex(data.length - 1);
|
|
1993
|
+
const lastDirection = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice >= lastPoint.o ? "up" : "down" : getCandleDirectionByIndex(data.length - 1);
|
|
1939
1994
|
const tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
|
|
1940
1995
|
const tickerThickness = Math.max(1, ticker.thickness ?? 1);
|
|
1941
1996
|
const tickerStyle = ticker.style ?? "solid";
|
|
@@ -2134,6 +2189,7 @@ function createChart(element, options = {}) {
|
|
|
2134
2189
|
xCenter = nextStart + nextSpan / 2;
|
|
2135
2190
|
clampXViewport();
|
|
2136
2191
|
updateFollowLatest(false);
|
|
2192
|
+
emitViewportChange();
|
|
2137
2193
|
draw();
|
|
2138
2194
|
};
|
|
2139
2195
|
const zoomXToLatest = (factor) => {
|
|
@@ -2148,6 +2204,7 @@ function createChart(element, options = {}) {
|
|
|
2148
2204
|
xSpan = nextSpan;
|
|
2149
2205
|
xCenter = nextStart + nextSpan / 2;
|
|
2150
2206
|
clampXViewport();
|
|
2207
|
+
emitViewportChange();
|
|
2151
2208
|
draw();
|
|
2152
2209
|
};
|
|
2153
2210
|
const zoomY = (factor, anchorY) => {
|
|
@@ -2168,6 +2225,7 @@ function createChart(element, options = {}) {
|
|
|
2168
2225
|
const clamped = clampYRange(nextMin, nextMax);
|
|
2169
2226
|
yMinOverride = clamped.min;
|
|
2170
2227
|
yMaxOverride = clamped.max;
|
|
2228
|
+
emitViewportChange();
|
|
2171
2229
|
draw();
|
|
2172
2230
|
};
|
|
2173
2231
|
const pan = (deltaX, deltaY, allowX, allowY) => {
|
|
@@ -2191,6 +2249,9 @@ function createChart(element, options = {}) {
|
|
|
2191
2249
|
yMinOverride = clamped.min;
|
|
2192
2250
|
yMaxOverride = clamped.max;
|
|
2193
2251
|
}
|
|
2252
|
+
if (allowX || allowY) {
|
|
2253
|
+
emitViewportChange();
|
|
2254
|
+
}
|
|
2194
2255
|
draw();
|
|
2195
2256
|
};
|
|
2196
2257
|
const resetYViewport = () => {
|
|
@@ -2230,6 +2291,7 @@ function createChart(element, options = {}) {
|
|
|
2230
2291
|
xCenter += bars;
|
|
2231
2292
|
clampXViewport();
|
|
2232
2293
|
updateFollowLatest(false);
|
|
2294
|
+
emitViewportChange();
|
|
2233
2295
|
draw();
|
|
2234
2296
|
};
|
|
2235
2297
|
const panY = (priceDelta) => {
|
|
@@ -2241,17 +2303,20 @@ function createChart(element, options = {}) {
|
|
|
2241
2303
|
const clamped = clampYRange(currentMin + priceDelta, currentMax + priceDelta);
|
|
2242
2304
|
yMinOverride = clamped.min;
|
|
2243
2305
|
yMaxOverride = clamped.max;
|
|
2306
|
+
emitViewportChange();
|
|
2244
2307
|
draw();
|
|
2245
2308
|
};
|
|
2246
2309
|
const fitContent = () => {
|
|
2247
2310
|
fitXViewport();
|
|
2248
2311
|
updateFollowLatest(true);
|
|
2312
|
+
emitViewportChange();
|
|
2249
2313
|
draw();
|
|
2250
2314
|
};
|
|
2251
2315
|
const resetViewport = () => {
|
|
2252
2316
|
fitXViewport();
|
|
2253
2317
|
resetYViewport();
|
|
2254
2318
|
updateFollowLatest(true);
|
|
2319
|
+
emitViewportChange();
|
|
2255
2320
|
draw();
|
|
2256
2321
|
};
|
|
2257
2322
|
const isFollowingLatest = () => followLatest;
|
|
@@ -2268,6 +2333,64 @@ function createChart(element, options = {}) {
|
|
|
2268
2333
|
const onFollowingLatestChange = (handler) => {
|
|
2269
2334
|
followingLatestChangeHandler = handler;
|
|
2270
2335
|
};
|
|
2336
|
+
const getViewport = () => {
|
|
2337
|
+
let centerTimeMs = null;
|
|
2338
|
+
if (data.length > 0) {
|
|
2339
|
+
const centerRounded = Math.round(xCenter);
|
|
2340
|
+
if (centerRounded >= 0 && centerRounded < data.length) {
|
|
2341
|
+
centerTimeMs = data[centerRounded]?.time.getTime() ?? null;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
return {
|
|
2345
|
+
xSpan,
|
|
2346
|
+
followingLatest: followLatest,
|
|
2347
|
+
centerTimeMs,
|
|
2348
|
+
yMin: yMinOverride,
|
|
2349
|
+
yMax: yMaxOverride
|
|
2350
|
+
};
|
|
2351
|
+
};
|
|
2352
|
+
const setViewport = (viewport) => {
|
|
2353
|
+
let changed = false;
|
|
2354
|
+
if (typeof viewport.xSpan === "number" && Number.isFinite(viewport.xSpan)) {
|
|
2355
|
+
const minSpan = minVisibleBars;
|
|
2356
|
+
const maxSpan = Math.min(maxVisibleBars, Math.max(minSpan, data.length + maxPanBars * 2));
|
|
2357
|
+
const nextSpan = clamp(viewport.xSpan, minSpan, maxSpan);
|
|
2358
|
+
if (nextSpan !== xSpan) {
|
|
2359
|
+
xSpan = nextSpan;
|
|
2360
|
+
changed = true;
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
if (typeof viewport.centerTimeMs === "number" && data.length > 0) {
|
|
2364
|
+
const nextCenter = findNearestIndexForTimeMs(viewport.centerTimeMs);
|
|
2365
|
+
if (nextCenter !== null) {
|
|
2366
|
+
xCenter = nextCenter;
|
|
2367
|
+
changed = true;
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
if (viewport.yMin !== void 0) {
|
|
2371
|
+
yMinOverride = viewport.yMin;
|
|
2372
|
+
changed = true;
|
|
2373
|
+
}
|
|
2374
|
+
if (viewport.yMax !== void 0) {
|
|
2375
|
+
yMaxOverride = viewport.yMax;
|
|
2376
|
+
changed = true;
|
|
2377
|
+
}
|
|
2378
|
+
if (typeof viewport.followingLatest === "boolean") {
|
|
2379
|
+
if (followLatest !== viewport.followingLatest) {
|
|
2380
|
+
followLatest = viewport.followingLatest;
|
|
2381
|
+
followingLatestChangeHandler?.(followLatest);
|
|
2382
|
+
changed = true;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
if (changed) {
|
|
2386
|
+
clampXViewport();
|
|
2387
|
+
draw();
|
|
2388
|
+
emitViewportChange();
|
|
2389
|
+
}
|
|
2390
|
+
};
|
|
2391
|
+
const onViewportChange = (handler) => {
|
|
2392
|
+
viewportChangeHandler = handler;
|
|
2393
|
+
};
|
|
2271
2394
|
const getCanvasPoint = (event) => {
|
|
2272
2395
|
const rect = canvas.getBoundingClientRect();
|
|
2273
2396
|
return {
|
|
@@ -2699,6 +2822,10 @@ function createChart(element, options = {}) {
|
|
|
2699
2822
|
fitXViewport();
|
|
2700
2823
|
}
|
|
2701
2824
|
}
|
|
2825
|
+
const lastClose = data.length > 0 ? data[data.length - 1].c : null;
|
|
2826
|
+
if (lastClose !== null) {
|
|
2827
|
+
pushSmoothedPrice(lastClose);
|
|
2828
|
+
}
|
|
2702
2829
|
draw();
|
|
2703
2830
|
};
|
|
2704
2831
|
const setPriceLines = (lines) => {
|
|
@@ -2849,6 +2976,10 @@ function createChart(element, options = {}) {
|
|
|
2849
2976
|
draw();
|
|
2850
2977
|
};
|
|
2851
2978
|
const destroy = () => {
|
|
2979
|
+
if (smoothingRafId !== null) {
|
|
2980
|
+
cancelAnimationFrame(smoothingRafId);
|
|
2981
|
+
smoothingRafId = null;
|
|
2982
|
+
}
|
|
2852
2983
|
canvas.removeEventListener("pointerdown", onPointerDown);
|
|
2853
2984
|
canvas.removeEventListener("pointermove", onPointerMove);
|
|
2854
2985
|
canvas.removeEventListener("pointerup", endPointerDrag);
|
|
@@ -2883,6 +3014,9 @@ function createChart(element, options = {}) {
|
|
|
2883
3014
|
isFollowingLatest,
|
|
2884
3015
|
setFollowingLatest,
|
|
2885
3016
|
onFollowingLatestChange,
|
|
3017
|
+
getViewport,
|
|
3018
|
+
setViewport,
|
|
3019
|
+
onViewportChange,
|
|
2886
3020
|
setDoubleClickEnabled,
|
|
2887
3021
|
setDoubleClickAction,
|
|
2888
3022
|
registerIndicator,
|
package/dist/index.cjs
CHANGED
|
@@ -821,10 +821,21 @@ function createChart(element, options = {}) {
|
|
|
821
821
|
let xSpan = 60;
|
|
822
822
|
let followLatest = true;
|
|
823
823
|
let followingLatestChangeHandler = null;
|
|
824
|
+
let viewportChangeHandler = null;
|
|
825
|
+
let viewportEmitScheduled = false;
|
|
826
|
+
const emitViewportChange = () => {
|
|
827
|
+
if (!viewportChangeHandler || viewportEmitScheduled) return;
|
|
828
|
+
viewportEmitScheduled = true;
|
|
829
|
+
queueMicrotask(() => {
|
|
830
|
+
viewportEmitScheduled = false;
|
|
831
|
+
viewportChangeHandler?.(getViewport());
|
|
832
|
+
});
|
|
833
|
+
};
|
|
824
834
|
const updateFollowLatest = (next) => {
|
|
825
835
|
if (followLatest === next) return;
|
|
826
836
|
followLatest = next;
|
|
827
837
|
followingLatestChangeHandler?.(next);
|
|
838
|
+
emitViewportChange();
|
|
828
839
|
};
|
|
829
840
|
let yMinOverride = null;
|
|
830
841
|
let yMaxOverride = null;
|
|
@@ -841,6 +852,41 @@ function createChart(element, options = {}) {
|
|
|
841
852
|
let crosshairPoint = null;
|
|
842
853
|
let doubleClickEnabled = mergedOptions.doubleClickEnabled;
|
|
843
854
|
let doubleClickAction = mergedOptions.doubleClickAction;
|
|
855
|
+
let smoothedTickerPrice = null;
|
|
856
|
+
let tickerPriceTarget = null;
|
|
857
|
+
let smoothingRafId = null;
|
|
858
|
+
const tickerSmoothingLoop = () => {
|
|
859
|
+
smoothingRafId = null;
|
|
860
|
+
if (smoothedTickerPrice === null || tickerPriceTarget === null) return;
|
|
861
|
+
const diff = tickerPriceTarget - smoothedTickerPrice;
|
|
862
|
+
if (Math.abs(diff) < 1e-9) {
|
|
863
|
+
smoothedTickerPrice = tickerPriceTarget;
|
|
864
|
+
draw();
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
868
|
+
const speed = clamp(tickerOpts.smoothingSpeed ?? 8, 1, 60);
|
|
869
|
+
const dt = 1 / 60;
|
|
870
|
+
const lerp = 1 - Math.exp(-speed * dt);
|
|
871
|
+
smoothedTickerPrice += diff * lerp;
|
|
872
|
+
draw();
|
|
873
|
+
smoothingRafId = requestAnimationFrame(tickerSmoothingLoop);
|
|
874
|
+
};
|
|
875
|
+
const pushSmoothedPrice = (target) => {
|
|
876
|
+
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
877
|
+
if (!tickerOpts.smoothing) {
|
|
878
|
+
smoothedTickerPrice = null;
|
|
879
|
+
tickerPriceTarget = null;
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
tickerPriceTarget = target;
|
|
883
|
+
if (smoothedTickerPrice === null) {
|
|
884
|
+
smoothedTickerPrice = target;
|
|
885
|
+
}
|
|
886
|
+
if (smoothingRafId === null) {
|
|
887
|
+
smoothingRafId = requestAnimationFrame(tickerSmoothingLoop);
|
|
888
|
+
}
|
|
889
|
+
};
|
|
844
890
|
const canvas = document.createElement("canvas");
|
|
845
891
|
const ctx = canvas.getContext("2d");
|
|
846
892
|
if (!ctx) {
|
|
@@ -1801,28 +1847,37 @@ function createChart(element, options = {}) {
|
|
|
1801
1847
|
}
|
|
1802
1848
|
ctx.restore();
|
|
1803
1849
|
}
|
|
1850
|
+
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
1851
|
+
const useSmoothedCandle = tickerOpts.smoothing && smoothedTickerPrice !== null;
|
|
1852
|
+
const lastDataIndex = data.length - 1;
|
|
1804
1853
|
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
1805
1854
|
const point = data[index];
|
|
1806
1855
|
if (!point) {
|
|
1807
1856
|
continue;
|
|
1808
1857
|
}
|
|
1858
|
+
const isLastCandle = useSmoothedCandle && index === lastDataIndex;
|
|
1859
|
+
const displayClose = isLastCandle ? smoothedTickerPrice : point.c;
|
|
1860
|
+
const displayHigh = isLastCandle ? Math.max(point.h, smoothedTickerPrice) : point.h;
|
|
1861
|
+
const displayLow = isLastCandle ? Math.min(point.l, smoothedTickerPrice) : point.l;
|
|
1809
1862
|
const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
1810
1863
|
const openY = yFromPrice(point.o);
|
|
1811
|
-
const closeY = yFromPrice(
|
|
1812
|
-
const highY = yFromPrice(
|
|
1813
|
-
const lowY = yFromPrice(
|
|
1814
|
-
const direction = getCandleDirectionByIndex(index);
|
|
1864
|
+
const closeY = yFromPrice(displayClose);
|
|
1865
|
+
const highY = yFromPrice(displayHigh);
|
|
1866
|
+
const lowY = yFromPrice(displayLow);
|
|
1867
|
+
const direction = isLastCandle ? displayClose >= point.o ? "up" : "down" : getCandleDirectionByIndex(index);
|
|
1815
1868
|
const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
|
|
1869
|
+
const roundedCenterX = Math.round(centerX);
|
|
1816
1870
|
ctx.strokeStyle = candleColor;
|
|
1817
1871
|
ctx.lineWidth = candleWickWidth;
|
|
1818
1872
|
ctx.beginPath();
|
|
1819
|
-
ctx.moveTo(
|
|
1820
|
-
ctx.lineTo(
|
|
1873
|
+
ctx.moveTo(roundedCenterX + 0.5, crisp(highY));
|
|
1874
|
+
ctx.lineTo(roundedCenterX + 0.5, crisp(lowY));
|
|
1821
1875
|
ctx.stroke();
|
|
1876
|
+
const bodyLeft = roundedCenterX - Math.floor(bodyWidth / 2);
|
|
1822
1877
|
const bodyTop = Math.min(openY, closeY);
|
|
1823
1878
|
const bodyHeight = Math.max(1, Math.abs(closeY - openY));
|
|
1824
1879
|
ctx.fillStyle = candleColor;
|
|
1825
|
-
ctx.fillRect(
|
|
1880
|
+
ctx.fillRect(bodyLeft, Math.round(bodyTop), bodyWidth, Math.max(1, Math.round(bodyHeight)));
|
|
1826
1881
|
}
|
|
1827
1882
|
const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
1828
1883
|
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
|
|
@@ -1956,10 +2011,10 @@ function createChart(element, options = {}) {
|
|
|
1956
2011
|
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
1957
2012
|
const lastPoint = data[data.length - 1];
|
|
1958
2013
|
if ((ticker.visible ?? true) && lastPoint) {
|
|
1959
|
-
const tickerPrice = lastPoint.c;
|
|
2014
|
+
const tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
|
|
1960
2015
|
const tickerY = yFromPrice(tickerPrice);
|
|
1961
2016
|
const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
|
|
1962
|
-
const lastDirection = getCandleDirectionByIndex(data.length - 1);
|
|
2017
|
+
const lastDirection = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice >= lastPoint.o ? "up" : "down" : getCandleDirectionByIndex(data.length - 1);
|
|
1963
2018
|
const tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
|
|
1964
2019
|
const tickerThickness = Math.max(1, ticker.thickness ?? 1);
|
|
1965
2020
|
const tickerStyle = ticker.style ?? "solid";
|
|
@@ -2158,6 +2213,7 @@ function createChart(element, options = {}) {
|
|
|
2158
2213
|
xCenter = nextStart + nextSpan / 2;
|
|
2159
2214
|
clampXViewport();
|
|
2160
2215
|
updateFollowLatest(false);
|
|
2216
|
+
emitViewportChange();
|
|
2161
2217
|
draw();
|
|
2162
2218
|
};
|
|
2163
2219
|
const zoomXToLatest = (factor) => {
|
|
@@ -2172,6 +2228,7 @@ function createChart(element, options = {}) {
|
|
|
2172
2228
|
xSpan = nextSpan;
|
|
2173
2229
|
xCenter = nextStart + nextSpan / 2;
|
|
2174
2230
|
clampXViewport();
|
|
2231
|
+
emitViewportChange();
|
|
2175
2232
|
draw();
|
|
2176
2233
|
};
|
|
2177
2234
|
const zoomY = (factor, anchorY) => {
|
|
@@ -2192,6 +2249,7 @@ function createChart(element, options = {}) {
|
|
|
2192
2249
|
const clamped = clampYRange(nextMin, nextMax);
|
|
2193
2250
|
yMinOverride = clamped.min;
|
|
2194
2251
|
yMaxOverride = clamped.max;
|
|
2252
|
+
emitViewportChange();
|
|
2195
2253
|
draw();
|
|
2196
2254
|
};
|
|
2197
2255
|
const pan = (deltaX, deltaY, allowX, allowY) => {
|
|
@@ -2215,6 +2273,9 @@ function createChart(element, options = {}) {
|
|
|
2215
2273
|
yMinOverride = clamped.min;
|
|
2216
2274
|
yMaxOverride = clamped.max;
|
|
2217
2275
|
}
|
|
2276
|
+
if (allowX || allowY) {
|
|
2277
|
+
emitViewportChange();
|
|
2278
|
+
}
|
|
2218
2279
|
draw();
|
|
2219
2280
|
};
|
|
2220
2281
|
const resetYViewport = () => {
|
|
@@ -2254,6 +2315,7 @@ function createChart(element, options = {}) {
|
|
|
2254
2315
|
xCenter += bars;
|
|
2255
2316
|
clampXViewport();
|
|
2256
2317
|
updateFollowLatest(false);
|
|
2318
|
+
emitViewportChange();
|
|
2257
2319
|
draw();
|
|
2258
2320
|
};
|
|
2259
2321
|
const panY = (priceDelta) => {
|
|
@@ -2265,17 +2327,20 @@ function createChart(element, options = {}) {
|
|
|
2265
2327
|
const clamped = clampYRange(currentMin + priceDelta, currentMax + priceDelta);
|
|
2266
2328
|
yMinOverride = clamped.min;
|
|
2267
2329
|
yMaxOverride = clamped.max;
|
|
2330
|
+
emitViewportChange();
|
|
2268
2331
|
draw();
|
|
2269
2332
|
};
|
|
2270
2333
|
const fitContent = () => {
|
|
2271
2334
|
fitXViewport();
|
|
2272
2335
|
updateFollowLatest(true);
|
|
2336
|
+
emitViewportChange();
|
|
2273
2337
|
draw();
|
|
2274
2338
|
};
|
|
2275
2339
|
const resetViewport = () => {
|
|
2276
2340
|
fitXViewport();
|
|
2277
2341
|
resetYViewport();
|
|
2278
2342
|
updateFollowLatest(true);
|
|
2343
|
+
emitViewportChange();
|
|
2279
2344
|
draw();
|
|
2280
2345
|
};
|
|
2281
2346
|
const isFollowingLatest = () => followLatest;
|
|
@@ -2292,6 +2357,64 @@ function createChart(element, options = {}) {
|
|
|
2292
2357
|
const onFollowingLatestChange = (handler) => {
|
|
2293
2358
|
followingLatestChangeHandler = handler;
|
|
2294
2359
|
};
|
|
2360
|
+
const getViewport = () => {
|
|
2361
|
+
let centerTimeMs = null;
|
|
2362
|
+
if (data.length > 0) {
|
|
2363
|
+
const centerRounded = Math.round(xCenter);
|
|
2364
|
+
if (centerRounded >= 0 && centerRounded < data.length) {
|
|
2365
|
+
centerTimeMs = data[centerRounded]?.time.getTime() ?? null;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
return {
|
|
2369
|
+
xSpan,
|
|
2370
|
+
followingLatest: followLatest,
|
|
2371
|
+
centerTimeMs,
|
|
2372
|
+
yMin: yMinOverride,
|
|
2373
|
+
yMax: yMaxOverride
|
|
2374
|
+
};
|
|
2375
|
+
};
|
|
2376
|
+
const setViewport = (viewport) => {
|
|
2377
|
+
let changed = false;
|
|
2378
|
+
if (typeof viewport.xSpan === "number" && Number.isFinite(viewport.xSpan)) {
|
|
2379
|
+
const minSpan = minVisibleBars;
|
|
2380
|
+
const maxSpan = Math.min(maxVisibleBars, Math.max(minSpan, data.length + maxPanBars * 2));
|
|
2381
|
+
const nextSpan = clamp(viewport.xSpan, minSpan, maxSpan);
|
|
2382
|
+
if (nextSpan !== xSpan) {
|
|
2383
|
+
xSpan = nextSpan;
|
|
2384
|
+
changed = true;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
if (typeof viewport.centerTimeMs === "number" && data.length > 0) {
|
|
2388
|
+
const nextCenter = findNearestIndexForTimeMs(viewport.centerTimeMs);
|
|
2389
|
+
if (nextCenter !== null) {
|
|
2390
|
+
xCenter = nextCenter;
|
|
2391
|
+
changed = true;
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
if (viewport.yMin !== void 0) {
|
|
2395
|
+
yMinOverride = viewport.yMin;
|
|
2396
|
+
changed = true;
|
|
2397
|
+
}
|
|
2398
|
+
if (viewport.yMax !== void 0) {
|
|
2399
|
+
yMaxOverride = viewport.yMax;
|
|
2400
|
+
changed = true;
|
|
2401
|
+
}
|
|
2402
|
+
if (typeof viewport.followingLatest === "boolean") {
|
|
2403
|
+
if (followLatest !== viewport.followingLatest) {
|
|
2404
|
+
followLatest = viewport.followingLatest;
|
|
2405
|
+
followingLatestChangeHandler?.(followLatest);
|
|
2406
|
+
changed = true;
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
if (changed) {
|
|
2410
|
+
clampXViewport();
|
|
2411
|
+
draw();
|
|
2412
|
+
emitViewportChange();
|
|
2413
|
+
}
|
|
2414
|
+
};
|
|
2415
|
+
const onViewportChange = (handler) => {
|
|
2416
|
+
viewportChangeHandler = handler;
|
|
2417
|
+
};
|
|
2295
2418
|
const getCanvasPoint = (event) => {
|
|
2296
2419
|
const rect = canvas.getBoundingClientRect();
|
|
2297
2420
|
return {
|
|
@@ -2723,6 +2846,10 @@ function createChart(element, options = {}) {
|
|
|
2723
2846
|
fitXViewport();
|
|
2724
2847
|
}
|
|
2725
2848
|
}
|
|
2849
|
+
const lastClose = data.length > 0 ? data[data.length - 1].c : null;
|
|
2850
|
+
if (lastClose !== null) {
|
|
2851
|
+
pushSmoothedPrice(lastClose);
|
|
2852
|
+
}
|
|
2726
2853
|
draw();
|
|
2727
2854
|
};
|
|
2728
2855
|
const setPriceLines = (lines) => {
|
|
@@ -2873,6 +3000,10 @@ function createChart(element, options = {}) {
|
|
|
2873
3000
|
draw();
|
|
2874
3001
|
};
|
|
2875
3002
|
const destroy = () => {
|
|
3003
|
+
if (smoothingRafId !== null) {
|
|
3004
|
+
cancelAnimationFrame(smoothingRafId);
|
|
3005
|
+
smoothingRafId = null;
|
|
3006
|
+
}
|
|
2876
3007
|
canvas.removeEventListener("pointerdown", onPointerDown);
|
|
2877
3008
|
canvas.removeEventListener("pointermove", onPointerMove);
|
|
2878
3009
|
canvas.removeEventListener("pointerup", endPointerDrag);
|
|
@@ -2907,6 +3038,9 @@ function createChart(element, options = {}) {
|
|
|
2907
3038
|
isFollowingLatest,
|
|
2908
3039
|
setFollowingLatest,
|
|
2909
3040
|
onFollowingLatestChange,
|
|
3041
|
+
getViewport,
|
|
3042
|
+
setViewport,
|
|
3043
|
+
onViewportChange,
|
|
2910
3044
|
setDoubleClickEnabled,
|
|
2911
3045
|
setDoubleClickAction,
|
|
2912
3046
|
registerIndicator,
|
package/dist/index.d.cts
CHANGED
|
@@ -259,6 +259,8 @@ interface TickerLineOptions {
|
|
|
259
259
|
labelBackgroundColor?: string;
|
|
260
260
|
labelTextColor?: string;
|
|
261
261
|
labelBorderRadius?: number;
|
|
262
|
+
smoothing?: boolean;
|
|
263
|
+
smoothingSpeed?: number;
|
|
262
264
|
}
|
|
263
265
|
interface ChartInstance {
|
|
264
266
|
setData: (data: OhlcDataPoint[]) => void;
|
|
@@ -284,6 +286,9 @@ interface ChartInstance {
|
|
|
284
286
|
isFollowingLatest: () => boolean;
|
|
285
287
|
setFollowingLatest: (follow: boolean) => void;
|
|
286
288
|
onFollowingLatestChange: (handler: ((following: boolean) => void) | null) => void;
|
|
289
|
+
getViewport: () => ViewportState;
|
|
290
|
+
setViewport: (viewport: Partial<ViewportState>) => void;
|
|
291
|
+
onViewportChange: (handler: ((viewport: ViewportState) => void) | null) => void;
|
|
287
292
|
setDoubleClickEnabled: (enabled: boolean) => void;
|
|
288
293
|
setDoubleClickAction: (action: "reset" | "placeLimitOrder") => void;
|
|
289
294
|
registerIndicator: (plugin: IndicatorPlugin<any>) => void;
|
|
@@ -305,6 +310,18 @@ interface OhlcDataPoint {
|
|
|
305
310
|
c: number;
|
|
306
311
|
v?: number;
|
|
307
312
|
}
|
|
313
|
+
interface ViewportState {
|
|
314
|
+
/** Number of bars visible horizontally. */
|
|
315
|
+
xSpan: number;
|
|
316
|
+
/** Whether the chart is auto-following the latest candle. */
|
|
317
|
+
followingLatest: boolean;
|
|
318
|
+
/** Timestamp (ms) at the horizontal center of the viewport. null when data is empty. */
|
|
319
|
+
centerTimeMs: number | null;
|
|
320
|
+
/** Manual Y-axis min override (null = auto-scale). */
|
|
321
|
+
yMin: number | null;
|
|
322
|
+
/** Manual Y-axis max override (null = auto-scale). */
|
|
323
|
+
yMax: number | null;
|
|
324
|
+
}
|
|
308
325
|
declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
|
|
309
326
|
|
|
310
|
-
export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPlugin, type IndicatorRenderContext, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };
|
|
327
|
+
export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPlugin, type IndicatorRenderContext, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type ViewportState, type WatermarkOptions, createChart };
|
package/dist/index.d.ts
CHANGED
|
@@ -259,6 +259,8 @@ interface TickerLineOptions {
|
|
|
259
259
|
labelBackgroundColor?: string;
|
|
260
260
|
labelTextColor?: string;
|
|
261
261
|
labelBorderRadius?: number;
|
|
262
|
+
smoothing?: boolean;
|
|
263
|
+
smoothingSpeed?: number;
|
|
262
264
|
}
|
|
263
265
|
interface ChartInstance {
|
|
264
266
|
setData: (data: OhlcDataPoint[]) => void;
|
|
@@ -284,6 +286,9 @@ interface ChartInstance {
|
|
|
284
286
|
isFollowingLatest: () => boolean;
|
|
285
287
|
setFollowingLatest: (follow: boolean) => void;
|
|
286
288
|
onFollowingLatestChange: (handler: ((following: boolean) => void) | null) => void;
|
|
289
|
+
getViewport: () => ViewportState;
|
|
290
|
+
setViewport: (viewport: Partial<ViewportState>) => void;
|
|
291
|
+
onViewportChange: (handler: ((viewport: ViewportState) => void) | null) => void;
|
|
287
292
|
setDoubleClickEnabled: (enabled: boolean) => void;
|
|
288
293
|
setDoubleClickAction: (action: "reset" | "placeLimitOrder") => void;
|
|
289
294
|
registerIndicator: (plugin: IndicatorPlugin<any>) => void;
|
|
@@ -305,6 +310,18 @@ interface OhlcDataPoint {
|
|
|
305
310
|
c: number;
|
|
306
311
|
v?: number;
|
|
307
312
|
}
|
|
313
|
+
interface ViewportState {
|
|
314
|
+
/** Number of bars visible horizontally. */
|
|
315
|
+
xSpan: number;
|
|
316
|
+
/** Whether the chart is auto-following the latest candle. */
|
|
317
|
+
followingLatest: boolean;
|
|
318
|
+
/** Timestamp (ms) at the horizontal center of the viewport. null when data is empty. */
|
|
319
|
+
centerTimeMs: number | null;
|
|
320
|
+
/** Manual Y-axis min override (null = auto-scale). */
|
|
321
|
+
yMin: number | null;
|
|
322
|
+
/** Manual Y-axis max override (null = auto-scale). */
|
|
323
|
+
yMax: number | null;
|
|
324
|
+
}
|
|
308
325
|
declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
|
|
309
326
|
|
|
310
|
-
export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPlugin, type IndicatorRenderContext, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };
|
|
327
|
+
export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPlugin, type IndicatorRenderContext, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type ViewportState, type WatermarkOptions, createChart };
|
package/dist/index.js
CHANGED
|
@@ -797,10 +797,21 @@ function createChart(element, options = {}) {
|
|
|
797
797
|
let xSpan = 60;
|
|
798
798
|
let followLatest = true;
|
|
799
799
|
let followingLatestChangeHandler = null;
|
|
800
|
+
let viewportChangeHandler = null;
|
|
801
|
+
let viewportEmitScheduled = false;
|
|
802
|
+
const emitViewportChange = () => {
|
|
803
|
+
if (!viewportChangeHandler || viewportEmitScheduled) return;
|
|
804
|
+
viewportEmitScheduled = true;
|
|
805
|
+
queueMicrotask(() => {
|
|
806
|
+
viewportEmitScheduled = false;
|
|
807
|
+
viewportChangeHandler?.(getViewport());
|
|
808
|
+
});
|
|
809
|
+
};
|
|
800
810
|
const updateFollowLatest = (next) => {
|
|
801
811
|
if (followLatest === next) return;
|
|
802
812
|
followLatest = next;
|
|
803
813
|
followingLatestChangeHandler?.(next);
|
|
814
|
+
emitViewportChange();
|
|
804
815
|
};
|
|
805
816
|
let yMinOverride = null;
|
|
806
817
|
let yMaxOverride = null;
|
|
@@ -817,6 +828,41 @@ function createChart(element, options = {}) {
|
|
|
817
828
|
let crosshairPoint = null;
|
|
818
829
|
let doubleClickEnabled = mergedOptions.doubleClickEnabled;
|
|
819
830
|
let doubleClickAction = mergedOptions.doubleClickAction;
|
|
831
|
+
let smoothedTickerPrice = null;
|
|
832
|
+
let tickerPriceTarget = null;
|
|
833
|
+
let smoothingRafId = null;
|
|
834
|
+
const tickerSmoothingLoop = () => {
|
|
835
|
+
smoothingRafId = null;
|
|
836
|
+
if (smoothedTickerPrice === null || tickerPriceTarget === null) return;
|
|
837
|
+
const diff = tickerPriceTarget - smoothedTickerPrice;
|
|
838
|
+
if (Math.abs(diff) < 1e-9) {
|
|
839
|
+
smoothedTickerPrice = tickerPriceTarget;
|
|
840
|
+
draw();
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
844
|
+
const speed = clamp(tickerOpts.smoothingSpeed ?? 8, 1, 60);
|
|
845
|
+
const dt = 1 / 60;
|
|
846
|
+
const lerp = 1 - Math.exp(-speed * dt);
|
|
847
|
+
smoothedTickerPrice += diff * lerp;
|
|
848
|
+
draw();
|
|
849
|
+
smoothingRafId = requestAnimationFrame(tickerSmoothingLoop);
|
|
850
|
+
};
|
|
851
|
+
const pushSmoothedPrice = (target) => {
|
|
852
|
+
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
853
|
+
if (!tickerOpts.smoothing) {
|
|
854
|
+
smoothedTickerPrice = null;
|
|
855
|
+
tickerPriceTarget = null;
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
tickerPriceTarget = target;
|
|
859
|
+
if (smoothedTickerPrice === null) {
|
|
860
|
+
smoothedTickerPrice = target;
|
|
861
|
+
}
|
|
862
|
+
if (smoothingRafId === null) {
|
|
863
|
+
smoothingRafId = requestAnimationFrame(tickerSmoothingLoop);
|
|
864
|
+
}
|
|
865
|
+
};
|
|
820
866
|
const canvas = document.createElement("canvas");
|
|
821
867
|
const ctx = canvas.getContext("2d");
|
|
822
868
|
if (!ctx) {
|
|
@@ -1777,28 +1823,37 @@ function createChart(element, options = {}) {
|
|
|
1777
1823
|
}
|
|
1778
1824
|
ctx.restore();
|
|
1779
1825
|
}
|
|
1826
|
+
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
1827
|
+
const useSmoothedCandle = tickerOpts.smoothing && smoothedTickerPrice !== null;
|
|
1828
|
+
const lastDataIndex = data.length - 1;
|
|
1780
1829
|
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
1781
1830
|
const point = data[index];
|
|
1782
1831
|
if (!point) {
|
|
1783
1832
|
continue;
|
|
1784
1833
|
}
|
|
1834
|
+
const isLastCandle = useSmoothedCandle && index === lastDataIndex;
|
|
1835
|
+
const displayClose = isLastCandle ? smoothedTickerPrice : point.c;
|
|
1836
|
+
const displayHigh = isLastCandle ? Math.max(point.h, smoothedTickerPrice) : point.h;
|
|
1837
|
+
const displayLow = isLastCandle ? Math.min(point.l, smoothedTickerPrice) : point.l;
|
|
1785
1838
|
const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
1786
1839
|
const openY = yFromPrice(point.o);
|
|
1787
|
-
const closeY = yFromPrice(
|
|
1788
|
-
const highY = yFromPrice(
|
|
1789
|
-
const lowY = yFromPrice(
|
|
1790
|
-
const direction = getCandleDirectionByIndex(index);
|
|
1840
|
+
const closeY = yFromPrice(displayClose);
|
|
1841
|
+
const highY = yFromPrice(displayHigh);
|
|
1842
|
+
const lowY = yFromPrice(displayLow);
|
|
1843
|
+
const direction = isLastCandle ? displayClose >= point.o ? "up" : "down" : getCandleDirectionByIndex(index);
|
|
1791
1844
|
const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
|
|
1845
|
+
const roundedCenterX = Math.round(centerX);
|
|
1792
1846
|
ctx.strokeStyle = candleColor;
|
|
1793
1847
|
ctx.lineWidth = candleWickWidth;
|
|
1794
1848
|
ctx.beginPath();
|
|
1795
|
-
ctx.moveTo(
|
|
1796
|
-
ctx.lineTo(
|
|
1849
|
+
ctx.moveTo(roundedCenterX + 0.5, crisp(highY));
|
|
1850
|
+
ctx.lineTo(roundedCenterX + 0.5, crisp(lowY));
|
|
1797
1851
|
ctx.stroke();
|
|
1852
|
+
const bodyLeft = roundedCenterX - Math.floor(bodyWidth / 2);
|
|
1798
1853
|
const bodyTop = Math.min(openY, closeY);
|
|
1799
1854
|
const bodyHeight = Math.max(1, Math.abs(closeY - openY));
|
|
1800
1855
|
ctx.fillStyle = candleColor;
|
|
1801
|
-
ctx.fillRect(
|
|
1856
|
+
ctx.fillRect(bodyLeft, Math.round(bodyTop), bodyWidth, Math.max(1, Math.round(bodyHeight)));
|
|
1802
1857
|
}
|
|
1803
1858
|
const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
1804
1859
|
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
|
|
@@ -1932,10 +1987,10 @@ function createChart(element, options = {}) {
|
|
|
1932
1987
|
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
1933
1988
|
const lastPoint = data[data.length - 1];
|
|
1934
1989
|
if ((ticker.visible ?? true) && lastPoint) {
|
|
1935
|
-
const tickerPrice = lastPoint.c;
|
|
1990
|
+
const tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
|
|
1936
1991
|
const tickerY = yFromPrice(tickerPrice);
|
|
1937
1992
|
const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
|
|
1938
|
-
const lastDirection = getCandleDirectionByIndex(data.length - 1);
|
|
1993
|
+
const lastDirection = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice >= lastPoint.o ? "up" : "down" : getCandleDirectionByIndex(data.length - 1);
|
|
1939
1994
|
const tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
|
|
1940
1995
|
const tickerThickness = Math.max(1, ticker.thickness ?? 1);
|
|
1941
1996
|
const tickerStyle = ticker.style ?? "solid";
|
|
@@ -2134,6 +2189,7 @@ function createChart(element, options = {}) {
|
|
|
2134
2189
|
xCenter = nextStart + nextSpan / 2;
|
|
2135
2190
|
clampXViewport();
|
|
2136
2191
|
updateFollowLatest(false);
|
|
2192
|
+
emitViewportChange();
|
|
2137
2193
|
draw();
|
|
2138
2194
|
};
|
|
2139
2195
|
const zoomXToLatest = (factor) => {
|
|
@@ -2148,6 +2204,7 @@ function createChart(element, options = {}) {
|
|
|
2148
2204
|
xSpan = nextSpan;
|
|
2149
2205
|
xCenter = nextStart + nextSpan / 2;
|
|
2150
2206
|
clampXViewport();
|
|
2207
|
+
emitViewportChange();
|
|
2151
2208
|
draw();
|
|
2152
2209
|
};
|
|
2153
2210
|
const zoomY = (factor, anchorY) => {
|
|
@@ -2168,6 +2225,7 @@ function createChart(element, options = {}) {
|
|
|
2168
2225
|
const clamped = clampYRange(nextMin, nextMax);
|
|
2169
2226
|
yMinOverride = clamped.min;
|
|
2170
2227
|
yMaxOverride = clamped.max;
|
|
2228
|
+
emitViewportChange();
|
|
2171
2229
|
draw();
|
|
2172
2230
|
};
|
|
2173
2231
|
const pan = (deltaX, deltaY, allowX, allowY) => {
|
|
@@ -2191,6 +2249,9 @@ function createChart(element, options = {}) {
|
|
|
2191
2249
|
yMinOverride = clamped.min;
|
|
2192
2250
|
yMaxOverride = clamped.max;
|
|
2193
2251
|
}
|
|
2252
|
+
if (allowX || allowY) {
|
|
2253
|
+
emitViewportChange();
|
|
2254
|
+
}
|
|
2194
2255
|
draw();
|
|
2195
2256
|
};
|
|
2196
2257
|
const resetYViewport = () => {
|
|
@@ -2230,6 +2291,7 @@ function createChart(element, options = {}) {
|
|
|
2230
2291
|
xCenter += bars;
|
|
2231
2292
|
clampXViewport();
|
|
2232
2293
|
updateFollowLatest(false);
|
|
2294
|
+
emitViewportChange();
|
|
2233
2295
|
draw();
|
|
2234
2296
|
};
|
|
2235
2297
|
const panY = (priceDelta) => {
|
|
@@ -2241,17 +2303,20 @@ function createChart(element, options = {}) {
|
|
|
2241
2303
|
const clamped = clampYRange(currentMin + priceDelta, currentMax + priceDelta);
|
|
2242
2304
|
yMinOverride = clamped.min;
|
|
2243
2305
|
yMaxOverride = clamped.max;
|
|
2306
|
+
emitViewportChange();
|
|
2244
2307
|
draw();
|
|
2245
2308
|
};
|
|
2246
2309
|
const fitContent = () => {
|
|
2247
2310
|
fitXViewport();
|
|
2248
2311
|
updateFollowLatest(true);
|
|
2312
|
+
emitViewportChange();
|
|
2249
2313
|
draw();
|
|
2250
2314
|
};
|
|
2251
2315
|
const resetViewport = () => {
|
|
2252
2316
|
fitXViewport();
|
|
2253
2317
|
resetYViewport();
|
|
2254
2318
|
updateFollowLatest(true);
|
|
2319
|
+
emitViewportChange();
|
|
2255
2320
|
draw();
|
|
2256
2321
|
};
|
|
2257
2322
|
const isFollowingLatest = () => followLatest;
|
|
@@ -2268,6 +2333,64 @@ function createChart(element, options = {}) {
|
|
|
2268
2333
|
const onFollowingLatestChange = (handler) => {
|
|
2269
2334
|
followingLatestChangeHandler = handler;
|
|
2270
2335
|
};
|
|
2336
|
+
const getViewport = () => {
|
|
2337
|
+
let centerTimeMs = null;
|
|
2338
|
+
if (data.length > 0) {
|
|
2339
|
+
const centerRounded = Math.round(xCenter);
|
|
2340
|
+
if (centerRounded >= 0 && centerRounded < data.length) {
|
|
2341
|
+
centerTimeMs = data[centerRounded]?.time.getTime() ?? null;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
return {
|
|
2345
|
+
xSpan,
|
|
2346
|
+
followingLatest: followLatest,
|
|
2347
|
+
centerTimeMs,
|
|
2348
|
+
yMin: yMinOverride,
|
|
2349
|
+
yMax: yMaxOverride
|
|
2350
|
+
};
|
|
2351
|
+
};
|
|
2352
|
+
const setViewport = (viewport) => {
|
|
2353
|
+
let changed = false;
|
|
2354
|
+
if (typeof viewport.xSpan === "number" && Number.isFinite(viewport.xSpan)) {
|
|
2355
|
+
const minSpan = minVisibleBars;
|
|
2356
|
+
const maxSpan = Math.min(maxVisibleBars, Math.max(minSpan, data.length + maxPanBars * 2));
|
|
2357
|
+
const nextSpan = clamp(viewport.xSpan, minSpan, maxSpan);
|
|
2358
|
+
if (nextSpan !== xSpan) {
|
|
2359
|
+
xSpan = nextSpan;
|
|
2360
|
+
changed = true;
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
if (typeof viewport.centerTimeMs === "number" && data.length > 0) {
|
|
2364
|
+
const nextCenter = findNearestIndexForTimeMs(viewport.centerTimeMs);
|
|
2365
|
+
if (nextCenter !== null) {
|
|
2366
|
+
xCenter = nextCenter;
|
|
2367
|
+
changed = true;
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
if (viewport.yMin !== void 0) {
|
|
2371
|
+
yMinOverride = viewport.yMin;
|
|
2372
|
+
changed = true;
|
|
2373
|
+
}
|
|
2374
|
+
if (viewport.yMax !== void 0) {
|
|
2375
|
+
yMaxOverride = viewport.yMax;
|
|
2376
|
+
changed = true;
|
|
2377
|
+
}
|
|
2378
|
+
if (typeof viewport.followingLatest === "boolean") {
|
|
2379
|
+
if (followLatest !== viewport.followingLatest) {
|
|
2380
|
+
followLatest = viewport.followingLatest;
|
|
2381
|
+
followingLatestChangeHandler?.(followLatest);
|
|
2382
|
+
changed = true;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
if (changed) {
|
|
2386
|
+
clampXViewport();
|
|
2387
|
+
draw();
|
|
2388
|
+
emitViewportChange();
|
|
2389
|
+
}
|
|
2390
|
+
};
|
|
2391
|
+
const onViewportChange = (handler) => {
|
|
2392
|
+
viewportChangeHandler = handler;
|
|
2393
|
+
};
|
|
2271
2394
|
const getCanvasPoint = (event) => {
|
|
2272
2395
|
const rect = canvas.getBoundingClientRect();
|
|
2273
2396
|
return {
|
|
@@ -2699,6 +2822,10 @@ function createChart(element, options = {}) {
|
|
|
2699
2822
|
fitXViewport();
|
|
2700
2823
|
}
|
|
2701
2824
|
}
|
|
2825
|
+
const lastClose = data.length > 0 ? data[data.length - 1].c : null;
|
|
2826
|
+
if (lastClose !== null) {
|
|
2827
|
+
pushSmoothedPrice(lastClose);
|
|
2828
|
+
}
|
|
2702
2829
|
draw();
|
|
2703
2830
|
};
|
|
2704
2831
|
const setPriceLines = (lines) => {
|
|
@@ -2849,6 +2976,10 @@ function createChart(element, options = {}) {
|
|
|
2849
2976
|
draw();
|
|
2850
2977
|
};
|
|
2851
2978
|
const destroy = () => {
|
|
2979
|
+
if (smoothingRafId !== null) {
|
|
2980
|
+
cancelAnimationFrame(smoothingRafId);
|
|
2981
|
+
smoothingRafId = null;
|
|
2982
|
+
}
|
|
2852
2983
|
canvas.removeEventListener("pointerdown", onPointerDown);
|
|
2853
2984
|
canvas.removeEventListener("pointermove", onPointerMove);
|
|
2854
2985
|
canvas.removeEventListener("pointerup", endPointerDrag);
|
|
@@ -2883,6 +3014,9 @@ function createChart(element, options = {}) {
|
|
|
2883
3014
|
isFollowingLatest,
|
|
2884
3015
|
setFollowingLatest,
|
|
2885
3016
|
onFollowingLatestChange,
|
|
3017
|
+
getViewport,
|
|
3018
|
+
setViewport,
|
|
3019
|
+
onViewportChange,
|
|
2886
3020
|
setDoubleClickEnabled,
|
|
2887
3021
|
setDoubleClickAction,
|
|
2888
3022
|
registerIndicator,
|