hyperprop-charting-library 0.1.44 → 0.1.46
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 +34 -0
- package/dist/hyperprop-charting-library.cjs +260 -19
- package/dist/hyperprop-charting-library.d.ts +35 -1
- package/dist/hyperprop-charting-library.js +260 -19
- package/dist/index.cjs +260 -19
- package/dist/index.d.cts +35 -1
- package/dist/index.d.ts +35 -1
- package/dist/index.js +260 -19
- package/docs/API.md +45 -0
- package/package.json +1 -1
|
@@ -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",
|
|
@@ -148,8 +177,13 @@ var DEFAULT_OPTIONS = {
|
|
|
148
177
|
style: "dotted",
|
|
149
178
|
thickness: 1,
|
|
150
179
|
labelTextColor: "#0b1220",
|
|
180
|
+
labelSubtext: "",
|
|
181
|
+
labelSubtextColor: "#0b1220",
|
|
182
|
+
labelSubtextFontSize: 0,
|
|
183
|
+
showCountdownInLabel: false,
|
|
151
184
|
labelBorderRadius: 3
|
|
152
185
|
},
|
|
186
|
+
labels: DEFAULT_LABELS_OPTIONS,
|
|
153
187
|
dashPatterns: DEFAULT_DASH_PATTERNS,
|
|
154
188
|
indicators: []
|
|
155
189
|
};
|
|
@@ -189,6 +223,10 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
|
|
|
189
223
|
...baseOptions.tickerLine,
|
|
190
224
|
...options.tickerLine ?? {}
|
|
191
225
|
},
|
|
226
|
+
labels: {
|
|
227
|
+
...baseOptions.labels,
|
|
228
|
+
...options.labels ?? {}
|
|
229
|
+
},
|
|
192
230
|
dashPatterns: {
|
|
193
231
|
...baseOptions.dashPatterns,
|
|
194
232
|
...options.dashPatterns ?? {}
|
|
@@ -1255,6 +1293,14 @@ function createChart(element, options = {}) {
|
|
|
1255
1293
|
ctx.textBaseline = baseline;
|
|
1256
1294
|
ctx.fillText(text, x, y);
|
|
1257
1295
|
};
|
|
1296
|
+
const formatDuration = (ms) => {
|
|
1297
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
|
|
1298
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
1299
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
1300
|
+
const seconds = totalSeconds % 60;
|
|
1301
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
1302
|
+
return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
|
|
1303
|
+
};
|
|
1258
1304
|
const drawPriceLine = (line, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
|
|
1259
1305
|
const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
|
|
1260
1306
|
if (!mergedLine.visible || !Number.isFinite(mergedLine.price)) {
|
|
@@ -2014,14 +2060,49 @@ function createChart(element, options = {}) {
|
|
|
2014
2060
|
drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
|
|
2015
2061
|
ctx.font = prevFont;
|
|
2016
2062
|
}
|
|
2063
|
+
const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
|
|
2064
|
+
const priceAxisLabels = [];
|
|
2065
|
+
const addPriceAxisLabel = (label) => {
|
|
2066
|
+
if (!labels.visible || !Number.isFinite(label.price) || label.text.length === 0) {
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
priceAxisLabels.push(label);
|
|
2070
|
+
};
|
|
2071
|
+
const drawReferenceLine = (price, color, style = "dotted") => {
|
|
2072
|
+
if (!Number.isFinite(price)) {
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
const y = clamp(yFromPrice(price), chartTop + 1, chartBottom - 1);
|
|
2076
|
+
ctx.save();
|
|
2077
|
+
ctx.strokeStyle = color;
|
|
2078
|
+
ctx.lineWidth = 1;
|
|
2079
|
+
applyDashPattern(style, dashPatterns.dotted, dashPatterns.dashed);
|
|
2080
|
+
ctx.beginPath();
|
|
2081
|
+
ctx.moveTo(crisp(chartLeft), crisp(y));
|
|
2082
|
+
ctx.lineTo(crisp(chartRight), crisp(y));
|
|
2083
|
+
ctx.stroke();
|
|
2084
|
+
ctx.restore();
|
|
2085
|
+
};
|
|
2086
|
+
const getCountdownText = () => {
|
|
2087
|
+
const last = data[data.length - 1];
|
|
2088
|
+
if (!last) {
|
|
2089
|
+
return null;
|
|
2090
|
+
}
|
|
2091
|
+
const stepMs = getTimeStepMs();
|
|
2092
|
+
const rawRemainingMs = last.time.getTime() + stepMs - Date.now();
|
|
2093
|
+
const countdownMs = rawRemainingMs >= 0 && rawRemainingMs <= stepMs * 2 ? rawRemainingMs : stepMs - Date.now() % stepMs;
|
|
2094
|
+
return formatDuration(countdownMs);
|
|
2095
|
+
};
|
|
2017
2096
|
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2018
2097
|
const lastPoint = data[data.length - 1];
|
|
2098
|
+
let tickerPrice = null;
|
|
2099
|
+
let tickerColor = null;
|
|
2019
2100
|
if ((ticker.visible ?? true) && lastPoint) {
|
|
2020
|
-
|
|
2101
|
+
tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
|
|
2021
2102
|
const tickerY = yFromPrice(tickerPrice);
|
|
2022
2103
|
const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
|
|
2023
2104
|
const lastDirection = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice >= lastPoint.o ? "up" : "down" : getCandleDirectionByIndex(data.length - 1);
|
|
2024
|
-
|
|
2105
|
+
tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
|
|
2025
2106
|
const tickerThickness = Math.max(1, ticker.thickness ?? 1);
|
|
2026
2107
|
const tickerStyle = ticker.style ?? "solid";
|
|
2027
2108
|
ctx.save();
|
|
@@ -2034,24 +2115,155 @@ function createChart(element, options = {}) {
|
|
|
2034
2115
|
ctx.stroke();
|
|
2035
2116
|
ctx.setLineDash([]);
|
|
2036
2117
|
ctx.restore();
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
const
|
|
2040
|
-
|
|
2041
|
-
|
|
2118
|
+
}
|
|
2119
|
+
if ((ticker.visible ?? true) && labels.showLastPrice && tickerPrice !== null && tickerColor !== null) {
|
|
2120
|
+
const tickerSubtext = ticker.showCountdownInLabel ? getCountdownText() : ticker.labelSubtext?.trim();
|
|
2121
|
+
addPriceAxisLabel({
|
|
2122
|
+
text: formatPrice(tickerPrice),
|
|
2123
|
+
...tickerSubtext ? { subtext: tickerSubtext } : {},
|
|
2124
|
+
subtextColor: ticker.labelSubtextColor ?? ticker.labelTextColor ?? "#0b1220",
|
|
2125
|
+
...ticker.labelSubtextFontSize === void 0 ? {} : { subtextFontSize: ticker.labelSubtextFontSize },
|
|
2126
|
+
price: tickerPrice,
|
|
2127
|
+
backgroundColor: ticker.labelBackgroundColor ?? tickerColor,
|
|
2128
|
+
textColor: ticker.labelTextColor ?? "#0b1220",
|
|
2129
|
+
color: tickerColor,
|
|
2130
|
+
priority: 100
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
if (labels.showSymbolName && labels.symbolName.trim().length > 0 && tickerPrice !== null) {
|
|
2134
|
+
addPriceAxisLabel({
|
|
2135
|
+
text: labels.symbolName.trim(),
|
|
2136
|
+
price: tickerPrice,
|
|
2137
|
+
backgroundColor: labels.symbolNameBackgroundColor,
|
|
2138
|
+
textColor: labels.symbolNameTextColor,
|
|
2139
|
+
color: labels.symbolNameBackgroundColor,
|
|
2140
|
+
priority: 95
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
if (labels.showPreviousClose) {
|
|
2144
|
+
const previousCloseCandidate = Number.isFinite(labels.previousClosePrice) ? labels.previousClosePrice : data.length > 1 ? data[data.length - 2]?.c : Number.NaN;
|
|
2145
|
+
if (previousCloseCandidate !== void 0 && Number.isFinite(previousCloseCandidate)) {
|
|
2146
|
+
const previousClose = previousCloseCandidate;
|
|
2147
|
+
drawReferenceLine(previousClose, labels.previousCloseColor, "dashed");
|
|
2148
|
+
addPriceAxisLabel({
|
|
2149
|
+
text: `PDC ${formatPrice(previousClose)}`,
|
|
2150
|
+
price: previousClose,
|
|
2151
|
+
backgroundColor: labels.backgroundColor,
|
|
2152
|
+
textColor: labels.mutedTextColor,
|
|
2153
|
+
color: labels.previousCloseColor,
|
|
2154
|
+
priority: 50
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
if (labels.showHighLow && visibleData.length > 0) {
|
|
2159
|
+
const visibleHigh = Math.max(...visibleData.map((point) => point.h));
|
|
2160
|
+
const visibleLow = Math.min(...visibleData.map((point) => point.l));
|
|
2161
|
+
addPriceAxisLabel({
|
|
2162
|
+
text: `H ${formatPrice(visibleHigh)}`,
|
|
2163
|
+
price: visibleHigh,
|
|
2164
|
+
backgroundColor: labels.backgroundColor,
|
|
2165
|
+
textColor: labels.textColor,
|
|
2166
|
+
color: labels.highLowColor,
|
|
2167
|
+
priority: 40
|
|
2168
|
+
});
|
|
2169
|
+
addPriceAxisLabel({
|
|
2170
|
+
text: `L ${formatPrice(visibleLow)}`,
|
|
2171
|
+
price: visibleLow,
|
|
2172
|
+
backgroundColor: labels.backgroundColor,
|
|
2173
|
+
textColor: labels.textColor,
|
|
2174
|
+
color: labels.highLowColor,
|
|
2175
|
+
priority: 40
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
if (labels.showBidAsk) {
|
|
2179
|
+
if (Number.isFinite(labels.bidPrice)) {
|
|
2180
|
+
drawReferenceLine(labels.bidPrice, labels.bidColor, "dotted");
|
|
2181
|
+
addPriceAxisLabel({
|
|
2182
|
+
text: `B ${formatPrice(labels.bidPrice)}`,
|
|
2183
|
+
price: labels.bidPrice,
|
|
2184
|
+
backgroundColor: labels.bidColor,
|
|
2185
|
+
textColor: "#0b1220",
|
|
2186
|
+
color: labels.bidColor,
|
|
2187
|
+
priority: 80
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
if (Number.isFinite(labels.askPrice)) {
|
|
2191
|
+
drawReferenceLine(labels.askPrice, labels.askColor, "dotted");
|
|
2192
|
+
addPriceAxisLabel({
|
|
2193
|
+
text: `A ${formatPrice(labels.askPrice)}`,
|
|
2194
|
+
price: labels.askPrice,
|
|
2195
|
+
backgroundColor: labels.askColor,
|
|
2196
|
+
textColor: "#0b1220",
|
|
2197
|
+
color: labels.askColor,
|
|
2198
|
+
priority: 80
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
if (priceAxisLabels.length > 0) {
|
|
2203
|
+
const labelPaddingX = Math.max(4, labels.labelPaddingX);
|
|
2204
|
+
const baseLabelHeight = Math.max(14, labels.labelHeight);
|
|
2205
|
+
const labelRadius = Math.max(0, labels.borderRadius);
|
|
2206
|
+
const priceLabelFontSize = Math.max(8, axis.fontSize);
|
|
2207
|
+
ctx.font = `${priceLabelFontSize}px ${mergedOptions.fontFamily}`;
|
|
2208
|
+
const positionedLabels = priceAxisLabels.map((label) => {
|
|
2209
|
+
const subtext = label.subtext?.trim();
|
|
2210
|
+
const subtextFontSize = label.subtextFontSize !== void 0 && label.subtextFontSize > 0 ? Math.max(8, Math.round(label.subtextFontSize)) : priceLabelFontSize;
|
|
2211
|
+
const labelHeight = baseLabelHeight + (subtext ? subtextFontSize + 5 : 0);
|
|
2212
|
+
const primaryWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(ctx.measureText(label.text).width) + labelPaddingX * 2;
|
|
2213
|
+
let subtextWidth = 0;
|
|
2214
|
+
if (subtext) {
|
|
2215
|
+
const baseFont = ctx.font;
|
|
2216
|
+
ctx.font = `${subtextFontSize}px ${mergedOptions.fontFamily}`;
|
|
2217
|
+
subtextWidth = Math.ceil(ctx.measureText(subtext).width) + labelPaddingX * 2;
|
|
2218
|
+
ctx.font = baseFont;
|
|
2219
|
+
}
|
|
2220
|
+
const labelTextWidth = Math.max(primaryWidth, subtextWidth);
|
|
2221
|
+
return {
|
|
2222
|
+
...label,
|
|
2223
|
+
subtext,
|
|
2224
|
+
subtextFontSize,
|
|
2225
|
+
height: labelHeight,
|
|
2226
|
+
width: labelTextWidth,
|
|
2227
|
+
targetY: clamp(yFromPrice(label.price), chartTop + 1, chartBottom - 1) - labelHeight / 2,
|
|
2228
|
+
y: 0
|
|
2229
|
+
};
|
|
2230
|
+
}).sort((a, b) => a.targetY - b.targetY || b.priority - a.priority);
|
|
2231
|
+
const minY = chartTop;
|
|
2232
|
+
let cursorY = minY;
|
|
2233
|
+
for (const label of positionedLabels) {
|
|
2234
|
+
const maxY = chartBottom - label.height;
|
|
2235
|
+
label.y = labels.noOverlapping ? Math.max(clamp(label.targetY, minY, maxY), cursorY) : clamp(label.targetY, minY, maxY);
|
|
2236
|
+
cursorY = label.y + label.height + 2;
|
|
2237
|
+
}
|
|
2238
|
+
if (labels.noOverlapping && positionedLabels.length > 0) {
|
|
2239
|
+
const lastLabel = positionedLabels[positionedLabels.length - 1];
|
|
2240
|
+
const overflow = lastLabel ? lastLabel.y + lastLabel.height - chartBottom : 0;
|
|
2241
|
+
if (overflow > 0) {
|
|
2242
|
+
for (const label of positionedLabels) {
|
|
2243
|
+
label.y = Math.max(minY, label.y - overflow);
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2042
2247
|
const labelX = chartRight + 4;
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2248
|
+
for (const label of positionedLabels) {
|
|
2249
|
+
ctx.fillStyle = label.backgroundColor;
|
|
2250
|
+
fillRoundedRect(Math.round(labelX), Math.round(label.y), label.width, label.height, labelRadius);
|
|
2251
|
+
const primaryY = label.subtext ? label.y + baseLabelHeight / 2 - 1 : label.y + label.height / 2;
|
|
2252
|
+
drawText(label.text, labelX + labelPaddingX, primaryY, "left", "middle", label.textColor);
|
|
2253
|
+
if (label.subtext) {
|
|
2254
|
+
const baseFont = ctx.font;
|
|
2255
|
+
ctx.font = `${label.subtextFontSize}px ${mergedOptions.fontFamily}`;
|
|
2256
|
+
drawText(
|
|
2257
|
+
label.subtext,
|
|
2258
|
+
labelX + labelPaddingX,
|
|
2259
|
+
label.y + baseLabelHeight + label.subtextFontSize / 2,
|
|
2260
|
+
"left",
|
|
2261
|
+
"middle",
|
|
2262
|
+
label.subtextColor ?? label.textColor
|
|
2263
|
+
);
|
|
2264
|
+
ctx.font = baseFont;
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2055
2267
|
}
|
|
2056
2268
|
for (const priceLine of priceLines) {
|
|
2057
2269
|
drawPriceLine(priceLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
|
|
@@ -2059,6 +2271,25 @@ function createChart(element, options = {}) {
|
|
|
2059
2271
|
for (const orderLine of orderLines) {
|
|
2060
2272
|
drawOrderLine(orderLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
|
|
2061
2273
|
}
|
|
2274
|
+
if (labels.visible && (labels.showIndicatorNames || labels.showIndicatorValues)) {
|
|
2275
|
+
const labelEntries = [...activeOverlayIndicators, ...activeSeparateIndicators].map(({ indicator, plugin }) => {
|
|
2276
|
+
const inputValues = Object.entries(indicator.inputs).filter(([, value]) => typeof value === "number" || typeof value === "string" || typeof value === "boolean").slice(0, 2).map(([, value]) => String(value));
|
|
2277
|
+
if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
|
|
2278
|
+
return `${plugin.name} ${inputValues.join(" ")}`;
|
|
2279
|
+
}
|
|
2280
|
+
if (labels.showIndicatorNames) {
|
|
2281
|
+
return plugin.name;
|
|
2282
|
+
}
|
|
2283
|
+
return inputValues.join(" ");
|
|
2284
|
+
}).filter((entry) => entry.length > 0);
|
|
2285
|
+
if (labelEntries.length > 0) {
|
|
2286
|
+
const prevFont = ctx.font;
|
|
2287
|
+
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
2288
|
+
const legendText = labelEntries.join(" ");
|
|
2289
|
+
drawText(legendText, chartLeft + 10, chartTop + 10, "left", "top", labels.indicatorTextColor);
|
|
2290
|
+
ctx.font = prevFont;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2062
2293
|
for (let index = tickStartIndex; index <= visibleTickEnd; index += xStep) {
|
|
2063
2294
|
const tickTime = getTimeForIndex(index);
|
|
2064
2295
|
if (!tickTime) {
|
|
@@ -2074,6 +2305,16 @@ function createChart(element, options = {}) {
|
|
|
2074
2305
|
drawText(timeLabel, x, fullChartBottom + 8, "center", "top", xAxis.textColor);
|
|
2075
2306
|
ctx.font = prevFont;
|
|
2076
2307
|
}
|
|
2308
|
+
if (labels.visible && labels.showCountdownToBarClose && lastPoint) {
|
|
2309
|
+
const stepMs = getTimeStepMs();
|
|
2310
|
+
const rawRemainingMs = lastPoint.time.getTime() + stepMs - Date.now();
|
|
2311
|
+
const countdownMs = rawRemainingMs >= 0 && rawRemainingMs <= stepMs * 2 ? rawRemainingMs : stepMs - Date.now() % stepMs;
|
|
2312
|
+
const countdownText = formatDuration(countdownMs);
|
|
2313
|
+
const prevFont = ctx.font;
|
|
2314
|
+
ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
2315
|
+
drawText(countdownText, chartRight + 6, fullChartBottom + 8, "left", "top", xAxis.textColor);
|
|
2316
|
+
ctx.font = prevFont;
|
|
2317
|
+
}
|
|
2077
2318
|
if (crosshair.visible && crosshairPoint) {
|
|
2078
2319
|
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
2079
2320
|
const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
|
package/dist/index.cjs
CHANGED
|
@@ -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",
|
|
@@ -172,8 +201,13 @@ var DEFAULT_OPTIONS = {
|
|
|
172
201
|
style: "dotted",
|
|
173
202
|
thickness: 1,
|
|
174
203
|
labelTextColor: "#0b1220",
|
|
204
|
+
labelSubtext: "",
|
|
205
|
+
labelSubtextColor: "#0b1220",
|
|
206
|
+
labelSubtextFontSize: 0,
|
|
207
|
+
showCountdownInLabel: false,
|
|
175
208
|
labelBorderRadius: 3
|
|
176
209
|
},
|
|
210
|
+
labels: DEFAULT_LABELS_OPTIONS,
|
|
177
211
|
dashPatterns: DEFAULT_DASH_PATTERNS,
|
|
178
212
|
indicators: []
|
|
179
213
|
};
|
|
@@ -213,6 +247,10 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
|
|
|
213
247
|
...baseOptions.tickerLine,
|
|
214
248
|
...options.tickerLine ?? {}
|
|
215
249
|
},
|
|
250
|
+
labels: {
|
|
251
|
+
...baseOptions.labels,
|
|
252
|
+
...options.labels ?? {}
|
|
253
|
+
},
|
|
216
254
|
dashPatterns: {
|
|
217
255
|
...baseOptions.dashPatterns,
|
|
218
256
|
...options.dashPatterns ?? {}
|
|
@@ -1279,6 +1317,14 @@ function createChart(element, options = {}) {
|
|
|
1279
1317
|
ctx.textBaseline = baseline;
|
|
1280
1318
|
ctx.fillText(text, x, y);
|
|
1281
1319
|
};
|
|
1320
|
+
const formatDuration = (ms) => {
|
|
1321
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
|
|
1322
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
1323
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
1324
|
+
const seconds = totalSeconds % 60;
|
|
1325
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
1326
|
+
return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
|
|
1327
|
+
};
|
|
1282
1328
|
const drawPriceLine = (line, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
|
|
1283
1329
|
const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
|
|
1284
1330
|
if (!mergedLine.visible || !Number.isFinite(mergedLine.price)) {
|
|
@@ -2038,14 +2084,49 @@ function createChart(element, options = {}) {
|
|
|
2038
2084
|
drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
|
|
2039
2085
|
ctx.font = prevFont;
|
|
2040
2086
|
}
|
|
2087
|
+
const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
|
|
2088
|
+
const priceAxisLabels = [];
|
|
2089
|
+
const addPriceAxisLabel = (label) => {
|
|
2090
|
+
if (!labels.visible || !Number.isFinite(label.price) || label.text.length === 0) {
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
priceAxisLabels.push(label);
|
|
2094
|
+
};
|
|
2095
|
+
const drawReferenceLine = (price, color, style = "dotted") => {
|
|
2096
|
+
if (!Number.isFinite(price)) {
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
const y = clamp(yFromPrice(price), chartTop + 1, chartBottom - 1);
|
|
2100
|
+
ctx.save();
|
|
2101
|
+
ctx.strokeStyle = color;
|
|
2102
|
+
ctx.lineWidth = 1;
|
|
2103
|
+
applyDashPattern(style, dashPatterns.dotted, dashPatterns.dashed);
|
|
2104
|
+
ctx.beginPath();
|
|
2105
|
+
ctx.moveTo(crisp(chartLeft), crisp(y));
|
|
2106
|
+
ctx.lineTo(crisp(chartRight), crisp(y));
|
|
2107
|
+
ctx.stroke();
|
|
2108
|
+
ctx.restore();
|
|
2109
|
+
};
|
|
2110
|
+
const getCountdownText = () => {
|
|
2111
|
+
const last = data[data.length - 1];
|
|
2112
|
+
if (!last) {
|
|
2113
|
+
return null;
|
|
2114
|
+
}
|
|
2115
|
+
const stepMs = getTimeStepMs();
|
|
2116
|
+
const rawRemainingMs = last.time.getTime() + stepMs - Date.now();
|
|
2117
|
+
const countdownMs = rawRemainingMs >= 0 && rawRemainingMs <= stepMs * 2 ? rawRemainingMs : stepMs - Date.now() % stepMs;
|
|
2118
|
+
return formatDuration(countdownMs);
|
|
2119
|
+
};
|
|
2041
2120
|
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2042
2121
|
const lastPoint = data[data.length - 1];
|
|
2122
|
+
let tickerPrice = null;
|
|
2123
|
+
let tickerColor = null;
|
|
2043
2124
|
if ((ticker.visible ?? true) && lastPoint) {
|
|
2044
|
-
|
|
2125
|
+
tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
|
|
2045
2126
|
const tickerY = yFromPrice(tickerPrice);
|
|
2046
2127
|
const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
|
|
2047
2128
|
const lastDirection = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice >= lastPoint.o ? "up" : "down" : getCandleDirectionByIndex(data.length - 1);
|
|
2048
|
-
|
|
2129
|
+
tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
|
|
2049
2130
|
const tickerThickness = Math.max(1, ticker.thickness ?? 1);
|
|
2050
2131
|
const tickerStyle = ticker.style ?? "solid";
|
|
2051
2132
|
ctx.save();
|
|
@@ -2058,24 +2139,155 @@ function createChart(element, options = {}) {
|
|
|
2058
2139
|
ctx.stroke();
|
|
2059
2140
|
ctx.setLineDash([]);
|
|
2060
2141
|
ctx.restore();
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
const
|
|
2064
|
-
|
|
2065
|
-
|
|
2142
|
+
}
|
|
2143
|
+
if ((ticker.visible ?? true) && labels.showLastPrice && tickerPrice !== null && tickerColor !== null) {
|
|
2144
|
+
const tickerSubtext = ticker.showCountdownInLabel ? getCountdownText() : ticker.labelSubtext?.trim();
|
|
2145
|
+
addPriceAxisLabel({
|
|
2146
|
+
text: formatPrice(tickerPrice),
|
|
2147
|
+
...tickerSubtext ? { subtext: tickerSubtext } : {},
|
|
2148
|
+
subtextColor: ticker.labelSubtextColor ?? ticker.labelTextColor ?? "#0b1220",
|
|
2149
|
+
...ticker.labelSubtextFontSize === void 0 ? {} : { subtextFontSize: ticker.labelSubtextFontSize },
|
|
2150
|
+
price: tickerPrice,
|
|
2151
|
+
backgroundColor: ticker.labelBackgroundColor ?? tickerColor,
|
|
2152
|
+
textColor: ticker.labelTextColor ?? "#0b1220",
|
|
2153
|
+
color: tickerColor,
|
|
2154
|
+
priority: 100
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
if (labels.showSymbolName && labels.symbolName.trim().length > 0 && tickerPrice !== null) {
|
|
2158
|
+
addPriceAxisLabel({
|
|
2159
|
+
text: labels.symbolName.trim(),
|
|
2160
|
+
price: tickerPrice,
|
|
2161
|
+
backgroundColor: labels.symbolNameBackgroundColor,
|
|
2162
|
+
textColor: labels.symbolNameTextColor,
|
|
2163
|
+
color: labels.symbolNameBackgroundColor,
|
|
2164
|
+
priority: 95
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
if (labels.showPreviousClose) {
|
|
2168
|
+
const previousCloseCandidate = Number.isFinite(labels.previousClosePrice) ? labels.previousClosePrice : data.length > 1 ? data[data.length - 2]?.c : Number.NaN;
|
|
2169
|
+
if (previousCloseCandidate !== void 0 && Number.isFinite(previousCloseCandidate)) {
|
|
2170
|
+
const previousClose = previousCloseCandidate;
|
|
2171
|
+
drawReferenceLine(previousClose, labels.previousCloseColor, "dashed");
|
|
2172
|
+
addPriceAxisLabel({
|
|
2173
|
+
text: `PDC ${formatPrice(previousClose)}`,
|
|
2174
|
+
price: previousClose,
|
|
2175
|
+
backgroundColor: labels.backgroundColor,
|
|
2176
|
+
textColor: labels.mutedTextColor,
|
|
2177
|
+
color: labels.previousCloseColor,
|
|
2178
|
+
priority: 50
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
if (labels.showHighLow && visibleData.length > 0) {
|
|
2183
|
+
const visibleHigh = Math.max(...visibleData.map((point) => point.h));
|
|
2184
|
+
const visibleLow = Math.min(...visibleData.map((point) => point.l));
|
|
2185
|
+
addPriceAxisLabel({
|
|
2186
|
+
text: `H ${formatPrice(visibleHigh)}`,
|
|
2187
|
+
price: visibleHigh,
|
|
2188
|
+
backgroundColor: labels.backgroundColor,
|
|
2189
|
+
textColor: labels.textColor,
|
|
2190
|
+
color: labels.highLowColor,
|
|
2191
|
+
priority: 40
|
|
2192
|
+
});
|
|
2193
|
+
addPriceAxisLabel({
|
|
2194
|
+
text: `L ${formatPrice(visibleLow)}`,
|
|
2195
|
+
price: visibleLow,
|
|
2196
|
+
backgroundColor: labels.backgroundColor,
|
|
2197
|
+
textColor: labels.textColor,
|
|
2198
|
+
color: labels.highLowColor,
|
|
2199
|
+
priority: 40
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
if (labels.showBidAsk) {
|
|
2203
|
+
if (Number.isFinite(labels.bidPrice)) {
|
|
2204
|
+
drawReferenceLine(labels.bidPrice, labels.bidColor, "dotted");
|
|
2205
|
+
addPriceAxisLabel({
|
|
2206
|
+
text: `B ${formatPrice(labels.bidPrice)}`,
|
|
2207
|
+
price: labels.bidPrice,
|
|
2208
|
+
backgroundColor: labels.bidColor,
|
|
2209
|
+
textColor: "#0b1220",
|
|
2210
|
+
color: labels.bidColor,
|
|
2211
|
+
priority: 80
|
|
2212
|
+
});
|
|
2213
|
+
}
|
|
2214
|
+
if (Number.isFinite(labels.askPrice)) {
|
|
2215
|
+
drawReferenceLine(labels.askPrice, labels.askColor, "dotted");
|
|
2216
|
+
addPriceAxisLabel({
|
|
2217
|
+
text: `A ${formatPrice(labels.askPrice)}`,
|
|
2218
|
+
price: labels.askPrice,
|
|
2219
|
+
backgroundColor: labels.askColor,
|
|
2220
|
+
textColor: "#0b1220",
|
|
2221
|
+
color: labels.askColor,
|
|
2222
|
+
priority: 80
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
if (priceAxisLabels.length > 0) {
|
|
2227
|
+
const labelPaddingX = Math.max(4, labels.labelPaddingX);
|
|
2228
|
+
const baseLabelHeight = Math.max(14, labels.labelHeight);
|
|
2229
|
+
const labelRadius = Math.max(0, labels.borderRadius);
|
|
2230
|
+
const priceLabelFontSize = Math.max(8, axis.fontSize);
|
|
2231
|
+
ctx.font = `${priceLabelFontSize}px ${mergedOptions.fontFamily}`;
|
|
2232
|
+
const positionedLabels = priceAxisLabels.map((label) => {
|
|
2233
|
+
const subtext = label.subtext?.trim();
|
|
2234
|
+
const subtextFontSize = label.subtextFontSize !== void 0 && label.subtextFontSize > 0 ? Math.max(8, Math.round(label.subtextFontSize)) : priceLabelFontSize;
|
|
2235
|
+
const labelHeight = baseLabelHeight + (subtext ? subtextFontSize + 5 : 0);
|
|
2236
|
+
const primaryWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(ctx.measureText(label.text).width) + labelPaddingX * 2;
|
|
2237
|
+
let subtextWidth = 0;
|
|
2238
|
+
if (subtext) {
|
|
2239
|
+
const baseFont = ctx.font;
|
|
2240
|
+
ctx.font = `${subtextFontSize}px ${mergedOptions.fontFamily}`;
|
|
2241
|
+
subtextWidth = Math.ceil(ctx.measureText(subtext).width) + labelPaddingX * 2;
|
|
2242
|
+
ctx.font = baseFont;
|
|
2243
|
+
}
|
|
2244
|
+
const labelTextWidth = Math.max(primaryWidth, subtextWidth);
|
|
2245
|
+
return {
|
|
2246
|
+
...label,
|
|
2247
|
+
subtext,
|
|
2248
|
+
subtextFontSize,
|
|
2249
|
+
height: labelHeight,
|
|
2250
|
+
width: labelTextWidth,
|
|
2251
|
+
targetY: clamp(yFromPrice(label.price), chartTop + 1, chartBottom - 1) - labelHeight / 2,
|
|
2252
|
+
y: 0
|
|
2253
|
+
};
|
|
2254
|
+
}).sort((a, b) => a.targetY - b.targetY || b.priority - a.priority);
|
|
2255
|
+
const minY = chartTop;
|
|
2256
|
+
let cursorY = minY;
|
|
2257
|
+
for (const label of positionedLabels) {
|
|
2258
|
+
const maxY = chartBottom - label.height;
|
|
2259
|
+
label.y = labels.noOverlapping ? Math.max(clamp(label.targetY, minY, maxY), cursorY) : clamp(label.targetY, minY, maxY);
|
|
2260
|
+
cursorY = label.y + label.height + 2;
|
|
2261
|
+
}
|
|
2262
|
+
if (labels.noOverlapping && positionedLabels.length > 0) {
|
|
2263
|
+
const lastLabel = positionedLabels[positionedLabels.length - 1];
|
|
2264
|
+
const overflow = lastLabel ? lastLabel.y + lastLabel.height - chartBottom : 0;
|
|
2265
|
+
if (overflow > 0) {
|
|
2266
|
+
for (const label of positionedLabels) {
|
|
2267
|
+
label.y = Math.max(minY, label.y - overflow);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2066
2271
|
const labelX = chartRight + 4;
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2272
|
+
for (const label of positionedLabels) {
|
|
2273
|
+
ctx.fillStyle = label.backgroundColor;
|
|
2274
|
+
fillRoundedRect(Math.round(labelX), Math.round(label.y), label.width, label.height, labelRadius);
|
|
2275
|
+
const primaryY = label.subtext ? label.y + baseLabelHeight / 2 - 1 : label.y + label.height / 2;
|
|
2276
|
+
drawText(label.text, labelX + labelPaddingX, primaryY, "left", "middle", label.textColor);
|
|
2277
|
+
if (label.subtext) {
|
|
2278
|
+
const baseFont = ctx.font;
|
|
2279
|
+
ctx.font = `${label.subtextFontSize}px ${mergedOptions.fontFamily}`;
|
|
2280
|
+
drawText(
|
|
2281
|
+
label.subtext,
|
|
2282
|
+
labelX + labelPaddingX,
|
|
2283
|
+
label.y + baseLabelHeight + label.subtextFontSize / 2,
|
|
2284
|
+
"left",
|
|
2285
|
+
"middle",
|
|
2286
|
+
label.subtextColor ?? label.textColor
|
|
2287
|
+
);
|
|
2288
|
+
ctx.font = baseFont;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2079
2291
|
}
|
|
2080
2292
|
for (const priceLine of priceLines) {
|
|
2081
2293
|
drawPriceLine(priceLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
|
|
@@ -2083,6 +2295,25 @@ function createChart(element, options = {}) {
|
|
|
2083
2295
|
for (const orderLine of orderLines) {
|
|
2084
2296
|
drawOrderLine(orderLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
|
|
2085
2297
|
}
|
|
2298
|
+
if (labels.visible && (labels.showIndicatorNames || labels.showIndicatorValues)) {
|
|
2299
|
+
const labelEntries = [...activeOverlayIndicators, ...activeSeparateIndicators].map(({ indicator, plugin }) => {
|
|
2300
|
+
const inputValues = Object.entries(indicator.inputs).filter(([, value]) => typeof value === "number" || typeof value === "string" || typeof value === "boolean").slice(0, 2).map(([, value]) => String(value));
|
|
2301
|
+
if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
|
|
2302
|
+
return `${plugin.name} ${inputValues.join(" ")}`;
|
|
2303
|
+
}
|
|
2304
|
+
if (labels.showIndicatorNames) {
|
|
2305
|
+
return plugin.name;
|
|
2306
|
+
}
|
|
2307
|
+
return inputValues.join(" ");
|
|
2308
|
+
}).filter((entry) => entry.length > 0);
|
|
2309
|
+
if (labelEntries.length > 0) {
|
|
2310
|
+
const prevFont = ctx.font;
|
|
2311
|
+
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
2312
|
+
const legendText = labelEntries.join(" ");
|
|
2313
|
+
drawText(legendText, chartLeft + 10, chartTop + 10, "left", "top", labels.indicatorTextColor);
|
|
2314
|
+
ctx.font = prevFont;
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2086
2317
|
for (let index = tickStartIndex; index <= visibleTickEnd; index += xStep) {
|
|
2087
2318
|
const tickTime = getTimeForIndex(index);
|
|
2088
2319
|
if (!tickTime) {
|
|
@@ -2098,6 +2329,16 @@ function createChart(element, options = {}) {
|
|
|
2098
2329
|
drawText(timeLabel, x, fullChartBottom + 8, "center", "top", xAxis.textColor);
|
|
2099
2330
|
ctx.font = prevFont;
|
|
2100
2331
|
}
|
|
2332
|
+
if (labels.visible && labels.showCountdownToBarClose && lastPoint) {
|
|
2333
|
+
const stepMs = getTimeStepMs();
|
|
2334
|
+
const rawRemainingMs = lastPoint.time.getTime() + stepMs - Date.now();
|
|
2335
|
+
const countdownMs = rawRemainingMs >= 0 && rawRemainingMs <= stepMs * 2 ? rawRemainingMs : stepMs - Date.now() % stepMs;
|
|
2336
|
+
const countdownText = formatDuration(countdownMs);
|
|
2337
|
+
const prevFont = ctx.font;
|
|
2338
|
+
ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
2339
|
+
drawText(countdownText, chartRight + 6, fullChartBottom + 8, "left", "top", xAxis.textColor);
|
|
2340
|
+
ctx.font = prevFont;
|
|
2341
|
+
}
|
|
2101
2342
|
if (crosshair.visible && crosshairPoint) {
|
|
2102
2343
|
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
2103
2344
|
const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
|