hyperprop-charting-library 0.1.44 → 0.1.45
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/README.md +22 -0
- package/dist/hyperprop-charting-library.cjs +212 -18
- package/dist/hyperprop-charting-library.d.ts +31 -1
- package/dist/hyperprop-charting-library.js +212 -18
- package/dist/index.cjs +212 -18
- package/dist/index.d.cts +31 -1
- package/dist/index.d.ts +31 -1
- package/dist/index.js +212 -18
- package/docs/API.md +41 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,6 +94,28 @@ const chart = createChart(root, {
|
|
|
94
94
|
});
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
## TradingView-Style Labels
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const chart = createChart(root, {
|
|
101
|
+
labels: {
|
|
102
|
+
symbolName: "ESH6",
|
|
103
|
+
showSymbolName: true,
|
|
104
|
+
showLastPrice: true,
|
|
105
|
+
showPreviousClose: true,
|
|
106
|
+
previousClosePrice: 5231.25,
|
|
107
|
+
showHighLow: true,
|
|
108
|
+
showBidAsk: true,
|
|
109
|
+
bidPrice: 5234.75,
|
|
110
|
+
askPrice: 5235.0,
|
|
111
|
+
showIndicatorNames: true,
|
|
112
|
+
showIndicatorValues: true,
|
|
113
|
+
showCountdownToBarClose: true,
|
|
114
|
+
noOverlapping: true
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
97
119
|
## Axis Label Density
|
|
98
120
|
|
|
99
121
|
```ts
|
|
@@ -85,6 +85,35 @@ var DEFAULT_DASH_PATTERNS = {
|
|
|
85
85
|
borderDotted: [2, 2],
|
|
86
86
|
borderDashed: [6, 4]
|
|
87
87
|
};
|
|
88
|
+
var DEFAULT_LABELS_OPTIONS = {
|
|
89
|
+
visible: true,
|
|
90
|
+
symbolName: "",
|
|
91
|
+
showSymbolName: false,
|
|
92
|
+
showLastPrice: true,
|
|
93
|
+
showPreviousClose: false,
|
|
94
|
+
previousClosePrice: Number.NaN,
|
|
95
|
+
showHighLow: false,
|
|
96
|
+
showBidAsk: false,
|
|
97
|
+
bidPrice: Number.NaN,
|
|
98
|
+
askPrice: Number.NaN,
|
|
99
|
+
showIndicatorNames: false,
|
|
100
|
+
showIndicatorValues: false,
|
|
101
|
+
showCountdownToBarClose: false,
|
|
102
|
+
noOverlapping: true,
|
|
103
|
+
backgroundColor: "#0b1220",
|
|
104
|
+
textColor: "#cbd5e1",
|
|
105
|
+
mutedTextColor: "#94a3b8",
|
|
106
|
+
symbolNameBackgroundColor: "#1f2937",
|
|
107
|
+
symbolNameTextColor: "#e5e7eb",
|
|
108
|
+
previousCloseColor: "#94a3b8",
|
|
109
|
+
highLowColor: "#a78bfa",
|
|
110
|
+
bidColor: "#ef4444",
|
|
111
|
+
askColor: "#22c55e",
|
|
112
|
+
indicatorTextColor: "#cbd5e1",
|
|
113
|
+
borderRadius: 3,
|
|
114
|
+
labelHeight: 20,
|
|
115
|
+
labelPaddingX: 8
|
|
116
|
+
};
|
|
88
117
|
var DEFAULT_PRICE_LINE_OPTIONS = {
|
|
89
118
|
visible: true,
|
|
90
119
|
style: "solid",
|
|
@@ -174,6 +203,7 @@ var DEFAULT_OPTIONS = {
|
|
|
174
203
|
labelTextColor: "#0b1220",
|
|
175
204
|
labelBorderRadius: 3
|
|
176
205
|
},
|
|
206
|
+
labels: DEFAULT_LABELS_OPTIONS,
|
|
177
207
|
dashPatterns: DEFAULT_DASH_PATTERNS,
|
|
178
208
|
indicators: []
|
|
179
209
|
};
|
|
@@ -213,6 +243,10 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
|
|
|
213
243
|
...baseOptions.tickerLine,
|
|
214
244
|
...options.tickerLine ?? {}
|
|
215
245
|
},
|
|
246
|
+
labels: {
|
|
247
|
+
...baseOptions.labels,
|
|
248
|
+
...options.labels ?? {}
|
|
249
|
+
},
|
|
216
250
|
dashPatterns: {
|
|
217
251
|
...baseOptions.dashPatterns,
|
|
218
252
|
...options.dashPatterns ?? {}
|
|
@@ -1279,6 +1313,14 @@ function createChart(element, options = {}) {
|
|
|
1279
1313
|
ctx.textBaseline = baseline;
|
|
1280
1314
|
ctx.fillText(text, x, y);
|
|
1281
1315
|
};
|
|
1316
|
+
const formatDuration = (ms) => {
|
|
1317
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
|
|
1318
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
1319
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
1320
|
+
const seconds = totalSeconds % 60;
|
|
1321
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
1322
|
+
return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
|
|
1323
|
+
};
|
|
1282
1324
|
const drawPriceLine = (line, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
|
|
1283
1325
|
const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
|
|
1284
1326
|
if (!mergedLine.visible || !Number.isFinite(mergedLine.price)) {
|
|
@@ -2038,14 +2080,39 @@ function createChart(element, options = {}) {
|
|
|
2038
2080
|
drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
|
|
2039
2081
|
ctx.font = prevFont;
|
|
2040
2082
|
}
|
|
2083
|
+
const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
|
|
2084
|
+
const priceAxisLabels = [];
|
|
2085
|
+
const addPriceAxisLabel = (label) => {
|
|
2086
|
+
if (!labels.visible || !Number.isFinite(label.price) || label.text.length === 0) {
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
priceAxisLabels.push(label);
|
|
2090
|
+
};
|
|
2091
|
+
const drawReferenceLine = (price, color, style = "dotted") => {
|
|
2092
|
+
if (!Number.isFinite(price)) {
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
const y = clamp(yFromPrice(price), chartTop + 1, chartBottom - 1);
|
|
2096
|
+
ctx.save();
|
|
2097
|
+
ctx.strokeStyle = color;
|
|
2098
|
+
ctx.lineWidth = 1;
|
|
2099
|
+
applyDashPattern(style, dashPatterns.dotted, dashPatterns.dashed);
|
|
2100
|
+
ctx.beginPath();
|
|
2101
|
+
ctx.moveTo(crisp(chartLeft), crisp(y));
|
|
2102
|
+
ctx.lineTo(crisp(chartRight), crisp(y));
|
|
2103
|
+
ctx.stroke();
|
|
2104
|
+
ctx.restore();
|
|
2105
|
+
};
|
|
2041
2106
|
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2042
2107
|
const lastPoint = data[data.length - 1];
|
|
2108
|
+
let tickerPrice = null;
|
|
2109
|
+
let tickerColor = null;
|
|
2043
2110
|
if ((ticker.visible ?? true) && lastPoint) {
|
|
2044
|
-
|
|
2111
|
+
tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
|
|
2045
2112
|
const tickerY = yFromPrice(tickerPrice);
|
|
2046
2113
|
const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
|
|
2047
2114
|
const lastDirection = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice >= lastPoint.o ? "up" : "down" : getCandleDirectionByIndex(data.length - 1);
|
|
2048
|
-
|
|
2115
|
+
tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
|
|
2049
2116
|
const tickerThickness = Math.max(1, ticker.thickness ?? 1);
|
|
2050
2117
|
const tickerStyle = ticker.style ?? "solid";
|
|
2051
2118
|
ctx.save();
|
|
@@ -2058,24 +2125,122 @@ function createChart(element, options = {}) {
|
|
|
2058
2125
|
ctx.stroke();
|
|
2059
2126
|
ctx.setLineDash([]);
|
|
2060
2127
|
ctx.restore();
|
|
2061
|
-
|
|
2128
|
+
}
|
|
2129
|
+
if ((ticker.visible ?? true) && labels.showLastPrice && tickerPrice !== null && tickerColor !== null) {
|
|
2130
|
+
addPriceAxisLabel({
|
|
2131
|
+
text: formatPrice(tickerPrice),
|
|
2132
|
+
price: tickerPrice,
|
|
2133
|
+
backgroundColor: ticker.labelBackgroundColor ?? tickerColor,
|
|
2134
|
+
textColor: ticker.labelTextColor ?? "#0b1220",
|
|
2135
|
+
color: tickerColor,
|
|
2136
|
+
priority: 100
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
if (labels.showSymbolName && labels.symbolName.trim().length > 0 && tickerPrice !== null) {
|
|
2140
|
+
addPriceAxisLabel({
|
|
2141
|
+
text: labels.symbolName.trim(),
|
|
2142
|
+
price: tickerPrice,
|
|
2143
|
+
backgroundColor: labels.symbolNameBackgroundColor,
|
|
2144
|
+
textColor: labels.symbolNameTextColor,
|
|
2145
|
+
color: labels.symbolNameBackgroundColor,
|
|
2146
|
+
priority: 95
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
if (labels.showPreviousClose) {
|
|
2150
|
+
const previousCloseCandidate = Number.isFinite(labels.previousClosePrice) ? labels.previousClosePrice : data.length > 1 ? data[data.length - 2]?.c : Number.NaN;
|
|
2151
|
+
if (previousCloseCandidate !== void 0 && Number.isFinite(previousCloseCandidate)) {
|
|
2152
|
+
const previousClose = previousCloseCandidate;
|
|
2153
|
+
drawReferenceLine(previousClose, labels.previousCloseColor, "dashed");
|
|
2154
|
+
addPriceAxisLabel({
|
|
2155
|
+
text: `PDC ${formatPrice(previousClose)}`,
|
|
2156
|
+
price: previousClose,
|
|
2157
|
+
backgroundColor: labels.backgroundColor,
|
|
2158
|
+
textColor: labels.mutedTextColor,
|
|
2159
|
+
color: labels.previousCloseColor,
|
|
2160
|
+
priority: 50
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
if (labels.showHighLow && visibleData.length > 0) {
|
|
2165
|
+
const visibleHigh = Math.max(...visibleData.map((point) => point.h));
|
|
2166
|
+
const visibleLow = Math.min(...visibleData.map((point) => point.l));
|
|
2167
|
+
addPriceAxisLabel({
|
|
2168
|
+
text: `H ${formatPrice(visibleHigh)}`,
|
|
2169
|
+
price: visibleHigh,
|
|
2170
|
+
backgroundColor: labels.backgroundColor,
|
|
2171
|
+
textColor: labels.textColor,
|
|
2172
|
+
color: labels.highLowColor,
|
|
2173
|
+
priority: 40
|
|
2174
|
+
});
|
|
2175
|
+
addPriceAxisLabel({
|
|
2176
|
+
text: `L ${formatPrice(visibleLow)}`,
|
|
2177
|
+
price: visibleLow,
|
|
2178
|
+
backgroundColor: labels.backgroundColor,
|
|
2179
|
+
textColor: labels.textColor,
|
|
2180
|
+
color: labels.highLowColor,
|
|
2181
|
+
priority: 40
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
if (labels.showBidAsk) {
|
|
2185
|
+
if (Number.isFinite(labels.bidPrice)) {
|
|
2186
|
+
drawReferenceLine(labels.bidPrice, labels.bidColor, "dotted");
|
|
2187
|
+
addPriceAxisLabel({
|
|
2188
|
+
text: `B ${formatPrice(labels.bidPrice)}`,
|
|
2189
|
+
price: labels.bidPrice,
|
|
2190
|
+
backgroundColor: labels.bidColor,
|
|
2191
|
+
textColor: "#0b1220",
|
|
2192
|
+
color: labels.bidColor,
|
|
2193
|
+
priority: 80
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
if (Number.isFinite(labels.askPrice)) {
|
|
2197
|
+
drawReferenceLine(labels.askPrice, labels.askColor, "dotted");
|
|
2198
|
+
addPriceAxisLabel({
|
|
2199
|
+
text: `A ${formatPrice(labels.askPrice)}`,
|
|
2200
|
+
price: labels.askPrice,
|
|
2201
|
+
backgroundColor: labels.askColor,
|
|
2202
|
+
textColor: "#0b1220",
|
|
2203
|
+
color: labels.askColor,
|
|
2204
|
+
priority: 80
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
if (priceAxisLabels.length > 0) {
|
|
2209
|
+
const labelPaddingX = Math.max(4, labels.labelPaddingX);
|
|
2210
|
+
const labelHeight = Math.max(14, labels.labelHeight);
|
|
2211
|
+
const labelRadius = Math.max(0, labels.borderRadius);
|
|
2062
2212
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
2063
|
-
const
|
|
2064
|
-
|
|
2065
|
-
|
|
2213
|
+
const positionedLabels = priceAxisLabels.map((label) => {
|
|
2214
|
+
const labelTextWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(ctx.measureText(label.text).width) + labelPaddingX * 2;
|
|
2215
|
+
return {
|
|
2216
|
+
...label,
|
|
2217
|
+
width: labelTextWidth,
|
|
2218
|
+
targetY: clamp(yFromPrice(label.price), chartTop + 1, chartBottom - 1) - labelHeight / 2,
|
|
2219
|
+
y: 0
|
|
2220
|
+
};
|
|
2221
|
+
}).sort((a, b) => a.targetY - b.targetY || b.priority - a.priority);
|
|
2222
|
+
const minY = chartTop;
|
|
2223
|
+
const maxY = chartBottom - labelHeight;
|
|
2224
|
+
let cursorY = minY;
|
|
2225
|
+
for (const label of positionedLabels) {
|
|
2226
|
+
label.y = labels.noOverlapping ? Math.max(clamp(label.targetY, minY, maxY), cursorY) : clamp(label.targetY, minY, maxY);
|
|
2227
|
+
cursorY = label.y + labelHeight + 2;
|
|
2228
|
+
}
|
|
2229
|
+
if (labels.noOverlapping && positionedLabels.length > 0) {
|
|
2230
|
+
const lastLabel = positionedLabels[positionedLabels.length - 1];
|
|
2231
|
+
const overflow = lastLabel ? lastLabel.y + labelHeight - maxY : 0;
|
|
2232
|
+
if (overflow > 0) {
|
|
2233
|
+
for (const label of positionedLabels) {
|
|
2234
|
+
label.y = Math.max(minY, label.y - overflow);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2066
2238
|
const labelX = chartRight + 4;
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
tickerLabel,
|
|
2073
|
-
labelX + labelPaddingX,
|
|
2074
|
-
labelY + labelHeight / 2,
|
|
2075
|
-
"left",
|
|
2076
|
-
"middle",
|
|
2077
|
-
ticker.labelTextColor ?? "#0b1220"
|
|
2078
|
-
);
|
|
2239
|
+
for (const label of positionedLabels) {
|
|
2240
|
+
ctx.fillStyle = label.backgroundColor;
|
|
2241
|
+
fillRoundedRect(Math.round(labelX), Math.round(label.y), label.width, labelHeight, labelRadius);
|
|
2242
|
+
drawText(label.text, labelX + labelPaddingX, label.y + labelHeight / 2, "left", "middle", label.textColor);
|
|
2243
|
+
}
|
|
2079
2244
|
}
|
|
2080
2245
|
for (const priceLine of priceLines) {
|
|
2081
2246
|
drawPriceLine(priceLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
|
|
@@ -2083,6 +2248,25 @@ function createChart(element, options = {}) {
|
|
|
2083
2248
|
for (const orderLine of orderLines) {
|
|
2084
2249
|
drawOrderLine(orderLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
|
|
2085
2250
|
}
|
|
2251
|
+
if (labels.visible && (labels.showIndicatorNames || labels.showIndicatorValues)) {
|
|
2252
|
+
const labelEntries = [...activeOverlayIndicators, ...activeSeparateIndicators].map(({ indicator, plugin }) => {
|
|
2253
|
+
const inputValues = Object.entries(indicator.inputs).filter(([, value]) => typeof value === "number" || typeof value === "string" || typeof value === "boolean").slice(0, 2).map(([, value]) => String(value));
|
|
2254
|
+
if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
|
|
2255
|
+
return `${plugin.name} ${inputValues.join(" ")}`;
|
|
2256
|
+
}
|
|
2257
|
+
if (labels.showIndicatorNames) {
|
|
2258
|
+
return plugin.name;
|
|
2259
|
+
}
|
|
2260
|
+
return inputValues.join(" ");
|
|
2261
|
+
}).filter((entry) => entry.length > 0);
|
|
2262
|
+
if (labelEntries.length > 0) {
|
|
2263
|
+
const prevFont = ctx.font;
|
|
2264
|
+
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
2265
|
+
const legendText = labelEntries.join(" ");
|
|
2266
|
+
drawText(legendText, chartLeft + 10, chartTop + 10, "left", "top", labels.indicatorTextColor);
|
|
2267
|
+
ctx.font = prevFont;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2086
2270
|
for (let index = tickStartIndex; index <= visibleTickEnd; index += xStep) {
|
|
2087
2271
|
const tickTime = getTimeForIndex(index);
|
|
2088
2272
|
if (!tickTime) {
|
|
@@ -2098,6 +2282,16 @@ function createChart(element, options = {}) {
|
|
|
2098
2282
|
drawText(timeLabel, x, fullChartBottom + 8, "center", "top", xAxis.textColor);
|
|
2099
2283
|
ctx.font = prevFont;
|
|
2100
2284
|
}
|
|
2285
|
+
if (labels.visible && labels.showCountdownToBarClose && lastPoint) {
|
|
2286
|
+
const stepMs = getTimeStepMs();
|
|
2287
|
+
const rawRemainingMs = lastPoint.time.getTime() + stepMs - Date.now();
|
|
2288
|
+
const countdownMs = rawRemainingMs >= 0 && rawRemainingMs <= stepMs * 2 ? rawRemainingMs : stepMs - Date.now() % stepMs;
|
|
2289
|
+
const countdownText = formatDuration(countdownMs);
|
|
2290
|
+
const prevFont = ctx.font;
|
|
2291
|
+
ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
2292
|
+
drawText(countdownText, chartRight + 6, fullChartBottom + 8, "left", "top", xAxis.textColor);
|
|
2293
|
+
ctx.font = prevFont;
|
|
2294
|
+
}
|
|
2101
2295
|
if (crosshair.visible && crosshairPoint) {
|
|
2102
2296
|
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
2103
2297
|
const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
|
|
@@ -37,6 +37,7 @@ interface ChartOptions {
|
|
|
37
37
|
priceLines?: PriceLineOptions[];
|
|
38
38
|
orderLines?: OrderLineOptions[];
|
|
39
39
|
tickerLine?: TickerLineOptions;
|
|
40
|
+
labels?: LabelsOptions;
|
|
40
41
|
dashPatterns?: Partial<DashPatternOptions>;
|
|
41
42
|
indicators?: IndicatorInstanceOptions[];
|
|
42
43
|
}
|
|
@@ -263,6 +264,35 @@ interface TickerLineOptions {
|
|
|
263
264
|
smoothing?: boolean;
|
|
264
265
|
smoothingSpeed?: number;
|
|
265
266
|
}
|
|
267
|
+
interface LabelsOptions {
|
|
268
|
+
visible?: boolean;
|
|
269
|
+
symbolName?: string;
|
|
270
|
+
showSymbolName?: boolean;
|
|
271
|
+
showLastPrice?: boolean;
|
|
272
|
+
showPreviousClose?: boolean;
|
|
273
|
+
previousClosePrice?: number;
|
|
274
|
+
showHighLow?: boolean;
|
|
275
|
+
showBidAsk?: boolean;
|
|
276
|
+
bidPrice?: number;
|
|
277
|
+
askPrice?: number;
|
|
278
|
+
showIndicatorNames?: boolean;
|
|
279
|
+
showIndicatorValues?: boolean;
|
|
280
|
+
showCountdownToBarClose?: boolean;
|
|
281
|
+
noOverlapping?: boolean;
|
|
282
|
+
backgroundColor?: string;
|
|
283
|
+
textColor?: string;
|
|
284
|
+
mutedTextColor?: string;
|
|
285
|
+
symbolNameBackgroundColor?: string;
|
|
286
|
+
symbolNameTextColor?: string;
|
|
287
|
+
previousCloseColor?: string;
|
|
288
|
+
highLowColor?: string;
|
|
289
|
+
bidColor?: string;
|
|
290
|
+
askColor?: string;
|
|
291
|
+
indicatorTextColor?: string;
|
|
292
|
+
borderRadius?: number;
|
|
293
|
+
labelHeight?: number;
|
|
294
|
+
labelPaddingX?: number;
|
|
295
|
+
}
|
|
266
296
|
interface ChartInstance {
|
|
267
297
|
updateOptions: (options: ChartOptions) => void;
|
|
268
298
|
setData: (data: OhlcDataPoint[]) => void;
|
|
@@ -326,4 +356,4 @@ interface ViewportState {
|
|
|
326
356
|
}
|
|
327
357
|
declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
|
|
328
358
|
|
|
329
|
-
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 };
|
|
359
|
+
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 LabelsOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type ViewportState, type WatermarkOptions, createChart };
|
|
@@ -61,6 +61,35 @@ var DEFAULT_DASH_PATTERNS = {
|
|
|
61
61
|
borderDotted: [2, 2],
|
|
62
62
|
borderDashed: [6, 4]
|
|
63
63
|
};
|
|
64
|
+
var DEFAULT_LABELS_OPTIONS = {
|
|
65
|
+
visible: true,
|
|
66
|
+
symbolName: "",
|
|
67
|
+
showSymbolName: false,
|
|
68
|
+
showLastPrice: true,
|
|
69
|
+
showPreviousClose: false,
|
|
70
|
+
previousClosePrice: Number.NaN,
|
|
71
|
+
showHighLow: false,
|
|
72
|
+
showBidAsk: false,
|
|
73
|
+
bidPrice: Number.NaN,
|
|
74
|
+
askPrice: Number.NaN,
|
|
75
|
+
showIndicatorNames: false,
|
|
76
|
+
showIndicatorValues: false,
|
|
77
|
+
showCountdownToBarClose: false,
|
|
78
|
+
noOverlapping: true,
|
|
79
|
+
backgroundColor: "#0b1220",
|
|
80
|
+
textColor: "#cbd5e1",
|
|
81
|
+
mutedTextColor: "#94a3b8",
|
|
82
|
+
symbolNameBackgroundColor: "#1f2937",
|
|
83
|
+
symbolNameTextColor: "#e5e7eb",
|
|
84
|
+
previousCloseColor: "#94a3b8",
|
|
85
|
+
highLowColor: "#a78bfa",
|
|
86
|
+
bidColor: "#ef4444",
|
|
87
|
+
askColor: "#22c55e",
|
|
88
|
+
indicatorTextColor: "#cbd5e1",
|
|
89
|
+
borderRadius: 3,
|
|
90
|
+
labelHeight: 20,
|
|
91
|
+
labelPaddingX: 8
|
|
92
|
+
};
|
|
64
93
|
var DEFAULT_PRICE_LINE_OPTIONS = {
|
|
65
94
|
visible: true,
|
|
66
95
|
style: "solid",
|
|
@@ -150,6 +179,7 @@ var DEFAULT_OPTIONS = {
|
|
|
150
179
|
labelTextColor: "#0b1220",
|
|
151
180
|
labelBorderRadius: 3
|
|
152
181
|
},
|
|
182
|
+
labels: DEFAULT_LABELS_OPTIONS,
|
|
153
183
|
dashPatterns: DEFAULT_DASH_PATTERNS,
|
|
154
184
|
indicators: []
|
|
155
185
|
};
|
|
@@ -189,6 +219,10 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
|
|
|
189
219
|
...baseOptions.tickerLine,
|
|
190
220
|
...options.tickerLine ?? {}
|
|
191
221
|
},
|
|
222
|
+
labels: {
|
|
223
|
+
...baseOptions.labels,
|
|
224
|
+
...options.labels ?? {}
|
|
225
|
+
},
|
|
192
226
|
dashPatterns: {
|
|
193
227
|
...baseOptions.dashPatterns,
|
|
194
228
|
...options.dashPatterns ?? {}
|
|
@@ -1255,6 +1289,14 @@ function createChart(element, options = {}) {
|
|
|
1255
1289
|
ctx.textBaseline = baseline;
|
|
1256
1290
|
ctx.fillText(text, x, y);
|
|
1257
1291
|
};
|
|
1292
|
+
const formatDuration = (ms) => {
|
|
1293
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
|
|
1294
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
1295
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
1296
|
+
const seconds = totalSeconds % 60;
|
|
1297
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
1298
|
+
return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
|
|
1299
|
+
};
|
|
1258
1300
|
const drawPriceLine = (line, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
|
|
1259
1301
|
const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
|
|
1260
1302
|
if (!mergedLine.visible || !Number.isFinite(mergedLine.price)) {
|
|
@@ -2014,14 +2056,39 @@ function createChart(element, options = {}) {
|
|
|
2014
2056
|
drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
|
|
2015
2057
|
ctx.font = prevFont;
|
|
2016
2058
|
}
|
|
2059
|
+
const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
|
|
2060
|
+
const priceAxisLabels = [];
|
|
2061
|
+
const addPriceAxisLabel = (label) => {
|
|
2062
|
+
if (!labels.visible || !Number.isFinite(label.price) || label.text.length === 0) {
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
priceAxisLabels.push(label);
|
|
2066
|
+
};
|
|
2067
|
+
const drawReferenceLine = (price, color, style = "dotted") => {
|
|
2068
|
+
if (!Number.isFinite(price)) {
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
const y = clamp(yFromPrice(price), chartTop + 1, chartBottom - 1);
|
|
2072
|
+
ctx.save();
|
|
2073
|
+
ctx.strokeStyle = color;
|
|
2074
|
+
ctx.lineWidth = 1;
|
|
2075
|
+
applyDashPattern(style, dashPatterns.dotted, dashPatterns.dashed);
|
|
2076
|
+
ctx.beginPath();
|
|
2077
|
+
ctx.moveTo(crisp(chartLeft), crisp(y));
|
|
2078
|
+
ctx.lineTo(crisp(chartRight), crisp(y));
|
|
2079
|
+
ctx.stroke();
|
|
2080
|
+
ctx.restore();
|
|
2081
|
+
};
|
|
2017
2082
|
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2018
2083
|
const lastPoint = data[data.length - 1];
|
|
2084
|
+
let tickerPrice = null;
|
|
2085
|
+
let tickerColor = null;
|
|
2019
2086
|
if ((ticker.visible ?? true) && lastPoint) {
|
|
2020
|
-
|
|
2087
|
+
tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
|
|
2021
2088
|
const tickerY = yFromPrice(tickerPrice);
|
|
2022
2089
|
const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
|
|
2023
2090
|
const lastDirection = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice >= lastPoint.o ? "up" : "down" : getCandleDirectionByIndex(data.length - 1);
|
|
2024
|
-
|
|
2091
|
+
tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
|
|
2025
2092
|
const tickerThickness = Math.max(1, ticker.thickness ?? 1);
|
|
2026
2093
|
const tickerStyle = ticker.style ?? "solid";
|
|
2027
2094
|
ctx.save();
|
|
@@ -2034,24 +2101,122 @@ function createChart(element, options = {}) {
|
|
|
2034
2101
|
ctx.stroke();
|
|
2035
2102
|
ctx.setLineDash([]);
|
|
2036
2103
|
ctx.restore();
|
|
2037
|
-
|
|
2104
|
+
}
|
|
2105
|
+
if ((ticker.visible ?? true) && labels.showLastPrice && tickerPrice !== null && tickerColor !== null) {
|
|
2106
|
+
addPriceAxisLabel({
|
|
2107
|
+
text: formatPrice(tickerPrice),
|
|
2108
|
+
price: tickerPrice,
|
|
2109
|
+
backgroundColor: ticker.labelBackgroundColor ?? tickerColor,
|
|
2110
|
+
textColor: ticker.labelTextColor ?? "#0b1220",
|
|
2111
|
+
color: tickerColor,
|
|
2112
|
+
priority: 100
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
if (labels.showSymbolName && labels.symbolName.trim().length > 0 && tickerPrice !== null) {
|
|
2116
|
+
addPriceAxisLabel({
|
|
2117
|
+
text: labels.symbolName.trim(),
|
|
2118
|
+
price: tickerPrice,
|
|
2119
|
+
backgroundColor: labels.symbolNameBackgroundColor,
|
|
2120
|
+
textColor: labels.symbolNameTextColor,
|
|
2121
|
+
color: labels.symbolNameBackgroundColor,
|
|
2122
|
+
priority: 95
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
if (labels.showPreviousClose) {
|
|
2126
|
+
const previousCloseCandidate = Number.isFinite(labels.previousClosePrice) ? labels.previousClosePrice : data.length > 1 ? data[data.length - 2]?.c : Number.NaN;
|
|
2127
|
+
if (previousCloseCandidate !== void 0 && Number.isFinite(previousCloseCandidate)) {
|
|
2128
|
+
const previousClose = previousCloseCandidate;
|
|
2129
|
+
drawReferenceLine(previousClose, labels.previousCloseColor, "dashed");
|
|
2130
|
+
addPriceAxisLabel({
|
|
2131
|
+
text: `PDC ${formatPrice(previousClose)}`,
|
|
2132
|
+
price: previousClose,
|
|
2133
|
+
backgroundColor: labels.backgroundColor,
|
|
2134
|
+
textColor: labels.mutedTextColor,
|
|
2135
|
+
color: labels.previousCloseColor,
|
|
2136
|
+
priority: 50
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
if (labels.showHighLow && visibleData.length > 0) {
|
|
2141
|
+
const visibleHigh = Math.max(...visibleData.map((point) => point.h));
|
|
2142
|
+
const visibleLow = Math.min(...visibleData.map((point) => point.l));
|
|
2143
|
+
addPriceAxisLabel({
|
|
2144
|
+
text: `H ${formatPrice(visibleHigh)}`,
|
|
2145
|
+
price: visibleHigh,
|
|
2146
|
+
backgroundColor: labels.backgroundColor,
|
|
2147
|
+
textColor: labels.textColor,
|
|
2148
|
+
color: labels.highLowColor,
|
|
2149
|
+
priority: 40
|
|
2150
|
+
});
|
|
2151
|
+
addPriceAxisLabel({
|
|
2152
|
+
text: `L ${formatPrice(visibleLow)}`,
|
|
2153
|
+
price: visibleLow,
|
|
2154
|
+
backgroundColor: labels.backgroundColor,
|
|
2155
|
+
textColor: labels.textColor,
|
|
2156
|
+
color: labels.highLowColor,
|
|
2157
|
+
priority: 40
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
if (labels.showBidAsk) {
|
|
2161
|
+
if (Number.isFinite(labels.bidPrice)) {
|
|
2162
|
+
drawReferenceLine(labels.bidPrice, labels.bidColor, "dotted");
|
|
2163
|
+
addPriceAxisLabel({
|
|
2164
|
+
text: `B ${formatPrice(labels.bidPrice)}`,
|
|
2165
|
+
price: labels.bidPrice,
|
|
2166
|
+
backgroundColor: labels.bidColor,
|
|
2167
|
+
textColor: "#0b1220",
|
|
2168
|
+
color: labels.bidColor,
|
|
2169
|
+
priority: 80
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
if (Number.isFinite(labels.askPrice)) {
|
|
2173
|
+
drawReferenceLine(labels.askPrice, labels.askColor, "dotted");
|
|
2174
|
+
addPriceAxisLabel({
|
|
2175
|
+
text: `A ${formatPrice(labels.askPrice)}`,
|
|
2176
|
+
price: labels.askPrice,
|
|
2177
|
+
backgroundColor: labels.askColor,
|
|
2178
|
+
textColor: "#0b1220",
|
|
2179
|
+
color: labels.askColor,
|
|
2180
|
+
priority: 80
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
if (priceAxisLabels.length > 0) {
|
|
2185
|
+
const labelPaddingX = Math.max(4, labels.labelPaddingX);
|
|
2186
|
+
const labelHeight = Math.max(14, labels.labelHeight);
|
|
2187
|
+
const labelRadius = Math.max(0, labels.borderRadius);
|
|
2038
2188
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
2039
|
-
const
|
|
2040
|
-
|
|
2041
|
-
|
|
2189
|
+
const positionedLabels = priceAxisLabels.map((label) => {
|
|
2190
|
+
const labelTextWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(ctx.measureText(label.text).width) + labelPaddingX * 2;
|
|
2191
|
+
return {
|
|
2192
|
+
...label,
|
|
2193
|
+
width: labelTextWidth,
|
|
2194
|
+
targetY: clamp(yFromPrice(label.price), chartTop + 1, chartBottom - 1) - labelHeight / 2,
|
|
2195
|
+
y: 0
|
|
2196
|
+
};
|
|
2197
|
+
}).sort((a, b) => a.targetY - b.targetY || b.priority - a.priority);
|
|
2198
|
+
const minY = chartTop;
|
|
2199
|
+
const maxY = chartBottom - labelHeight;
|
|
2200
|
+
let cursorY = minY;
|
|
2201
|
+
for (const label of positionedLabels) {
|
|
2202
|
+
label.y = labels.noOverlapping ? Math.max(clamp(label.targetY, minY, maxY), cursorY) : clamp(label.targetY, minY, maxY);
|
|
2203
|
+
cursorY = label.y + labelHeight + 2;
|
|
2204
|
+
}
|
|
2205
|
+
if (labels.noOverlapping && positionedLabels.length > 0) {
|
|
2206
|
+
const lastLabel = positionedLabels[positionedLabels.length - 1];
|
|
2207
|
+
const overflow = lastLabel ? lastLabel.y + labelHeight - maxY : 0;
|
|
2208
|
+
if (overflow > 0) {
|
|
2209
|
+
for (const label of positionedLabels) {
|
|
2210
|
+
label.y = Math.max(minY, label.y - overflow);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2042
2214
|
const labelX = chartRight + 4;
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
tickerLabel,
|
|
2049
|
-
labelX + labelPaddingX,
|
|
2050
|
-
labelY + labelHeight / 2,
|
|
2051
|
-
"left",
|
|
2052
|
-
"middle",
|
|
2053
|
-
ticker.labelTextColor ?? "#0b1220"
|
|
2054
|
-
);
|
|
2215
|
+
for (const label of positionedLabels) {
|
|
2216
|
+
ctx.fillStyle = label.backgroundColor;
|
|
2217
|
+
fillRoundedRect(Math.round(labelX), Math.round(label.y), label.width, labelHeight, labelRadius);
|
|
2218
|
+
drawText(label.text, labelX + labelPaddingX, label.y + labelHeight / 2, "left", "middle", label.textColor);
|
|
2219
|
+
}
|
|
2055
2220
|
}
|
|
2056
2221
|
for (const priceLine of priceLines) {
|
|
2057
2222
|
drawPriceLine(priceLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
|
|
@@ -2059,6 +2224,25 @@ function createChart(element, options = {}) {
|
|
|
2059
2224
|
for (const orderLine of orderLines) {
|
|
2060
2225
|
drawOrderLine(orderLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
|
|
2061
2226
|
}
|
|
2227
|
+
if (labels.visible && (labels.showIndicatorNames || labels.showIndicatorValues)) {
|
|
2228
|
+
const labelEntries = [...activeOverlayIndicators, ...activeSeparateIndicators].map(({ indicator, plugin }) => {
|
|
2229
|
+
const inputValues = Object.entries(indicator.inputs).filter(([, value]) => typeof value === "number" || typeof value === "string" || typeof value === "boolean").slice(0, 2).map(([, value]) => String(value));
|
|
2230
|
+
if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
|
|
2231
|
+
return `${plugin.name} ${inputValues.join(" ")}`;
|
|
2232
|
+
}
|
|
2233
|
+
if (labels.showIndicatorNames) {
|
|
2234
|
+
return plugin.name;
|
|
2235
|
+
}
|
|
2236
|
+
return inputValues.join(" ");
|
|
2237
|
+
}).filter((entry) => entry.length > 0);
|
|
2238
|
+
if (labelEntries.length > 0) {
|
|
2239
|
+
const prevFont = ctx.font;
|
|
2240
|
+
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
2241
|
+
const legendText = labelEntries.join(" ");
|
|
2242
|
+
drawText(legendText, chartLeft + 10, chartTop + 10, "left", "top", labels.indicatorTextColor);
|
|
2243
|
+
ctx.font = prevFont;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2062
2246
|
for (let index = tickStartIndex; index <= visibleTickEnd; index += xStep) {
|
|
2063
2247
|
const tickTime = getTimeForIndex(index);
|
|
2064
2248
|
if (!tickTime) {
|
|
@@ -2074,6 +2258,16 @@ function createChart(element, options = {}) {
|
|
|
2074
2258
|
drawText(timeLabel, x, fullChartBottom + 8, "center", "top", xAxis.textColor);
|
|
2075
2259
|
ctx.font = prevFont;
|
|
2076
2260
|
}
|
|
2261
|
+
if (labels.visible && labels.showCountdownToBarClose && lastPoint) {
|
|
2262
|
+
const stepMs = getTimeStepMs();
|
|
2263
|
+
const rawRemainingMs = lastPoint.time.getTime() + stepMs - Date.now();
|
|
2264
|
+
const countdownMs = rawRemainingMs >= 0 && rawRemainingMs <= stepMs * 2 ? rawRemainingMs : stepMs - Date.now() % stepMs;
|
|
2265
|
+
const countdownText = formatDuration(countdownMs);
|
|
2266
|
+
const prevFont = ctx.font;
|
|
2267
|
+
ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
2268
|
+
drawText(countdownText, chartRight + 6, fullChartBottom + 8, "left", "top", xAxis.textColor);
|
|
2269
|
+
ctx.font = prevFont;
|
|
2270
|
+
}
|
|
2077
2271
|
if (crosshair.visible && crosshairPoint) {
|
|
2078
2272
|
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
2079
2273
|
const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
|