@trdrs/quickcharts 4.0.0 → 5.0.0
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/CHANGELOG.md +106 -0
- package/README.md +325 -79
- package/dist/adapters/rest.d.ts +4 -4
- package/dist/adapters/rest.js +1 -1
- package/dist/adapters/rest.js.map +1 -1
- package/dist/ar-TG26ZCCE.js +1322 -0
- package/dist/ar-TG26ZCCE.js.map +1 -0
- package/dist/{assets-Gg-UquDA.d.ts → assets-DF3nozNV.d.ts} +167 -18
- package/dist/ca_ES-WJWH5BXU.js +1322 -0
- package/dist/ca_ES-WJWH5BXU.js.map +1 -0
- package/dist/{chunk-D3S26ARP.js → chunk-EXJAJZTN.js} +1 -1
- package/dist/chunk-EXJAJZTN.js.map +1 -0
- package/dist/{chunk-C4MEVUNK.js → chunk-Q3DCXQDW.js} +992 -284
- package/dist/chunk-Q3DCXQDW.js.map +1 -0
- package/dist/de-SUDTTPQ3.js +1322 -0
- package/dist/de-SUDTTPQ3.js.map +1 -0
- package/dist/drawings.d.ts +18 -7
- package/dist/drawings.js +7 -1
- package/dist/es-FOBXUK2X.js +1322 -0
- package/dist/es-FOBXUK2X.js.map +1 -0
- package/dist/feature-manifest.json +105 -9
- package/dist/fr-7KQC4IJH.js +1322 -0
- package/dist/fr-7KQC4IJH.js.map +1 -0
- package/dist/he_IL-2T6XBA6D.js +1322 -0
- package/dist/he_IL-2T6XBA6D.js.map +1 -0
- package/dist/id_ID-SI25N57C.js +1322 -0
- package/dist/id_ID-SI25N57C.js.map +1 -0
- package/dist/index.d.ts +952 -176
- package/dist/index.js +11905 -7472
- package/dist/index.js.map +1 -1
- package/dist/it-PMSTWZDL.js +1322 -0
- package/dist/it-PMSTWZDL.js.map +1 -0
- package/dist/ja-PABDRAXV.js +1322 -0
- package/dist/ja-PABDRAXV.js.map +1 -0
- package/dist/ko-JHRYYFCV.js +1322 -0
- package/dist/ko-JHRYYFCV.js.map +1 -0
- package/dist/ms_MY-U4EE5Q4V.js +1322 -0
- package/dist/ms_MY-U4EE5Q4V.js.map +1 -0
- package/dist/pl-4HCG3T3J.js +1322 -0
- package/dist/pl-4HCG3T3J.js.map +1 -0
- package/dist/pt-6LTNN653.js +1322 -0
- package/dist/pt-6LTNN653.js.map +1 -0
- package/dist/quickcharts.css +627 -208
- package/dist/{resources-Le6flcEn.d.ts → resources-XglhzzW0.d.ts} +5 -3
- package/dist/rest-openapi.json +16 -11
- package/dist/ru-LFHCSDBV.js +1322 -0
- package/dist/ru-LFHCSDBV.js.map +1 -0
- package/dist/sv-4XVUXYK6.js +1322 -0
- package/dist/sv-4XVUXYK6.js.map +1 -0
- package/dist/{symbols-6V22WaRX.d.ts → symbols-DdHLHQFC.d.ts} +77 -8
- package/dist/symbols.d.ts +1 -1
- package/dist/th-WM2PVA24.js +1322 -0
- package/dist/th-WM2PVA24.js.map +1 -0
- package/dist/theme-manifest.json +105 -21
- package/dist/tr-HMAZXSJM.js +1322 -0
- package/dist/tr-HMAZXSJM.js.map +1 -0
- package/dist/vi-ITFBV5B7.js +1322 -0
- package/dist/vi-ITFBV5B7.js.map +1 -0
- package/dist/zh-ZGU2RI7K.js +1322 -0
- package/dist/zh-ZGU2RI7K.js.map +1 -0
- package/dist/zh_TW-PNINSQ6F.js +1322 -0
- package/dist/zh_TW-PNINSQ6F.js.map +1 -0
- package/package.json +1 -1
- package/dist/ar-FXZ5QJUR.js +0 -1178
- package/dist/ar-FXZ5QJUR.js.map +0 -1
- package/dist/ca_ES-3WVKFBIF.js +0 -1178
- package/dist/ca_ES-3WVKFBIF.js.map +0 -1
- package/dist/chunk-C4MEVUNK.js.map +0 -1
- package/dist/chunk-D3S26ARP.js.map +0 -1
- package/dist/de-V72Q3OI7.js +0 -1178
- package/dist/de-V72Q3OI7.js.map +0 -1
- package/dist/es-CNQ3OJ6E.js +0 -1178
- package/dist/es-CNQ3OJ6E.js.map +0 -1
- package/dist/fr-5WQBCGR4.js +0 -1178
- package/dist/fr-5WQBCGR4.js.map +0 -1
- package/dist/he_IL-WXKA77UE.js +0 -1178
- package/dist/he_IL-WXKA77UE.js.map +0 -1
- package/dist/id_ID-XPBAYSWK.js +0 -1178
- package/dist/id_ID-XPBAYSWK.js.map +0 -1
- package/dist/it-DR2EBNWX.js +0 -1178
- package/dist/it-DR2EBNWX.js.map +0 -1
- package/dist/ja-URB2CPXO.js +0 -1178
- package/dist/ja-URB2CPXO.js.map +0 -1
- package/dist/ko-OE47NYVP.js +0 -1178
- package/dist/ko-OE47NYVP.js.map +0 -1
- package/dist/ms_MY-BKCFRK2K.js +0 -1178
- package/dist/ms_MY-BKCFRK2K.js.map +0 -1
- package/dist/pl-3IKQRG3Y.js +0 -1178
- package/dist/pl-3IKQRG3Y.js.map +0 -1
- package/dist/pt-PZHV5EZE.js +0 -1178
- package/dist/pt-PZHV5EZE.js.map +0 -1
- package/dist/ru-BAL6RQUV.js +0 -1178
- package/dist/ru-BAL6RQUV.js.map +0 -1
- package/dist/sv-Y6GBGDHM.js +0 -1178
- package/dist/sv-Y6GBGDHM.js.map +0 -1
- package/dist/th-MAMLMZKU.js +0 -1178
- package/dist/th-MAMLMZKU.js.map +0 -1
- package/dist/tr-3LQHINUZ.js +0 -1178
- package/dist/tr-3LQHINUZ.js.map +0 -1
- package/dist/vi-GR4PSDKP.js +0 -1178
- package/dist/vi-GR4PSDKP.js.map +0 -1
- package/dist/zh-2RJ5VNAI.js +0 -1178
- package/dist/zh-2RJ5VNAI.js.map +0 -1
- package/dist/zh_TW-XEBSACHU.js +0 -1178
- package/dist/zh_TW-XEBSACHU.js.map +0 -1
|
@@ -187,23 +187,25 @@ function lineLeft(block, index, align, width = block.width) {
|
|
|
187
187
|
var LIGHT_THEME = {
|
|
188
188
|
"canvas.background": "#ffffff",
|
|
189
189
|
"canvas.paneBorder": "#ebebeb",
|
|
190
|
+
"canvas.watermark": "rgba(80, 83, 94, 0.2)",
|
|
190
191
|
"series.up": "#089981",
|
|
191
192
|
"series.down": "#f23645",
|
|
192
193
|
"series.neutral": "#787b86",
|
|
193
|
-
"scale.grid": "rgba(
|
|
194
|
+
"scale.grid": "rgba(46, 46, 46, 0.2)",
|
|
194
195
|
"scale.background": "#ffffff",
|
|
195
|
-
"scale.border": "
|
|
196
|
-
"scale.text": "#
|
|
197
|
-
"scale.crosshair": "#
|
|
198
|
-
"scale.crosshairLabelBackground": "#
|
|
196
|
+
"scale.border": "rgba(46, 46, 46, 0)",
|
|
197
|
+
"scale.text": "#0f0f0f",
|
|
198
|
+
"scale.crosshair": "#9c9c9c",
|
|
199
|
+
"scale.crosshairLabelBackground": "#0f0f0f",
|
|
199
200
|
"scale.crosshairLabelText": "#ffffff",
|
|
200
|
-
"scale.sessionPreMarket": "rgba(
|
|
201
|
-
"scale.sessionExtended": "rgba(
|
|
202
|
-
"scale.sessionAfterHours": "rgba(
|
|
201
|
+
"scale.sessionPreMarket": "rgba(255, 152, 0, 0.08)",
|
|
202
|
+
"scale.sessionExtended": "rgba(213, 0, 249, 0.08)",
|
|
203
|
+
"scale.sessionAfterHours": "rgba(41, 98, 255, 0.08)",
|
|
203
204
|
"scale.sessionClosed": "rgba(0, 0, 0, 0.05)",
|
|
204
205
|
"text.primary": "#0f0f0f",
|
|
205
206
|
"text.secondary": "#4a4a4a",
|
|
206
207
|
"text.muted": "#6a6a6a",
|
|
208
|
+
"text.caption": "#707070",
|
|
207
209
|
"text.disabled": "#b8b8b8",
|
|
208
210
|
"text.inverse": "#ffffff",
|
|
209
211
|
"text.link": "#2962ff",
|
|
@@ -229,6 +231,8 @@ var LIGHT_THEME = {
|
|
|
229
231
|
"overlay.separator": "#ebebeb",
|
|
230
232
|
"overlay.shadow": "0 2px 4px rgba(0, 0, 0, 0.2)",
|
|
231
233
|
"overlay.scrim": "rgba(156, 156, 156, 0.5)",
|
|
234
|
+
"overlay.tooltip": "#3d3d3d",
|
|
235
|
+
"overlay.tooltipInk": "#f2f2f2",
|
|
232
236
|
"state.accent": "#2962ff",
|
|
233
237
|
"state.hover": "#f2f2f2",
|
|
234
238
|
"state.hoverInk": "#0f0f0f",
|
|
@@ -256,6 +260,8 @@ var LIGHT_THEME = {
|
|
|
256
260
|
"control.selectEdgeHover": "#a8a8a8",
|
|
257
261
|
"control.fieldInvalid": "#f23645",
|
|
258
262
|
"control.fieldFill": "#f2f2f2",
|
|
263
|
+
"control.tipMark": "#a8a8a8",
|
|
264
|
+
"control.tipMarkHover": "#707070",
|
|
259
265
|
"status.positive": "#078671",
|
|
260
266
|
"status.negative": "#df323f",
|
|
261
267
|
"status.warning": "#ac6600",
|
|
@@ -290,23 +296,25 @@ var LIGHT_THEME = {
|
|
|
290
296
|
var DARK_THEME = {
|
|
291
297
|
"canvas.background": "#0f0f0f",
|
|
292
298
|
"canvas.paneBorder": "#2e2e2e",
|
|
299
|
+
"canvas.watermark": "rgba(80, 83, 94, 0.3)",
|
|
293
300
|
"series.up": "#089981",
|
|
294
301
|
"series.down": "#f23645",
|
|
295
302
|
"series.neutral": "#787b86",
|
|
296
|
-
"scale.grid": "rgba(
|
|
303
|
+
"scale.grid": "rgba(242, 242, 242, 0.2)",
|
|
297
304
|
"scale.background": "#0f0f0f",
|
|
298
|
-
"scale.border": "
|
|
299
|
-
"scale.text": "#
|
|
300
|
-
"scale.crosshair": "#
|
|
301
|
-
"scale.crosshairLabelBackground": "#
|
|
302
|
-
"scale.crosshairLabelText": "#
|
|
303
|
-
"scale.sessionPreMarket": "rgba(
|
|
304
|
-
"scale.sessionExtended": "rgba(
|
|
305
|
-
"scale.sessionAfterHours": "rgba(
|
|
305
|
+
"scale.border": "rgba(242, 242, 242, 0)",
|
|
306
|
+
"scale.text": "#b8b8b8",
|
|
307
|
+
"scale.crosshair": "#9c9c9c",
|
|
308
|
+
"scale.crosshairLabelBackground": "#3d3d3d",
|
|
309
|
+
"scale.crosshairLabelText": "#ffffff",
|
|
310
|
+
"scale.sessionPreMarket": "rgba(255, 152, 0, 0.08)",
|
|
311
|
+
"scale.sessionExtended": "rgba(213, 0, 249, 0.08)",
|
|
312
|
+
"scale.sessionAfterHours": "rgba(41, 98, 255, 0.08)",
|
|
306
313
|
"scale.sessionClosed": "rgba(0, 0, 0, 0.22)",
|
|
307
314
|
"text.primary": "#dbdbdb",
|
|
308
315
|
"text.secondary": "#b8b8b8",
|
|
309
316
|
"text.muted": "#9b9b9b",
|
|
317
|
+
"text.caption": "#8c8c8c",
|
|
310
318
|
"text.disabled": "#575757",
|
|
311
319
|
"text.inverse": "#0f0f0f",
|
|
312
320
|
"text.link": "#5b9cf6",
|
|
@@ -332,6 +340,8 @@ var DARK_THEME = {
|
|
|
332
340
|
"overlay.separator": "#4a4a4a",
|
|
333
341
|
"overlay.shadow": "0 2px 4px rgba(0, 0, 0, 0.4)",
|
|
334
342
|
"overlay.scrim": "rgba(0, 0, 0, 0.5)",
|
|
343
|
+
"overlay.tooltip": "#3d3d3d",
|
|
344
|
+
"overlay.tooltipInk": "#f2f2f2",
|
|
335
345
|
"state.accent": "#2962ff",
|
|
336
346
|
"state.hover": "#2e2e2e",
|
|
337
347
|
"state.hoverInk": "#c9c9c9",
|
|
@@ -359,6 +369,8 @@ var DARK_THEME = {
|
|
|
359
369
|
"control.selectEdgeHover": "#808080",
|
|
360
370
|
"control.fieldInvalid": "#f23645",
|
|
361
371
|
"control.fieldFill": "#2e2e2e",
|
|
372
|
+
"control.tipMark": "#575757",
|
|
373
|
+
"control.tipMarkHover": "#8c8c8c",
|
|
362
374
|
"status.positive": "#089981",
|
|
363
375
|
"status.negative": "#f34452",
|
|
364
376
|
"status.warning": "#ff9800",
|
|
@@ -394,6 +406,25 @@ var BUILT_IN_THEMES = Object.freeze({
|
|
|
394
406
|
light: LIGHT_THEME,
|
|
395
407
|
dark: DARK_THEME
|
|
396
408
|
});
|
|
409
|
+
var CHART_FACTORY_COLORS = Object.freeze({
|
|
410
|
+
line: "#2962ff",
|
|
411
|
+
lineGradientTop: "#d500f9",
|
|
412
|
+
lineGradientBottom: "#00bce5",
|
|
413
|
+
areaTop: "rgba(41, 98, 255, 0.28)",
|
|
414
|
+
areaBottom: "rgba(41, 98, 255, 0)",
|
|
415
|
+
sessionBreaks: "#4985e7",
|
|
416
|
+
previousClose: "#555555",
|
|
417
|
+
highLowLine: "#808080",
|
|
418
|
+
highLowLabel: "#142e61",
|
|
419
|
+
bid: "#2962ff",
|
|
420
|
+
ask: "#f7525f",
|
|
421
|
+
scaleLabelText: "#ffffff",
|
|
422
|
+
preMarketLabel: "#fb8c00",
|
|
423
|
+
postMarketLabel: "#2962ff",
|
|
424
|
+
nightLabel: "#8e24aa"
|
|
425
|
+
});
|
|
426
|
+
var BRAND_UP = "#4c98fb";
|
|
427
|
+
var BRAND_DOWN = "#f23645";
|
|
397
428
|
|
|
398
429
|
// src/internal/drawings/core/inks.ts
|
|
399
430
|
function drawingInksOf(theme) {
|
|
@@ -679,16 +710,19 @@ var DrawingPaneView = class {
|
|
|
679
710
|
drawing2.paintConstruction(ctx, viewport);
|
|
680
711
|
}
|
|
681
712
|
const state = drawing2.state;
|
|
713
|
+
const inks = drawing2.inks();
|
|
714
|
+
const handleInks = { ring: inks.handleRing, center: inks.handleCenter };
|
|
682
715
|
if (state === "selected" || state === "editing") {
|
|
716
|
+
if (drawing2.isValid()) drawing2.paintTextHint(ctx, viewport);
|
|
683
717
|
const points = drawing2.getControlPoints(viewport);
|
|
684
|
-
const inks = drawing2.inks();
|
|
685
|
-
const handleInks = { ring: inks.handleRing, center: inks.handleCenter };
|
|
686
718
|
if (points.length > 0) paintHandles(ctx, points, handleInks, drawing2.handleShape(), hoveredIndex(points, drawing2.pointer, HANDLE_REACH));
|
|
687
719
|
const grips = drawing2.resizeHandles(viewport);
|
|
688
720
|
const gripShape = drawing2.gripShape();
|
|
689
721
|
if (grips.length > 0 && gripShape) paintHandles(ctx, grips, handleInks, gripShape, hoveredIndex(grips, drawing2.pointer, GRIP_REACH));
|
|
690
722
|
else if (grips.length > 0) paintResizeGrips(ctx, grips, drawing2.style.lineColor);
|
|
691
|
-
|
|
723
|
+
} else if (drawing2.hovered && drawing2.handlesOnHover() && drawing2.isValid()) {
|
|
724
|
+
const points = drawing2.getControlPoints(viewport);
|
|
725
|
+
if (points.length > 0) paintHandles(ctx, points, handleInks, drawing2.handleShape(), null, "thin");
|
|
692
726
|
}
|
|
693
727
|
} finally {
|
|
694
728
|
ctx.restore();
|
|
@@ -698,6 +732,66 @@ var DrawingPaneView = class {
|
|
|
698
732
|
}
|
|
699
733
|
};
|
|
700
734
|
|
|
735
|
+
// src/internal/drawings/render/axis-view.ts
|
|
736
|
+
var AxisLabel = class {
|
|
737
|
+
_source;
|
|
738
|
+
constructor(source) {
|
|
739
|
+
this._source = source;
|
|
740
|
+
}
|
|
741
|
+
coordinate() {
|
|
742
|
+
return this._source.coordinate() ?? -1e6;
|
|
743
|
+
}
|
|
744
|
+
text() {
|
|
745
|
+
return this._source.text();
|
|
746
|
+
}
|
|
747
|
+
textColor() {
|
|
748
|
+
return this._source.textColor?.() ?? "#ffffff";
|
|
749
|
+
}
|
|
750
|
+
backColor() {
|
|
751
|
+
return this._source.color();
|
|
752
|
+
}
|
|
753
|
+
visible() {
|
|
754
|
+
return this._source.visible() && this._source.coordinate() !== null;
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
var AxisBandView = class {
|
|
758
|
+
_source;
|
|
759
|
+
_axis;
|
|
760
|
+
constructor(source, axis) {
|
|
761
|
+
this._source = source;
|
|
762
|
+
this._axis = axis;
|
|
763
|
+
}
|
|
764
|
+
zOrder() {
|
|
765
|
+
return "bottom";
|
|
766
|
+
}
|
|
767
|
+
renderer() {
|
|
768
|
+
return this;
|
|
769
|
+
}
|
|
770
|
+
draw(target) {
|
|
771
|
+
const span = this._source.span();
|
|
772
|
+
if (!span || span.coordinates.length === 0) return;
|
|
773
|
+
target.useMediaCoordinateSpace(({ context: ctx, mediaSize }) => {
|
|
774
|
+
paintAxisBand(ctx, this._axis, span.coordinates, span.color, mediaSize);
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
function paintAxisBand(ctx, axis, coordinates, color, size) {
|
|
779
|
+
const low = Math.min(...coordinates);
|
|
780
|
+
const high = Math.max(...coordinates);
|
|
781
|
+
ctx.save();
|
|
782
|
+
ctx.fillStyle = color;
|
|
783
|
+
if (axis === "price") {
|
|
784
|
+
const top = Math.round(low) + 1;
|
|
785
|
+
const bottom = Math.round(high) - 1;
|
|
786
|
+
if (bottom > top) ctx.fillRect(1, top, size.width, bottom - top);
|
|
787
|
+
else ctx.fillRect(1, Math.round(high) - 1, size.width, 1);
|
|
788
|
+
} else {
|
|
789
|
+
const left = Math.floor(low);
|
|
790
|
+
ctx.fillRect(left, 0, Math.max(1, Math.ceil(high) - left), size.height);
|
|
791
|
+
}
|
|
792
|
+
ctx.restore();
|
|
793
|
+
}
|
|
794
|
+
|
|
701
795
|
// src/i18n/en/drawing.ts
|
|
702
796
|
var drawing = {
|
|
703
797
|
// The toolbar itself, and its tool GROUPS (a button each) with the SECTION headings inside their
|
|
@@ -808,6 +902,8 @@ var drawing = {
|
|
|
808
902
|
"drawing.modifierCommand": "Cmd",
|
|
809
903
|
"drawing.hintClone": "{modifier} + Drag",
|
|
810
904
|
"drawing.hintCopy": "{modifier} + C",
|
|
905
|
+
"drawing.hintRemove": "Del",
|
|
906
|
+
"drawing.drawingMenu": "Drawing menu",
|
|
811
907
|
"drawing.hintPaste": "{modifier} + V",
|
|
812
908
|
"drawing.deleteWithKey": "Delete (Del)",
|
|
813
909
|
"drawing.statusDrawingsHidden": "Drawings hidden",
|
|
@@ -902,6 +998,7 @@ var drawing = {
|
|
|
902
998
|
"drawing.sectionInfo": "Info",
|
|
903
999
|
"drawing.priceRange": "Price range",
|
|
904
1000
|
"drawing.barsRange": "Bars range",
|
|
1001
|
+
"drawing.distance": "Distance",
|
|
905
1002
|
"drawing.angle": "Angle",
|
|
906
1003
|
"drawing.statsPosition": "Stats position",
|
|
907
1004
|
"drawing.statsHidden": "Hidden",
|
|
@@ -1176,6 +1273,30 @@ var drawing = {
|
|
|
1176
1273
|
"drawing.imageErrorDecode": "That image could not be opened. It may be damaged."
|
|
1177
1274
|
};
|
|
1178
1275
|
|
|
1276
|
+
// src/paneGeometry.ts
|
|
1277
|
+
function paneIndexOf(series) {
|
|
1278
|
+
try {
|
|
1279
|
+
const index = series.getPane().paneIndex();
|
|
1280
|
+
return Number.isInteger(index) && index >= 0 ? index : 0;
|
|
1281
|
+
} catch {
|
|
1282
|
+
return 0;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
function paneTopIn(chart, index, box2) {
|
|
1286
|
+
if (index <= 0) return 0;
|
|
1287
|
+
try {
|
|
1288
|
+
const panes = chart.panes();
|
|
1289
|
+
const rect = panes[index]?.getHTMLElement()?.getBoundingClientRect();
|
|
1290
|
+
if (rect && rect.height > 0) return rect.top - box2.getBoundingClientRect().top;
|
|
1291
|
+
let top = 0;
|
|
1292
|
+
for (let i = 0; i < index && i < panes.length; i++) top += panes[i].getHeight();
|
|
1293
|
+
return top;
|
|
1294
|
+
} catch {
|
|
1295
|
+
return 0;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
var seriesPaneTop = (chart, series, box2) => paneTopIn(chart, paneIndexOf(series), box2);
|
|
1299
|
+
|
|
1179
1300
|
// src/internal/drawings/core/drawing.ts
|
|
1180
1301
|
function normalizeOptions(patch) {
|
|
1181
1302
|
return { ...DEFAULT_OPTIONS, ...patch, visibility: normalizeVisibility(patch.visibility) };
|
|
@@ -1185,7 +1306,7 @@ function viewportOf(chart, series) {
|
|
|
1185
1306
|
let height;
|
|
1186
1307
|
try {
|
|
1187
1308
|
width = chart.timeScale().width();
|
|
1188
|
-
height = chart.paneSize().height;
|
|
1309
|
+
height = chart.paneSize(paneIndexOf(series)).height;
|
|
1189
1310
|
} catch {
|
|
1190
1311
|
return null;
|
|
1191
1312
|
}
|
|
@@ -1279,6 +1400,13 @@ function viewportOf(chart, series) {
|
|
|
1279
1400
|
};
|
|
1280
1401
|
}
|
|
1281
1402
|
var UNRESOLVED_PRICE_TEXT = (price) => price.toFixed(2);
|
|
1403
|
+
function plainTime(time) {
|
|
1404
|
+
const t = Number(time);
|
|
1405
|
+
if (!Number.isFinite(t)) return String(time);
|
|
1406
|
+
const d = new Date(t * 1e3);
|
|
1407
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1408
|
+
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`;
|
|
1409
|
+
}
|
|
1282
1410
|
var Drawing = class {
|
|
1283
1411
|
id;
|
|
1284
1412
|
/** Set when this drawing belongs to ONE surface; undefined when every surface showing the symbol
|
|
@@ -1296,6 +1424,8 @@ var Drawing = class {
|
|
|
1296
1424
|
_series = null;
|
|
1297
1425
|
_requestUpdate = null;
|
|
1298
1426
|
_paneViews;
|
|
1427
|
+
_priceBands;
|
|
1428
|
+
_timeBands;
|
|
1299
1429
|
constructor(id, anchors = [], style = {}, options = {}, props = {}) {
|
|
1300
1430
|
this.id = id;
|
|
1301
1431
|
this._anchors = this.upgradeAnchors(anchors.map((a) => ({ ...a })));
|
|
@@ -1303,6 +1433,8 @@ var Drawing = class {
|
|
|
1303
1433
|
this._options = normalizeOptions(options);
|
|
1304
1434
|
this._props = { ...this.defaultProps(), ...this.upgradeProps(props) };
|
|
1305
1435
|
this._paneViews = [new DrawingPaneView(this)];
|
|
1436
|
+
this._priceBands = [new AxisBandView({ span: () => this.bandSpan("price") }, "price")];
|
|
1437
|
+
this._timeBands = [new AxisBandView({ span: () => this.bandSpan("time") }, "time")];
|
|
1306
1438
|
}
|
|
1307
1439
|
/** Tool-specific defaults. Must not read instance fields (runs during construction). */
|
|
1308
1440
|
defaultProps() {
|
|
@@ -1365,6 +1497,39 @@ var Drawing = class {
|
|
|
1365
1497
|
timeViews() {
|
|
1366
1498
|
return [];
|
|
1367
1499
|
}
|
|
1500
|
+
priceAxisPaneViews() {
|
|
1501
|
+
return this._priceBands;
|
|
1502
|
+
}
|
|
1503
|
+
timeAxisPaneViews() {
|
|
1504
|
+
return this._timeBands;
|
|
1505
|
+
}
|
|
1506
|
+
/** The prices and times a selected drawing marks on the axes with a band across their span, in
|
|
1507
|
+
* the accent at a quarter; none by default. */
|
|
1508
|
+
axisSpans() {
|
|
1509
|
+
return { prices: null, times: null };
|
|
1510
|
+
}
|
|
1511
|
+
/** The band a selected drawing lays on an axis: the coordinates its marked prices or times stand
|
|
1512
|
+
* at, or null where it lays none. */
|
|
1513
|
+
bandSpan(axis) {
|
|
1514
|
+
if (this._state !== "selected" && this._state !== "editing" || !this.isVisibleNow()) return null;
|
|
1515
|
+
const viewport = this.getViewport();
|
|
1516
|
+
if (!viewport) return null;
|
|
1517
|
+
const spans = this.axisSpans();
|
|
1518
|
+
const raw = axis === "price" ? (spans.prices ?? []).map((p) => viewport.yOf(p)) : (spans.times ?? []).map((t) => viewport.xOf(t));
|
|
1519
|
+
const coordinates = raw.filter((c) => c !== null && Number.isFinite(c));
|
|
1520
|
+
return coordinates.length > 0 ? { coordinates, color: withAlpha(this.inks().accent, 0.25) } : null;
|
|
1521
|
+
}
|
|
1522
|
+
/** A time as the chart writes it on its time axis: through the chart's own time formatter, or
|
|
1523
|
+
* its plain UTC date and time where the chart has none. */
|
|
1524
|
+
formatTime(time) {
|
|
1525
|
+
if (time === void 0) return "";
|
|
1526
|
+
try {
|
|
1527
|
+
const formatter = this._chart?.options().localization?.timeFormatter;
|
|
1528
|
+
if (formatter) return formatter(time);
|
|
1529
|
+
} catch {
|
|
1530
|
+
}
|
|
1531
|
+
return plainTime(time);
|
|
1532
|
+
}
|
|
1368
1533
|
hitTest(x, y) {
|
|
1369
1534
|
if (!this.isVisibleNow()) return null;
|
|
1370
1535
|
const viewport = this.getViewport();
|
|
@@ -1376,6 +1541,11 @@ var Drawing = class {
|
|
|
1376
1541
|
zOrder: "normal"
|
|
1377
1542
|
};
|
|
1378
1543
|
}
|
|
1544
|
+
/** The cursor a point over the drawing wears: a handle's, its words', or null for the plain
|
|
1545
|
+
* pointer. */
|
|
1546
|
+
cursorFor(point, viewport) {
|
|
1547
|
+
return this.cursorAt(point, viewport);
|
|
1548
|
+
}
|
|
1379
1549
|
/** Position-specific hover cursor (a table divider's col-resize); null = the default pointer. */
|
|
1380
1550
|
cursorAt(_point, _viewport) {
|
|
1381
1551
|
return null;
|
|
@@ -1561,6 +1731,11 @@ var Drawing = class {
|
|
|
1561
1731
|
resizeTo(_handleIndex, _point, _viewport) {
|
|
1562
1732
|
}
|
|
1563
1733
|
_textHint = null;
|
|
1734
|
+
/** Note where the invitation a paint just laid stands, where a press opens the words' editor; null
|
|
1735
|
+
* where none stands. */
|
|
1736
|
+
noteTextHint(region) {
|
|
1737
|
+
this._textHint = region;
|
|
1738
|
+
}
|
|
1564
1739
|
/** An inline text editor is open on this drawing (transient view state — never serialized). */
|
|
1565
1740
|
textEditing = false;
|
|
1566
1741
|
/**
|
|
@@ -1688,6 +1863,11 @@ var Drawing = class {
|
|
|
1688
1863
|
gripShape() {
|
|
1689
1864
|
return null;
|
|
1690
1865
|
}
|
|
1866
|
+
/** Whether the drawing shows its handles, in their thin form, while the pointer rests on it
|
|
1867
|
+
* unselected; a press on one then moves its point and selects the drawing. */
|
|
1868
|
+
handlesOnHover() {
|
|
1869
|
+
return false;
|
|
1870
|
+
}
|
|
1691
1871
|
_hovered = false;
|
|
1692
1872
|
_pointer = null;
|
|
1693
1873
|
/** Whether the pointer rests on this drawing (transient view state, never serialized). */
|
|
@@ -2143,28 +2323,207 @@ function cachedImageBitmap(key) {
|
|
|
2143
2323
|
return hit.complete && hit.naturalWidth > 0 ? hit : null;
|
|
2144
2324
|
}
|
|
2145
2325
|
|
|
2146
|
-
// src/internal/drawings/
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2326
|
+
// src/internal/drawings/core/savedLook.ts
|
|
2327
|
+
function endSavedLook(patch) {
|
|
2328
|
+
return "savedLook" in patch || Object.keys(patch).length === 0 ? patch : { ...patch, savedLook: null };
|
|
2329
|
+
}
|
|
2330
|
+
function savedLevels(look, key = "levels") {
|
|
2331
|
+
const levels = look[key];
|
|
2332
|
+
if (!Array.isArray(levels)) return [];
|
|
2333
|
+
return levels.filter((l) => !!l && typeof l === "object" && typeof l.value === "number").map((l) => ({
|
|
2334
|
+
value: l.value,
|
|
2335
|
+
visible: l.visible !== false,
|
|
2336
|
+
...typeof l.color === "string" ? { color: l.color } : {},
|
|
2337
|
+
...typeof l.text === "string" ? { text: l.text } : {}
|
|
2338
|
+
}));
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
// src/internal/drawings/render/lineWords.ts
|
|
2342
|
+
var wordsOffset = (fontSize) => fontSize / 2 + 4;
|
|
2343
|
+
var plusSize = (fontSize) => fontSize / 2 + 5;
|
|
2344
|
+
var plusGap = (fontSize) => fontSize / 2 - 3;
|
|
2345
|
+
function placeAt(place, along, across) {
|
|
2346
|
+
const cos = Math.cos(place.angle);
|
|
2347
|
+
const sin = Math.sin(place.angle);
|
|
2348
|
+
return { x: place.x + along * cos - across * sin, y: place.y + along * sin + across * cos };
|
|
2349
|
+
}
|
|
2350
|
+
function paintInvitation(ctx, words, place, style, measure) {
|
|
2351
|
+
const size = style.fontSize;
|
|
2352
|
+
const plus = plusSize(size);
|
|
2353
|
+
const gap = plusGap(size);
|
|
2354
|
+
const width = plus + gap + measure(words);
|
|
2355
|
+
const left = place.align === "left" ? 0 : place.align === "right" ? -width : -width / 2;
|
|
2356
|
+
const ink = withAlpha(style.textColor, 0.5);
|
|
2357
|
+
ctx.save();
|
|
2358
|
+
ctx.setLineDash([]);
|
|
2359
|
+
ctx.translate(place.x, place.y);
|
|
2360
|
+
if (place.angle) ctx.rotate(place.angle);
|
|
2361
|
+
ctx.font = fontOf(style);
|
|
2362
|
+
ctx.fillStyle = ink;
|
|
2363
|
+
ctx.textAlign = "left";
|
|
2364
|
+
ctx.textBaseline = "middle";
|
|
2365
|
+
ctx.fillText(words, left + plus + gap, 0);
|
|
2366
|
+
let px = left + plus / 2;
|
|
2367
|
+
let py = -0.5;
|
|
2368
|
+
if (!place.angle) {
|
|
2369
|
+
px = Math.round(place.x + px - 0.5) + 0.5 - place.x;
|
|
2370
|
+
py = Math.round(place.y - 1) + 0.5 - place.y;
|
|
2371
|
+
}
|
|
2372
|
+
ctx.strokeStyle = ink;
|
|
2373
|
+
ctx.lineWidth = 1;
|
|
2374
|
+
ctx.lineCap = "butt";
|
|
2375
|
+
ctx.lineJoin = "miter";
|
|
2376
|
+
ctx.beginPath();
|
|
2377
|
+
ctx.moveTo(px - plus / 2, py);
|
|
2378
|
+
ctx.lineTo(px + plus / 2, py);
|
|
2379
|
+
ctx.moveTo(px, py - plus / 2);
|
|
2380
|
+
ctx.lineTo(px, py + plus / 2);
|
|
2381
|
+
ctx.stroke();
|
|
2382
|
+
ctx.restore();
|
|
2383
|
+
const middle = placeAt(place, left + width / 2, 0);
|
|
2384
|
+
return { cx: middle.x, cy: middle.y, angle: place.angle, halfW: width / 2 + 2, halfH: size / 2 + 2 };
|
|
2385
|
+
}
|
|
2386
|
+
function inRegion(point, r) {
|
|
2387
|
+
const dx = point.x - r.cx;
|
|
2388
|
+
const dy = point.y - r.cy;
|
|
2389
|
+
const cos = Math.cos(-r.angle);
|
|
2390
|
+
const sin = Math.sin(-r.angle);
|
|
2391
|
+
return Math.abs(dx * cos - dy * sin) <= r.halfW && Math.abs(dx * sin + dy * cos) <= r.halfH;
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
// src/internal/drawings/render/statsBox.ts
|
|
2395
|
+
var STATS_GROUND = "rgba(70, 70, 70, 0.9)";
|
|
2396
|
+
var STATS_INK = "#ffffff";
|
|
2397
|
+
var MARK_INK = "#f9f9f9";
|
|
2398
|
+
var STATS_FONT_SIZE = 12;
|
|
2399
|
+
var PAD_Y = 8;
|
|
2400
|
+
var ROW = 26;
|
|
2401
|
+
var MARK_X = 21;
|
|
2402
|
+
var WORDS_X = 42;
|
|
2403
|
+
var PAD_END = 13;
|
|
2404
|
+
function statsBoxAt(at, rows, measure) {
|
|
2405
|
+
const widest = rows.reduce((w, r) => Math.max(w, measure(r.text)), 0);
|
|
2406
|
+
return { x: at.x, y: at.y, width: Math.round(WORDS_X + widest + PAD_END), height: PAD_Y * 2 + rows.length * ROW };
|
|
2407
|
+
}
|
|
2408
|
+
function paintStatsBox(ctx, box2, rows, font) {
|
|
2409
|
+
ctx.save();
|
|
2410
|
+
ctx.setLineDash([]);
|
|
2411
|
+
ctx.fillStyle = STATS_GROUND;
|
|
2412
|
+
ctx.beginPath();
|
|
2413
|
+
ctx.roundRect(box2.x, box2.y, box2.width, box2.height, 4);
|
|
2414
|
+
ctx.fill();
|
|
2415
|
+
ctx.font = font;
|
|
2416
|
+
ctx.textAlign = "start";
|
|
2417
|
+
ctx.textBaseline = "middle";
|
|
2418
|
+
rows.forEach((row, i) => {
|
|
2419
|
+
const middle = box2.y + PAD_Y + ROW / 2 + i * ROW;
|
|
2420
|
+
paintMark(ctx, row.kind, box2.x + MARK_X, middle);
|
|
2421
|
+
ctx.fillStyle = STATS_INK;
|
|
2422
|
+
ctx.fillText(row.text, box2.x + WORDS_X, middle);
|
|
2423
|
+
});
|
|
2424
|
+
ctx.restore();
|
|
2425
|
+
}
|
|
2426
|
+
function paintMark(ctx, kind, cx, cy) {
|
|
2427
|
+
ctx.strokeStyle = MARK_INK;
|
|
2428
|
+
ctx.lineWidth = 1;
|
|
2429
|
+
ctx.lineCap = "butt";
|
|
2430
|
+
ctx.lineJoin = "miter";
|
|
2431
|
+
ctx.beginPath();
|
|
2432
|
+
if (kind === "price") {
|
|
2433
|
+
for (const dy of [-6.5, 6.5]) {
|
|
2434
|
+
ctx.moveTo(cx - 6, cy + dy);
|
|
2435
|
+
ctx.lineTo(cx + 5, cy + dy);
|
|
2436
|
+
}
|
|
2437
|
+
ctx.moveTo(cx - 0.5, cy - 5);
|
|
2438
|
+
ctx.lineTo(cx - 0.5, cy + 5);
|
|
2439
|
+
for (const s of [-1, 1]) {
|
|
2440
|
+
ctx.moveTo(cx - 3, cy + s * 2);
|
|
2441
|
+
ctx.lineTo(cx - 0.5, cy + s * 4.5);
|
|
2442
|
+
ctx.lineTo(cx + 2, cy + s * 2);
|
|
2443
|
+
}
|
|
2444
|
+
} else if (kind === "span") {
|
|
2445
|
+
for (const x of [-7.5, 5.5]) {
|
|
2446
|
+
ctx.rect(cx + x, cy - 3.5, 2, 8);
|
|
2447
|
+
ctx.moveTo(cx + x + 1, cy - 6);
|
|
2448
|
+
ctx.lineTo(cx + x + 1, cy - 4);
|
|
2449
|
+
ctx.moveTo(cx + x + 1, cy + 5);
|
|
2450
|
+
ctx.lineTo(cx + x + 1, cy + 7);
|
|
2451
|
+
}
|
|
2452
|
+
ctx.moveTo(cx - 3.5, cy + 0.5);
|
|
2453
|
+
ctx.lineTo(cx + 3.5, cy + 0.5);
|
|
2454
|
+
for (const s of [-1, 1]) {
|
|
2455
|
+
ctx.moveTo(cx + s, cy - 2);
|
|
2456
|
+
ctx.lineTo(cx + s * 3.5, cy + 0.5);
|
|
2457
|
+
ctx.lineTo(cx + s, cy + 3);
|
|
2458
|
+
}
|
|
2459
|
+
} else {
|
|
2460
|
+
const corner = { x: cx - 5.5, y: cy + 5.5 };
|
|
2461
|
+
const ray = 64.7 * Math.PI / 180;
|
|
2462
|
+
ctx.moveTo(cx - 6, corner.y);
|
|
2463
|
+
ctx.lineTo(cx + 6, corner.y);
|
|
2464
|
+
ctx.moveTo(corner.x, corner.y);
|
|
2465
|
+
ctx.lineTo(corner.x + 12.5 * Math.cos(ray), corner.y - 12.5 * Math.sin(ray));
|
|
2466
|
+
ctx.moveTo(corner.x + 7, corner.y);
|
|
2467
|
+
ctx.arc(corner.x, corner.y, 7, 0, -ray, true);
|
|
2157
2468
|
}
|
|
2158
|
-
|
|
2159
|
-
|
|
2469
|
+
ctx.stroke();
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
// src/internal/drawings/render/textEntry.ts
|
|
2473
|
+
var SELECTION_WASH = "rgba(255, 255, 255, 0.2)";
|
|
2474
|
+
var CARET_WIDTH = 2;
|
|
2475
|
+
var lineMiddle = (lineHeight) => lineHeight / 2 + 1;
|
|
2476
|
+
var caretHeight = (font, lineHeight) => Math.min(lineHeight, Number(/(\d+(?:\.\d+)?)px/.exec(font)?.[1]) || lineHeight);
|
|
2477
|
+
function paintTextEntry(ctx, p) {
|
|
2478
|
+
const draft = p.draft;
|
|
2479
|
+
const shown = p.placeholder ? p.placeholder.block : p.block;
|
|
2480
|
+
const lineTop = (i) => p.y + i * p.lineHeight;
|
|
2481
|
+
const left = (block, i) => p.x + lineLeft(block, i, p.align, p.width);
|
|
2482
|
+
ctx.save();
|
|
2483
|
+
ctx.setLineDash([]);
|
|
2484
|
+
ctx.direction = "ltr";
|
|
2485
|
+
if (draft && draft.selectionStart !== draft.selectionEnd && !p.placeholder) {
|
|
2486
|
+
ctx.fillStyle = SELECTION_WASH;
|
|
2487
|
+
for (const span of runSpans(p.block, draft.selectionStart, draft.selectionEnd, p.measure)) {
|
|
2488
|
+
const from = Math.round(left(p.block, span.line) + span.x0);
|
|
2489
|
+
const to = Math.ceil(left(p.block, span.line) + span.x1);
|
|
2490
|
+
if (to > from) ctx.fillRect(from, lineTop(span.line), to - from, p.lineHeight);
|
|
2491
|
+
}
|
|
2160
2492
|
}
|
|
2161
|
-
|
|
2162
|
-
|
|
2493
|
+
ctx.font = p.font;
|
|
2494
|
+
ctx.fillStyle = p.color;
|
|
2495
|
+
ctx.textAlign = "left";
|
|
2496
|
+
ctx.textBaseline = "middle";
|
|
2497
|
+
const alpha = ctx.globalAlpha;
|
|
2498
|
+
if (p.placeholder) ctx.globalAlpha = alpha * p.placeholder.alpha;
|
|
2499
|
+
shown.lines.forEach((l, i) => {
|
|
2500
|
+
if (l.text === "") return;
|
|
2501
|
+
ctx.fillText(l.text, left(shown, i), lineTop(i) + lineMiddle(p.lineHeight));
|
|
2502
|
+
});
|
|
2503
|
+
ctx.globalAlpha = alpha;
|
|
2504
|
+
if (draft?.composition && !p.placeholder) {
|
|
2505
|
+
for (const span of runSpans(p.block, draft.composition.start, draft.composition.end, p.measure)) {
|
|
2506
|
+
const from = Math.round(left(p.block, span.line) + span.x0);
|
|
2507
|
+
const to = Math.ceil(left(p.block, span.line) + span.x1);
|
|
2508
|
+
if (to > from) ctx.fillRect(from, lineTop(span.line) + p.lineHeight - 1, to - from, 1);
|
|
2509
|
+
}
|
|
2163
2510
|
}
|
|
2164
|
-
|
|
2165
|
-
|
|
2511
|
+
if (draft?.caret && draft.selectionStart === draft.selectionEnd) {
|
|
2512
|
+
const at = caretPlace(p.block, draft.selectionEnd, p.measure);
|
|
2513
|
+
const tall = caretHeight(p.font, p.lineHeight);
|
|
2514
|
+
ctx.fillRect(Math.round(left(p.block, at.line) + at.x) - CARET_WIDTH / 2, lineTop(at.line) + Math.round((p.lineHeight - tall) / 2), CARET_WIDTH, tall);
|
|
2166
2515
|
}
|
|
2167
|
-
|
|
2516
|
+
ctx.restore();
|
|
2517
|
+
}
|
|
2518
|
+
function paintWordsFrame(ctx, box2, color, alpha) {
|
|
2519
|
+
ctx.save();
|
|
2520
|
+
ctx.setLineDash([]);
|
|
2521
|
+
ctx.globalAlpha = ctx.globalAlpha * alpha;
|
|
2522
|
+
ctx.strokeStyle = color;
|
|
2523
|
+
ctx.lineWidth = 2;
|
|
2524
|
+
ctx.strokeRect(box2.x - 1, box2.y - 1, box2.width + 2, box2.height + 2);
|
|
2525
|
+
ctx.restore();
|
|
2526
|
+
}
|
|
2168
2527
|
|
|
2169
2528
|
// src/internal/drawings/tools/lines.ts
|
|
2170
2529
|
var LINE_PROPS = {
|
|
@@ -2180,39 +2539,227 @@ var LINE_PROPS = {
|
|
|
2180
2539
|
showPipsChange: false,
|
|
2181
2540
|
showBarsRange: false,
|
|
2182
2541
|
showDateTimeRange: false,
|
|
2542
|
+
showDistance: false,
|
|
2183
2543
|
showAngle: false,
|
|
2184
2544
|
statsPosition: "right",
|
|
2185
2545
|
alwaysShowStats: false,
|
|
2186
2546
|
textVAlign: "top",
|
|
2187
|
-
textHAlign: "center"
|
|
2547
|
+
textHAlign: "center",
|
|
2548
|
+
savedLook: null
|
|
2188
2549
|
};
|
|
2189
2550
|
var ACROSS = {
|
|
2190
2551
|
top: { y: -4, baseline: "bottom" },
|
|
2191
2552
|
middle: { y: 0, baseline: "middle" },
|
|
2192
2553
|
bottom: { y: 4, baseline: "top" }
|
|
2193
2554
|
};
|
|
2555
|
+
var LEVEL_WORDS_INSET = 7;
|
|
2556
|
+
var LEVEL_HANDLE_AT = 0.9;
|
|
2194
2557
|
function hitTolerance(lineWidth) {
|
|
2195
2558
|
return Math.max(6, lineWidth / 2 + 4);
|
|
2196
2559
|
}
|
|
2197
|
-
|
|
2560
|
+
function crisp(v, width) {
|
|
2561
|
+
return Math.round(v) + (Math.round(width) % 2 === 1 ? 0.5 : 0);
|
|
2562
|
+
}
|
|
2563
|
+
function openHead(ctx, from, tip, lineWidth) {
|
|
2564
|
+
const length = Math.hypot(tip.x - from.x, tip.y - from.y);
|
|
2565
|
+
if (!(length > 0)) return;
|
|
2566
|
+
const ux = (tip.x - from.x) / length;
|
|
2567
|
+
const uy = (tip.y - from.y) / length;
|
|
2568
|
+
const size = 5 + 2.5 * lineWidth;
|
|
2569
|
+
ctx.moveTo(tip.x - size * ux - size * uy, tip.y - size * uy + size * ux);
|
|
2570
|
+
ctx.lineTo(tip.x, tip.y);
|
|
2571
|
+
ctx.lineTo(tip.x - size * ux + size * uy, tip.y - size * uy - size * ux);
|
|
2572
|
+
}
|
|
2573
|
+
function grouped(n) {
|
|
2574
|
+
const digits = String(Math.abs(n)).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
2575
|
+
return n < 0 ? `-${digits}` : digits;
|
|
2576
|
+
}
|
|
2577
|
+
function spanText(seconds) {
|
|
2578
|
+
const d = Math.floor(seconds / 86400);
|
|
2579
|
+
const h = Math.floor(seconds % 86400 / 3600);
|
|
2580
|
+
const m = Math.floor(seconds % 3600 / 60);
|
|
2581
|
+
return d > 0 ? `${d}d ${h}h` : h > 0 ? `${h}h ${m}m` : `${m}m`;
|
|
2582
|
+
}
|
|
2583
|
+
function wordsBox(place, block, lineHeight, across) {
|
|
2584
|
+
const lines = Math.max(1, block.lines.length);
|
|
2585
|
+
const width = block.width;
|
|
2586
|
+
const left = place.align === "left" ? 0 : place.align === "right" ? -width : -width / 2;
|
|
2587
|
+
const first = -lineHeight / 2 - 1;
|
|
2588
|
+
const top = across === "top" ? first - (lines - 1) * lineHeight : across === "bottom" ? first : first - (lines - 1) * lineHeight / 2;
|
|
2589
|
+
return { left, top, width, lines };
|
|
2590
|
+
}
|
|
2591
|
+
var LineWords = class extends Drawing {
|
|
2592
|
+
/** Whether the line invites words while it is selected without any. */
|
|
2593
|
+
invites() {
|
|
2594
|
+
return true;
|
|
2595
|
+
}
|
|
2596
|
+
wordsBlock() {
|
|
2597
|
+
return this.shownWords(lineMeasure(this.style));
|
|
2598
|
+
}
|
|
2599
|
+
/** Paint the words, or an open edit's draft with its placeholder at half strength while it is
|
|
2600
|
+
* empty. */
|
|
2601
|
+
paintWords(ctx, viewport) {
|
|
2602
|
+
const draft = this.textDraft;
|
|
2603
|
+
if (!draft && this.props.text === "") return;
|
|
2604
|
+
const place = this.wordsPlace(viewport);
|
|
2605
|
+
if (!place) return;
|
|
2606
|
+
const { block, placeholder } = this.wordsBlock();
|
|
2607
|
+
const shown = draft ? placeholder ?? block : block;
|
|
2608
|
+
const box2 = wordsBox(place, shown, this.style.fontSize, this.wordsAcross());
|
|
2609
|
+
ctx.save();
|
|
2610
|
+
ctx.translate(place.x, place.y);
|
|
2611
|
+
if (place.angle) ctx.rotate(place.angle);
|
|
2612
|
+
paintTextEntry(ctx, {
|
|
2613
|
+
x: box2.left,
|
|
2614
|
+
y: box2.top,
|
|
2615
|
+
width: box2.width,
|
|
2616
|
+
lineHeight: this.style.fontSize,
|
|
2617
|
+
font: fontOf(this.style),
|
|
2618
|
+
color: this.style.textColor,
|
|
2619
|
+
align: place.align,
|
|
2620
|
+
block,
|
|
2621
|
+
placeholder: draft && placeholder ? { block: placeholder, alpha: 0.5 } : null,
|
|
2622
|
+
draft,
|
|
2623
|
+
measure: lineMeasure(this.style)
|
|
2624
|
+
});
|
|
2625
|
+
ctx.restore();
|
|
2626
|
+
}
|
|
2627
|
+
textFrame(viewport) {
|
|
2628
|
+
const place = this.wordsPlace(viewport);
|
|
2629
|
+
if (!place) return null;
|
|
2630
|
+
const { block, placeholder } = this.wordsBlock();
|
|
2631
|
+
const shown = placeholder ?? block;
|
|
2632
|
+
const box2 = wordsBox(place, shown, this.style.fontSize, this.wordsAcross());
|
|
2633
|
+
const at = placeAt(place, box2.left, box2.top);
|
|
2634
|
+
return { x: at.x, y: at.y, width: box2.width, lines: box2.lines, lineHeight: this.style.fontSize, font: fontOf(this.style), align: place.align, wrapWidth: null, angle: place.angle };
|
|
2635
|
+
}
|
|
2636
|
+
/** The region the words cover, turned with them; null without words. */
|
|
2637
|
+
wordsRegion(viewport) {
|
|
2638
|
+
if (this.props.text === "" && !this.textDraft) return null;
|
|
2639
|
+
const place = this.wordsPlace(viewport);
|
|
2640
|
+
if (!place) return null;
|
|
2641
|
+
const { block, placeholder } = this.wordsBlock();
|
|
2642
|
+
const shown = this.textDraft && placeholder ? placeholder : block;
|
|
2643
|
+
const lineHeight = this.style.fontSize;
|
|
2644
|
+
const box2 = wordsBox(place, shown, lineHeight, this.wordsAcross());
|
|
2645
|
+
const middle = placeAt(place, box2.left + box2.width / 2, box2.top + box2.lines * lineHeight / 2);
|
|
2646
|
+
return { cx: middle.x, cy: middle.y, angle: place.angle, halfW: box2.width / 2 + 2, halfH: box2.lines * lineHeight / 2 + 2 };
|
|
2647
|
+
}
|
|
2648
|
+
wordsAt(point, viewport) {
|
|
2649
|
+
const region = this.wordsRegion(viewport);
|
|
2650
|
+
return !!region && inRegion(point, region);
|
|
2651
|
+
}
|
|
2652
|
+
/** The selected line without words invites them where they would stand. */
|
|
2653
|
+
paintTextHint(ctx, viewport) {
|
|
2654
|
+
this.noteTextHint(null);
|
|
2655
|
+
if (this.state !== "selected" || this.textEditing || this.props.text !== "" || !this.invites()) return;
|
|
2656
|
+
const place = this.wordsPlace(viewport);
|
|
2657
|
+
if (!place) return;
|
|
2658
|
+
this.noteTextHint(paintInvitation(ctx, this.textPlaceholder(), place, this.style, lineMeasure(this.style)));
|
|
2659
|
+
}
|
|
2660
|
+
/** Over its words or its invitation the selected line reads as words, and over a handle as the
|
|
2661
|
+
* handle it is. */
|
|
2662
|
+
cursorAt(point, viewport) {
|
|
2663
|
+
const handle = this.handleCursor(point, viewport);
|
|
2664
|
+
if (handle) return handle;
|
|
2665
|
+
if (this.state === "selected" && (this.wordsAt(point, viewport) || this.hitTextHint(point))) return "text";
|
|
2666
|
+
return null;
|
|
2667
|
+
}
|
|
2668
|
+
/** The cursor a handle under a point wears, or null where none is. */
|
|
2669
|
+
handleCursor(point, viewport) {
|
|
2670
|
+
const near = this.getControlPoints(viewport).some((p) => Math.hypot(p.x - point.x, p.y - point.y) <= 6.5);
|
|
2671
|
+
return near ? this.handleCursorName() : null;
|
|
2672
|
+
}
|
|
2673
|
+
handleCursorName() {
|
|
2674
|
+
return "default";
|
|
2675
|
+
}
|
|
2676
|
+
/** The line's handles show in their thin form while the pointer rests on it. */
|
|
2677
|
+
handlesOnHover() {
|
|
2678
|
+
return true;
|
|
2679
|
+
}
|
|
2680
|
+
/** A press on the words or the invitation of the selected line lands on the line. */
|
|
2681
|
+
hitWords(point, viewport) {
|
|
2682
|
+
if (this.wordsAt(point, viewport)) return true;
|
|
2683
|
+
return this.state === "selected" && this.hitTextHint(point);
|
|
2684
|
+
}
|
|
2685
|
+
};
|
|
2686
|
+
var pointLabel = (drawing2, coordinate, text, shown) => new AxisLabel({
|
|
2687
|
+
coordinate,
|
|
2688
|
+
text,
|
|
2689
|
+
color: () => drawing2.inks().accent,
|
|
2690
|
+
textColor: () => "#ffffff",
|
|
2691
|
+
visible: shown
|
|
2692
|
+
});
|
|
2693
|
+
var TrendLine = class extends LineWords {
|
|
2198
2694
|
type = "trend_line";
|
|
2695
|
+
_priceViews = [0, 1].map(
|
|
2696
|
+
(i) => pointLabel(
|
|
2697
|
+
this,
|
|
2698
|
+
() => {
|
|
2699
|
+
const viewport = this.getViewport();
|
|
2700
|
+
const anchor = this.anchors[i];
|
|
2701
|
+
return viewport && anchor ? viewport.yOf(anchor.price) : null;
|
|
2702
|
+
},
|
|
2703
|
+
() => this.formatPrice(this.anchors[i]?.price ?? 0),
|
|
2704
|
+
() => this.marksAxes() && !!this.anchors[i]
|
|
2705
|
+
)
|
|
2706
|
+
);
|
|
2707
|
+
_timeViews = [0, 1].map(
|
|
2708
|
+
(i) => pointLabel(
|
|
2709
|
+
this,
|
|
2710
|
+
() => {
|
|
2711
|
+
const viewport = this.getViewport();
|
|
2712
|
+
const anchor = this.anchors[i];
|
|
2713
|
+
return viewport && anchor ? viewport.xOf(anchor.time) : null;
|
|
2714
|
+
},
|
|
2715
|
+
() => this.formatTime(this.anchors[i]?.time),
|
|
2716
|
+
() => this.marksAxes() && !!this.anchors[i]
|
|
2717
|
+
)
|
|
2718
|
+
);
|
|
2199
2719
|
defaultProps() {
|
|
2200
2720
|
return { ...LINE_PROPS };
|
|
2201
2721
|
}
|
|
2722
|
+
applyProps(patch) {
|
|
2723
|
+
super.applyProps(endSavedLook(patch));
|
|
2724
|
+
}
|
|
2202
2725
|
requiredAnchors() {
|
|
2203
2726
|
return 2;
|
|
2204
2727
|
}
|
|
2205
|
-
/** The
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2728
|
+
/** The selected line marks its points' prices and times on the axes. */
|
|
2729
|
+
marksAxes() {
|
|
2730
|
+
return (this.state === "selected" || this.state === "editing") && this.isVisibleNow();
|
|
2731
|
+
}
|
|
2732
|
+
axisViews() {
|
|
2733
|
+
return this._priceViews;
|
|
2734
|
+
}
|
|
2735
|
+
timeViews() {
|
|
2736
|
+
return this._timeViews;
|
|
2737
|
+
}
|
|
2738
|
+
axisSpans() {
|
|
2739
|
+
return { prices: this.anchors.map((a) => a.price), times: this.anchors.map((a) => a.time) };
|
|
2740
|
+
}
|
|
2741
|
+
/** While its second point is being placed, the point following the pointer shows no handle. */
|
|
2742
|
+
getControlPoints(viewport) {
|
|
2743
|
+
const points = super.getControlPoints(viewport);
|
|
2744
|
+
return this.state === "editing" ? points.filter((p) => p.index < this.anchors.length - 1) : points;
|
|
2745
|
+
}
|
|
2746
|
+
/** The words ride the segment's slope at its middle or at the end they align to, above, on or
|
|
2747
|
+
* below it. */
|
|
2748
|
+
wordsPlace(viewport) {
|
|
2749
|
+
const [pa, pb] = this.anchorPixels(viewport);
|
|
2750
|
+
if (!pa || !pb) return null;
|
|
2751
|
+
const angle = segmentTextAngle(pa, pb);
|
|
2752
|
+
const [left, right] = pa.x <= pb.x ? [pa, pb] : [pb, pa];
|
|
2753
|
+
const along = this.props.textHAlign;
|
|
2754
|
+
const at = along === "left" ? left : along === "right" ? right : midpoint(pa, pb);
|
|
2755
|
+
const across = this.props.textVAlign === "top" ? -wordsOffset(this.style.fontSize) : this.props.textVAlign === "bottom" ? wordsOffset(this.style.fontSize) : 0;
|
|
2756
|
+
const p = placeAt({ x: at.x, y: at.y, angle, align: along }, 0, across);
|
|
2757
|
+
return { x: p.x, y: p.y, angle, align: along };
|
|
2758
|
+
}
|
|
2759
|
+
wordsAcross() {
|
|
2760
|
+
return this.props.textVAlign;
|
|
2214
2761
|
}
|
|
2215
|
-
/** The on-screen segment after extension
|
|
2762
|
+
/** The on-screen segment after extension, the shared basis for painting and hit-testing. */
|
|
2216
2763
|
segment(viewport) {
|
|
2217
2764
|
const [pa, pb] = this.anchorPixels(viewport);
|
|
2218
2765
|
if (!pa || !pb) return null;
|
|
@@ -2222,13 +2769,13 @@ var TrendLine = class extends Drawing {
|
|
|
2222
2769
|
}
|
|
2223
2770
|
/** A format-2 line showed its stats whether or not it was selected and counted no price moves; a
|
|
2224
2771
|
* stats position counted its left and right from the first point, and a save naming none stood
|
|
2225
|
-
* them at the middle. */
|
|
2772
|
+
* them at the middle. Its ends, words and stats paint as format 2 painted them. */
|
|
2226
2773
|
keepSavedLook(saved) {
|
|
2227
2774
|
const position = saved.statsPosition ?? "center";
|
|
2228
2775
|
const [a, b] = this._anchors;
|
|
2229
2776
|
const flipped = !!a && !!b && Number(b.time) < Number(a.time);
|
|
2230
2777
|
const statsPosition = flipped && position === "left" ? "right" : flipped && position === "right" ? "left" : position;
|
|
2231
|
-
this._props = { ...this._props, statsPosition, alwaysShowStats: true, showPipsChange: false };
|
|
2778
|
+
this._props = { ...this._props, statsPosition, alwaysShowStats: true, showPipsChange: false, savedLook: {} };
|
|
2232
2779
|
}
|
|
2233
2780
|
/** The stats stand while the line is selected or edited, or always where the viewer asked. */
|
|
2234
2781
|
statsShown() {
|
|
@@ -2237,15 +2784,24 @@ var TrendLine = class extends Drawing {
|
|
|
2237
2784
|
paint(ctx, viewport) {
|
|
2238
2785
|
const seg = this.segment(viewport);
|
|
2239
2786
|
if (!seg) return;
|
|
2787
|
+
if (this.props.savedLook) {
|
|
2788
|
+
this.paintSavedLook(ctx, viewport, seg);
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2240
2791
|
applyStroke(ctx, this.style);
|
|
2241
|
-
|
|
2242
|
-
if (this.props.
|
|
2243
|
-
if (this.props.
|
|
2244
|
-
|
|
2792
|
+
ctx.beginPath();
|
|
2793
|
+
if (this.props.rightEnd === "arrow") openHead(ctx, seg.a, seg.b, this.style.lineWidth);
|
|
2794
|
+
if (this.props.leftEnd === "arrow") openHead(ctx, seg.b, seg.a, this.style.lineWidth);
|
|
2795
|
+
ctx.moveTo(seg.a.x, seg.a.y);
|
|
2796
|
+
ctx.lineTo(seg.b.x, seg.b.y);
|
|
2797
|
+
ctx.stroke();
|
|
2798
|
+
this.paintMarks(ctx, viewport);
|
|
2799
|
+
this.paintWords(ctx, viewport);
|
|
2800
|
+
this.paintStats(ctx, viewport);
|
|
2245
2801
|
this.paintDecorations(ctx, viewport);
|
|
2246
2802
|
}
|
|
2247
|
-
/**
|
|
2248
|
-
|
|
2803
|
+
/** The midpoint marker and the end price pills, as their props ask. */
|
|
2804
|
+
paintMarks(ctx, viewport) {
|
|
2249
2805
|
const [pa, pb] = this.anchorPixels(viewport);
|
|
2250
2806
|
if (!pa || !pb) return;
|
|
2251
2807
|
if (this.props.middlePoint) {
|
|
@@ -2260,48 +2816,128 @@ var TrendLine = class extends Drawing {
|
|
|
2260
2816
|
}
|
|
2261
2817
|
if (this.props.showPriceLabels) {
|
|
2262
2818
|
const [a, b] = this.anchors;
|
|
2263
|
-
paintLabel(ctx, this.formatPrice(a.price), { x: pa.x - 8, y: pa.y }, this.style, {
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2819
|
+
paintLabel(ctx, this.formatPrice(a.price), { x: pa.x - 8, y: pa.y }, this.style, { align: "right", background: withAlpha(this.style.lineColor, 0.2) });
|
|
2820
|
+
paintLabel(ctx, this.formatPrice(b.price), { x: pb.x + 8, y: pb.y }, this.style, { background: withAlpha(this.style.lineColor, 0.2) });
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
/** The stats box: rows of the price move, the span and the angle, each led by its mark, standing
|
|
2824
|
+
* off the point along the line the stats position names, below and right of a line that rises
|
|
2825
|
+
* and above and right of one that falls. */
|
|
2826
|
+
paintStats(ctx, viewport) {
|
|
2827
|
+
if (!this.statsShown()) return;
|
|
2828
|
+
const rows = this.statsRows(viewport);
|
|
2829
|
+
if (rows.length === 0) return;
|
|
2830
|
+
const at = this.statsPoint(viewport);
|
|
2831
|
+
if (!at) return;
|
|
2832
|
+
const type = { ...this.style, fontSize: STATS_FONT_SIZE, bold: false, italic: false };
|
|
2833
|
+
const sized = statsBoxAt({ x: 0, y: 0 }, rows, lineMeasure(type));
|
|
2834
|
+
const box2 = { ...sized, x: Math.round(at.point.x + 12), y: Math.round(at.rising ? at.point.y + 11 : at.point.y - 11 - sized.height) };
|
|
2835
|
+
paintStatsBox(ctx, box2, rows, fontOf(type));
|
|
2836
|
+
}
|
|
2837
|
+
/** Where the stats stand along the line, and whether it rises left to right. */
|
|
2838
|
+
statsPoint(viewport) {
|
|
2839
|
+
const [pa, pb] = this.anchorPixels(viewport);
|
|
2840
|
+
if (!pa || !pb) return null;
|
|
2272
2841
|
const [left, right] = pa.x <= pb.x ? [pa, pb] : [pb, pa];
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2842
|
+
const toRight = (f) => ({ x: left.x + (right.x - left.x) * f, y: left.y + (right.y - left.y) * f });
|
|
2843
|
+
const place = this.props.statsPosition;
|
|
2844
|
+
const t = place === "left" ? 0.12 : place === "center" ? 0.5 : place === "auto" && toRight(0.88).x > viewport.width * 0.85 ? 0.12 : 0.88;
|
|
2845
|
+
return { point: toRight(t), rising: right.y <= left.y };
|
|
2846
|
+
}
|
|
2847
|
+
/** The stats rows: the price move with its percent and its count of the symbol's smallest moves;
|
|
2848
|
+
* the bars and the time it spans with its length on the pane; and its angle. A row whose items
|
|
2849
|
+
* are all off is left out. */
|
|
2850
|
+
statsRows(viewport) {
|
|
2851
|
+
const [a, b] = this.anchors;
|
|
2852
|
+
if (!a || !b) return [];
|
|
2853
|
+
const rows = [];
|
|
2854
|
+
const dPrice = b.price - a.price;
|
|
2855
|
+
const price = [];
|
|
2856
|
+
if (this.props.showPriceRange) price.push(this.formatPrice(dPrice));
|
|
2857
|
+
if (this.props.showPercentChange) {
|
|
2858
|
+
const pct = (a.price !== 0 ? dPrice / Math.abs(a.price) * 100 : 0).toFixed(2);
|
|
2859
|
+
price.push(price.length ? `(${pct}%)` : `${pct}%`);
|
|
2282
2860
|
}
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
const
|
|
2287
|
-
|
|
2288
|
-
const at = toRight(t);
|
|
2289
|
-
const crowded = !!this.props.text && this.props.textVAlign === "top" && (place === "center" && this.props.textHAlign === "center" || t === 0.12 && this.props.textHAlign === "left" || t === 0.88 && this.props.textHAlign === "right");
|
|
2290
|
-
ctx.save();
|
|
2291
|
-
ctx.translate(at.x, at.y);
|
|
2292
|
-
ctx.rotate(textAngle);
|
|
2293
|
-
paintLabel(ctx, stats, { x: 0, y: crowded ? -32 : -14 }, this.style, { align: "center", background: withAlpha("#1b1f27", 0.92) });
|
|
2294
|
-
ctx.restore();
|
|
2861
|
+
let priceRow = price.join(" ");
|
|
2862
|
+
const tick = this.tickSize();
|
|
2863
|
+
if (this.props.showPipsChange && tick !== null && tick > 0) {
|
|
2864
|
+
const moves = grouped(Math.round(dPrice / tick));
|
|
2865
|
+
priceRow = priceRow ? `${priceRow}, ${moves}` : moves;
|
|
2295
2866
|
}
|
|
2867
|
+
if (priceRow) rows.push({ kind: "price", text: priceRow });
|
|
2868
|
+
const span = [];
|
|
2869
|
+
if (this.props.showBarsRange) {
|
|
2870
|
+
const bars = viewport.barsBetween(a.time, b.time);
|
|
2871
|
+
if (bars !== null) span.push(`${Math.round(bars)} bars`);
|
|
2872
|
+
}
|
|
2873
|
+
if (this.props.showDateTimeRange) {
|
|
2874
|
+
const secs = Math.abs(Number(b.time) - Number(a.time));
|
|
2875
|
+
if (Number.isFinite(secs) && secs > 0) span.push(span.length ? `(${spanText(secs)})` : spanText(secs));
|
|
2876
|
+
}
|
|
2877
|
+
let spanRow = span.join(" ");
|
|
2878
|
+
const [pa, pb] = this.anchorPixels(viewport);
|
|
2879
|
+
if (this.props.showDistance && pa && pb) {
|
|
2880
|
+
const distance = `distance: ${Math.round(Math.hypot(pb.x - pa.x, pb.y - pa.y))} px`;
|
|
2881
|
+
spanRow = spanRow ? `${spanRow}, ${distance}` : distance;
|
|
2882
|
+
}
|
|
2883
|
+
if (spanRow) rows.push({ kind: "span", text: spanRow });
|
|
2884
|
+
if (this.props.showAngle && pa && pb) rows.push({ kind: "angle", text: `${(-angleOf(pa, pb) * 180 / Math.PI).toFixed(1)}\xB0` });
|
|
2885
|
+
return rows;
|
|
2886
|
+
}
|
|
2887
|
+
/** Extra ink beyond the shared prop set: the family variants override. */
|
|
2888
|
+
paintDecorations(_ctx, _viewport) {
|
|
2296
2889
|
}
|
|
2297
|
-
|
|
2890
|
+
testHit(point, viewport) {
|
|
2891
|
+
const seg = this.segment(viewport);
|
|
2892
|
+
if (!seg) return false;
|
|
2893
|
+
return distanceToSegment(point, seg.a, seg.b) <= hitTolerance(this.style.lineWidth) || this.hitWords(point, viewport);
|
|
2894
|
+
}
|
|
2895
|
+
// ============ The format-2 look ============
|
|
2896
|
+
/** A format-2 line: filled arrow heads, its words on a label across it, and its stats on a pill. */
|
|
2897
|
+
paintSavedLook(ctx, viewport, seg) {
|
|
2898
|
+
applyStroke(ctx, this.style);
|
|
2899
|
+
strokeSegment(ctx, seg.a, seg.b);
|
|
2900
|
+
if (this.props.leftEnd === "arrow") paintArrowHead(ctx, seg.b, seg.a, this.style);
|
|
2901
|
+
if (this.props.rightEnd === "arrow") paintArrowHead(ctx, seg.a, seg.b, this.style);
|
|
2902
|
+
this.paintMarks(ctx, viewport);
|
|
2903
|
+
const [pa, pb] = this.anchorPixels(viewport);
|
|
2904
|
+
if (pa && pb) {
|
|
2905
|
+
const textAngle = segmentTextAngle(pa, pb);
|
|
2906
|
+
const [left, right] = pa.x <= pb.x ? [pa, pb] : [pb, pa];
|
|
2907
|
+
if (this.textDraft) this.paintWords(ctx, viewport);
|
|
2908
|
+
else if (this.props.text) {
|
|
2909
|
+
const along = this.props.textHAlign;
|
|
2910
|
+
const at = along === "left" ? left : along === "right" ? right : midpoint(pa, pb);
|
|
2911
|
+
const across = ACROSS[this.props.textVAlign] ?? ACROSS.top;
|
|
2912
|
+
ctx.save();
|
|
2913
|
+
ctx.translate(at.x, at.y);
|
|
2914
|
+
ctx.rotate(textAngle);
|
|
2915
|
+
paintLabel(ctx, this.props.text, { x: 0, y: across.y }, this.style, { align: along === "left" ? "left" : along === "right" ? "right" : "center", baseline: across.baseline });
|
|
2916
|
+
ctx.restore();
|
|
2917
|
+
}
|
|
2918
|
+
const stats = this.statsShown() ? this.savedStatsText(viewport) : null;
|
|
2919
|
+
if (stats) {
|
|
2920
|
+
const place = this.props.statsPosition;
|
|
2921
|
+
const toRight = (f) => ({ x: left.x + (right.x - left.x) * f, y: left.y + (right.y - left.y) * f });
|
|
2922
|
+
const t = place === "left" ? 0.12 : place === "center" ? 0.5 : place === "auto" && toRight(0.88).x > viewport.width * 0.85 ? 0.12 : 0.88;
|
|
2923
|
+
const at = toRight(t);
|
|
2924
|
+
const crowded = !!this.props.text && this.props.textVAlign === "top" && (place === "center" && this.props.textHAlign === "center" || t === 0.12 && this.props.textHAlign === "left" || t === 0.88 && this.props.textHAlign === "right");
|
|
2925
|
+
ctx.save();
|
|
2926
|
+
ctx.translate(at.x, at.y);
|
|
2927
|
+
ctx.rotate(textAngle);
|
|
2928
|
+
paintLabel(ctx, stats, { x: 0, y: crowded ? -32 : -14 }, this.style, { align: "center", background: withAlpha("#1b1f27", 0.92) });
|
|
2929
|
+
ctx.restore();
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
this.paintDecorations(ctx, viewport);
|
|
2933
|
+
}
|
|
2934
|
+
/** A format-2 line's stats on one line: signed moves, the bars, the span and the angle. */
|
|
2935
|
+
savedStatsText(viewport) {
|
|
2298
2936
|
const [a, b] = this.anchors;
|
|
2299
2937
|
if (!a || !b) return null;
|
|
2300
2938
|
const parts = [];
|
|
2301
2939
|
const dPrice = b.price - a.price;
|
|
2302
|
-
if (this.props.showPriceRange) {
|
|
2303
|
-
parts.push(`${dPrice >= 0 ? "+" : ""}${this.formatPrice(dPrice)}`);
|
|
2304
|
-
}
|
|
2940
|
+
if (this.props.showPriceRange) parts.push(`${dPrice >= 0 ? "+" : ""}${this.formatPrice(dPrice)}`);
|
|
2305
2941
|
if (this.props.showPercentChange) {
|
|
2306
2942
|
const pct = a.price !== 0 ? dPrice / Math.abs(a.price) * 100 : 0;
|
|
2307
2943
|
parts.push(`${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`);
|
|
@@ -2319,12 +2955,7 @@ var TrendLine = class extends Drawing {
|
|
|
2319
2955
|
}
|
|
2320
2956
|
if (this.props.showDateTimeRange) {
|
|
2321
2957
|
const secs = Math.abs(Number(b.time) - Number(a.time));
|
|
2322
|
-
if (Number.isFinite(secs) && secs > 0)
|
|
2323
|
-
const d = Math.floor(secs / 86400);
|
|
2324
|
-
const h = Math.floor(secs % 86400 / 3600);
|
|
2325
|
-
const m = Math.floor(secs % 3600 / 60);
|
|
2326
|
-
parts.push(d > 0 ? `${d}d ${h}h` : h > 0 ? `${h}h ${m}m` : `${m}m`);
|
|
2327
|
-
}
|
|
2958
|
+
if (Number.isFinite(secs) && secs > 0) parts.push(spanText(secs));
|
|
2328
2959
|
}
|
|
2329
2960
|
if (this.props.showAngle) {
|
|
2330
2961
|
const [pa, pb] = this.anchorPixels(viewport);
|
|
@@ -2332,14 +2963,6 @@ var TrendLine = class extends Drawing {
|
|
|
2332
2963
|
}
|
|
2333
2964
|
return parts.length ? parts.join(" \xB7 ") : null;
|
|
2334
2965
|
}
|
|
2335
|
-
/** Extra ink beyond the shared prop set — the family variants override. */
|
|
2336
|
-
paintDecorations(_ctx, _viewport) {
|
|
2337
|
-
}
|
|
2338
|
-
testHit(point, viewport) {
|
|
2339
|
-
const seg = this.segment(viewport);
|
|
2340
|
-
if (!seg) return false;
|
|
2341
|
-
return distanceToSegment(point, seg.a, seg.b) <= hitTolerance(this.style.lineWidth);
|
|
2342
|
-
}
|
|
2343
2966
|
};
|
|
2344
2967
|
var Ray = class extends TrendLine {
|
|
2345
2968
|
type = "ray";
|
|
@@ -2358,6 +2981,9 @@ var Arrow = class extends TrendLine {
|
|
|
2358
2981
|
defaultProps() {
|
|
2359
2982
|
return { ...LINE_PROPS, rightEnd: "arrow" };
|
|
2360
2983
|
}
|
|
2984
|
+
cursorAt(point, viewport) {
|
|
2985
|
+
return super.cursorAt(point, viewport) ?? (this.state === "selected" ? "text" : null);
|
|
2986
|
+
}
|
|
2361
2987
|
};
|
|
2362
2988
|
var InfoLine = class extends TrendLine {
|
|
2363
2989
|
type = "info_line";
|
|
@@ -2369,15 +2995,16 @@ var InfoLine = class extends TrendLine {
|
|
|
2369
2995
|
showPipsChange: true,
|
|
2370
2996
|
showBarsRange: true,
|
|
2371
2997
|
showDateTimeRange: true,
|
|
2998
|
+
showDistance: true,
|
|
2372
2999
|
showAngle: true,
|
|
2373
3000
|
statsPosition: "center",
|
|
2374
3001
|
alwaysShowStats: true
|
|
2375
3002
|
};
|
|
2376
3003
|
}
|
|
2377
|
-
/** A format-2 info line that named no percent change or span showed neither. */
|
|
3004
|
+
/** A format-2 info line that named no percent change or span showed neither, and no distance. */
|
|
2378
3005
|
keepSavedLook(saved) {
|
|
2379
3006
|
super.keepSavedLook(saved);
|
|
2380
|
-
const props = {};
|
|
3007
|
+
const props = { showDistance: false };
|
|
2381
3008
|
if (!("showPercentChange" in saved)) props.showPercentChange = false;
|
|
2382
3009
|
if (!("showDateTimeRange" in saved)) props.showDateTimeRange = false;
|
|
2383
3010
|
this._props = { ...this._props, ...props };
|
|
@@ -2385,12 +3012,38 @@ var InfoLine = class extends TrendLine {
|
|
|
2385
3012
|
};
|
|
2386
3013
|
var TrendAngle = class extends TrendLine {
|
|
2387
3014
|
type = "trend_angle";
|
|
2388
|
-
|
|
2389
|
-
|
|
3015
|
+
invites() {
|
|
3016
|
+
return false;
|
|
2390
3017
|
}
|
|
2391
3018
|
paintDecorations(ctx, viewport) {
|
|
2392
3019
|
const [pa, pb] = this.anchorPixels(viewport);
|
|
2393
3020
|
if (!pa || !pb) return;
|
|
3021
|
+
if (this.props.savedLook) {
|
|
3022
|
+
this.paintSavedAngle(ctx, pa, pb);
|
|
3023
|
+
return;
|
|
3024
|
+
}
|
|
3025
|
+
const angle = angleOf(pa, pb);
|
|
3026
|
+
ctx.save();
|
|
3027
|
+
ctx.strokeStyle = this.style.lineColor;
|
|
3028
|
+
ctx.lineWidth = 1;
|
|
3029
|
+
ctx.lineCap = "butt";
|
|
3030
|
+
ctx.lineJoin = "miter";
|
|
3031
|
+
ctx.setLineDash([1, 2]);
|
|
3032
|
+
ctx.beginPath();
|
|
3033
|
+
ctx.moveTo(pa.x, pa.y);
|
|
3034
|
+
ctx.lineTo(pa.x + 50, pa.y);
|
|
3035
|
+
ctx.arc(pa.x, pa.y, 50, 0, angle, angle < 0);
|
|
3036
|
+
ctx.stroke();
|
|
3037
|
+
ctx.setLineDash([]);
|
|
3038
|
+
ctx.font = fontOf(this.style);
|
|
3039
|
+
ctx.fillStyle = this.style.lineColor;
|
|
3040
|
+
ctx.textAlign = "start";
|
|
3041
|
+
ctx.textBaseline = "middle";
|
|
3042
|
+
ctx.fillText(`${(-angle * 180 / Math.PI).toFixed(1)}\xB0`, pa.x + 58.5, pa.y + 1);
|
|
3043
|
+
ctx.restore();
|
|
3044
|
+
}
|
|
3045
|
+
/** A format-2 trend angle's mark: a short dotted level and arc, the angle on a pill. */
|
|
3046
|
+
paintSavedAngle(ctx, pa, pb) {
|
|
2394
3047
|
const angle = angleOf(pa, pb);
|
|
2395
3048
|
const angleDeg = -angle * (180 / Math.PI);
|
|
2396
3049
|
const radius = Math.min(36, Math.hypot(pb.x - pa.x, pb.y - pa.y) / 2);
|
|
@@ -2408,25 +3061,33 @@ var TrendAngle = class extends TrendLine {
|
|
|
2408
3061
|
ctx.stroke();
|
|
2409
3062
|
ctx.restore();
|
|
2410
3063
|
}
|
|
2411
|
-
paintLabel(ctx, `${angleDeg.toFixed(0)}\xB0`, { x: pa.x + radius + 18, y: pa.y }, this.style, {
|
|
2412
|
-
background: withAlpha("#1b1f27", 0.92)
|
|
2413
|
-
});
|
|
3064
|
+
paintLabel(ctx, `${angleDeg.toFixed(0)}\xB0`, { x: pa.x + radius + 18, y: pa.y }, this.style, { background: withAlpha("#1b1f27", 0.92) });
|
|
2414
3065
|
}
|
|
2415
3066
|
};
|
|
2416
|
-
var
|
|
3067
|
+
var levelLabel = (drawing2, coordinate, text, own) => {
|
|
3068
|
+
const selected = () => drawing2.state === "selected" || drawing2.state === "editing";
|
|
3069
|
+
const color = () => own() ? drawing2.style.lineColor : drawing2.inks().accent;
|
|
3070
|
+
return new AxisLabel({
|
|
3071
|
+
coordinate,
|
|
3072
|
+
text,
|
|
3073
|
+
color,
|
|
3074
|
+
textColor: () => inkOn(color()),
|
|
3075
|
+
visible: () => (own() || selected()) && drawing2.isVisibleNow()
|
|
3076
|
+
});
|
|
3077
|
+
};
|
|
3078
|
+
var HorizontalLine = class extends LineWords {
|
|
2417
3079
|
type = "horizontal_line";
|
|
2418
3080
|
_axisViews = [
|
|
2419
|
-
|
|
2420
|
-
|
|
3081
|
+
levelLabel(
|
|
3082
|
+
this,
|
|
3083
|
+
() => {
|
|
2421
3084
|
const anchor = this.anchors[0];
|
|
2422
3085
|
const viewport = this.getViewport();
|
|
2423
|
-
|
|
2424
|
-
return viewport.yOf(anchor.price);
|
|
3086
|
+
return anchor && viewport ? viewport.yOf(anchor.price) : null;
|
|
2425
3087
|
},
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
})
|
|
3088
|
+
() => this.formatPrice(this.anchors[0]?.price ?? 0),
|
|
3089
|
+
() => this.props.showPrice
|
|
3090
|
+
)
|
|
2430
3091
|
];
|
|
2431
3092
|
defaultProps() {
|
|
2432
3093
|
return { text: "", showPrice: true, textVAlign: "middle", textHAlign: "center" };
|
|
@@ -2434,21 +3095,56 @@ var HorizontalLine = class extends Drawing {
|
|
|
2434
3095
|
axisViews() {
|
|
2435
3096
|
return this._axisViews;
|
|
2436
3097
|
}
|
|
3098
|
+
axisSpans() {
|
|
3099
|
+
return { prices: this.anchors.slice(0, 1).map((a) => a.price), times: null };
|
|
3100
|
+
}
|
|
2437
3101
|
requiredAnchors() {
|
|
2438
3102
|
return 1;
|
|
2439
3103
|
}
|
|
2440
|
-
|
|
3104
|
+
handleShape() {
|
|
3105
|
+
return "square";
|
|
3106
|
+
}
|
|
3107
|
+
handleCursorName() {
|
|
3108
|
+
return "ns-resize";
|
|
3109
|
+
}
|
|
3110
|
+
/** The square handle stands nine tenths of the way across the pane at the level. */
|
|
3111
|
+
getControlPoints(viewport) {
|
|
3112
|
+
const y = this.levelY(viewport);
|
|
3113
|
+
return y === null ? [] : [{ index: 0, x: viewport.width * LEVEL_HANDLE_AT, y }];
|
|
3114
|
+
}
|
|
3115
|
+
/** The level's height on the pane, or null off the scale. */
|
|
3116
|
+
levelY(viewport) {
|
|
2441
3117
|
const y = viewport.yOf(this.anchors[0]?.price ?? NaN);
|
|
2442
|
-
|
|
2443
|
-
|
|
3118
|
+
return y === null || !Number.isFinite(y) ? null : y;
|
|
3119
|
+
}
|
|
3120
|
+
paint(ctx, viewport) {
|
|
3121
|
+
const level = this.levelY(viewport);
|
|
3122
|
+
if (level === null) return;
|
|
3123
|
+
const y = crisp(level, this.style.lineWidth);
|
|
2444
3124
|
applyStroke(ctx, this.style);
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
3125
|
+
ctx.lineCap = this.levelCap();
|
|
3126
|
+
ctx.lineJoin = "miter";
|
|
3127
|
+
strokeSegment(ctx, { x: this.leftEdge(viewport), y }, { x: viewport.width, y });
|
|
3128
|
+
this.paintWords(ctx, viewport);
|
|
3129
|
+
}
|
|
3130
|
+
/** The cap the line's ends wear: square across the pane's edges, round at a ray's point. */
|
|
3131
|
+
levelCap() {
|
|
3132
|
+
return "butt";
|
|
3133
|
+
}
|
|
3134
|
+
/** The words stand above, on or below the drawn line, in from its left end, at its middle, or in
|
|
3135
|
+
* from the pane's right edge. */
|
|
3136
|
+
wordsPlace(viewport) {
|
|
3137
|
+
const level = this.levelY(viewport);
|
|
3138
|
+
if (level === null) return null;
|
|
3139
|
+
const y = crisp(level, this.style.lineWidth);
|
|
3140
|
+
const start = this.leftEdge(viewport);
|
|
3141
|
+
const along = this.props.textHAlign;
|
|
3142
|
+
const x = along === "left" ? start + LEVEL_WORDS_INSET : along === "right" ? viewport.width - LEVEL_WORDS_INSET : (start + viewport.width) / 2;
|
|
3143
|
+
const across = this.props.textVAlign === "top" ? -wordsOffset(this.style.fontSize) : this.props.textVAlign === "bottom" ? wordsOffset(this.style.fontSize) : 0;
|
|
3144
|
+
return { x, y: y + across, angle: 0, align: along };
|
|
3145
|
+
}
|
|
3146
|
+
wordsAcross() {
|
|
3147
|
+
return this.props.textVAlign;
|
|
2452
3148
|
}
|
|
2453
3149
|
/** Where the line starts; the ray variant starts at its anchor. */
|
|
2454
3150
|
leftEdge(_viewport) {
|
|
@@ -2459,16 +3155,53 @@ var HorizontalLine = class extends Drawing {
|
|
|
2459
3155
|
this._props = { ...this._props, textVAlign: "top", textHAlign: "left" };
|
|
2460
3156
|
}
|
|
2461
3157
|
testHit(point, viewport) {
|
|
2462
|
-
const y =
|
|
3158
|
+
const y = this.levelY(viewport);
|
|
2463
3159
|
if (y === null) return false;
|
|
2464
|
-
|
|
3160
|
+
const onLine = point.x >= this.leftEdge(viewport) - 4 && Math.abs(point.y - y) <= hitTolerance(this.style.lineWidth);
|
|
3161
|
+
return onLine || this.hitWords(point, viewport);
|
|
2465
3162
|
}
|
|
2466
3163
|
};
|
|
2467
3164
|
var HorizontalRay = class extends HorizontalLine {
|
|
2468
3165
|
type = "horizontal_ray";
|
|
3166
|
+
_rayTimeViews = [
|
|
3167
|
+
pointLabel(
|
|
3168
|
+
this,
|
|
3169
|
+
() => {
|
|
3170
|
+
const anchor = this.anchors[0];
|
|
3171
|
+
const viewport = this.getViewport();
|
|
3172
|
+
return anchor && viewport ? viewport.xOf(anchor.time) : null;
|
|
3173
|
+
},
|
|
3174
|
+
() => this.formatTime(this.anchors[0]?.time),
|
|
3175
|
+
() => (this.state === "selected" || this.state === "editing") && this.isVisibleNow()
|
|
3176
|
+
)
|
|
3177
|
+
];
|
|
2469
3178
|
defaultProps() {
|
|
2470
3179
|
return { text: "", showPrice: true, textVAlign: "bottom", textHAlign: "center" };
|
|
2471
3180
|
}
|
|
3181
|
+
timeViews() {
|
|
3182
|
+
return this._rayTimeViews;
|
|
3183
|
+
}
|
|
3184
|
+
axisSpans() {
|
|
3185
|
+
const anchor = this.anchors[0];
|
|
3186
|
+
return { prices: anchor ? [anchor.price] : null, times: anchor ? [anchor.time] : null };
|
|
3187
|
+
}
|
|
3188
|
+
handleShape() {
|
|
3189
|
+
return "circle";
|
|
3190
|
+
}
|
|
3191
|
+
handleCursorName() {
|
|
3192
|
+
return "default";
|
|
3193
|
+
}
|
|
3194
|
+
getControlPoints(viewport) {
|
|
3195
|
+
const anchor = this.anchors[0];
|
|
3196
|
+
const p = anchor ? this.anchorToPixel(anchor, viewport) : null;
|
|
3197
|
+
return p ? [{ index: 0, x: p.x, y: p.y }] : [];
|
|
3198
|
+
}
|
|
3199
|
+
invites() {
|
|
3200
|
+
return false;
|
|
3201
|
+
}
|
|
3202
|
+
levelCap() {
|
|
3203
|
+
return "round";
|
|
3204
|
+
}
|
|
2472
3205
|
leftEdge(viewport) {
|
|
2473
3206
|
const anchor = this.anchors[0];
|
|
2474
3207
|
if (!anchor) return 0;
|
|
@@ -2476,27 +3209,19 @@ var HorizontalRay = class extends HorizontalLine {
|
|
|
2476
3209
|
return p ? p.x : 0;
|
|
2477
3210
|
}
|
|
2478
3211
|
};
|
|
2479
|
-
|
|
2480
|
-
const t = Number(time);
|
|
2481
|
-
if (!Number.isFinite(t)) return String(time);
|
|
2482
|
-
const d = new Date(t * 1e3);
|
|
2483
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
2484
|
-
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`;
|
|
2485
|
-
}
|
|
2486
|
-
var VerticalLine = class extends Drawing {
|
|
3212
|
+
var VerticalLine = class extends LineWords {
|
|
2487
3213
|
type = "vertical_line";
|
|
2488
3214
|
_timeViews = [
|
|
2489
|
-
|
|
2490
|
-
|
|
3215
|
+
levelLabel(
|
|
3216
|
+
this,
|
|
3217
|
+
() => {
|
|
2491
3218
|
const anchor = this.anchors[0];
|
|
2492
3219
|
const viewport = this.getViewport();
|
|
2493
|
-
|
|
2494
|
-
return viewport.xOf(anchor.time);
|
|
3220
|
+
return anchor && viewport ? viewport.xOf(anchor.time) : null;
|
|
2495
3221
|
},
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
})
|
|
3222
|
+
() => this.formatTime(this.anchors[0]?.time),
|
|
3223
|
+
() => this.props.showTime
|
|
3224
|
+
)
|
|
2500
3225
|
];
|
|
2501
3226
|
defaultProps() {
|
|
2502
3227
|
return { text: "", showTime: true, textVAlign: "middle", textHAlign: "center", textOrientation: "vertical" };
|
|
@@ -2504,76 +3229,94 @@ var VerticalLine = class extends Drawing {
|
|
|
2504
3229
|
timeViews() {
|
|
2505
3230
|
return this._timeViews;
|
|
2506
3231
|
}
|
|
3232
|
+
axisSpans() {
|
|
3233
|
+
return { prices: null, times: this.anchors.slice(0, 1).map((a) => a.time) };
|
|
3234
|
+
}
|
|
2507
3235
|
requiredAnchors() {
|
|
2508
3236
|
return 1;
|
|
2509
3237
|
}
|
|
3238
|
+
handleShape() {
|
|
3239
|
+
return "square";
|
|
3240
|
+
}
|
|
3241
|
+
handleCursorName() {
|
|
3242
|
+
return "ew-resize";
|
|
3243
|
+
}
|
|
3244
|
+
/** The square handle stands on the line nine tenths of the way down the pane. */
|
|
3245
|
+
getControlPoints(viewport) {
|
|
3246
|
+
const x = this.timeX(viewport);
|
|
3247
|
+
return x === null ? [] : [{ index: 0, x, y: viewport.height * LEVEL_HANDLE_AT }];
|
|
3248
|
+
}
|
|
3249
|
+
timeX(viewport) {
|
|
3250
|
+
const anchor = this.anchors[0];
|
|
3251
|
+
return anchor ? viewport.xOf(anchor.time) : null;
|
|
3252
|
+
}
|
|
2510
3253
|
/** A format-2 line's label read across it, to its right at the top of the pane. */
|
|
2511
3254
|
keepSavedLook(_saved) {
|
|
2512
3255
|
this._props = { ...this._props, textOrientation: "horizontal", textVAlign: "top", textHAlign: "right" };
|
|
2513
3256
|
}
|
|
2514
3257
|
paint(ctx, viewport) {
|
|
2515
|
-
const
|
|
2516
|
-
if (
|
|
2517
|
-
const x =
|
|
2518
|
-
if (x === null) return;
|
|
3258
|
+
const time = this.timeX(viewport);
|
|
3259
|
+
if (time === null) return;
|
|
3260
|
+
const x = crisp(time, this.style.lineWidth);
|
|
2519
3261
|
applyStroke(ctx, this.style);
|
|
3262
|
+
ctx.lineCap = "butt";
|
|
3263
|
+
ctx.lineJoin = "miter";
|
|
2520
3264
|
strokeSegment(ctx, { x, y: 0 }, { x, y: viewport.height });
|
|
2521
|
-
|
|
2522
|
-
}
|
|
2523
|
-
/** The
|
|
2524
|
-
* a quarter left, so the line's top is where
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
const
|
|
2528
|
-
if (
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
}
|
|
2540
|
-
|
|
3265
|
+
this.paintWords(ctx, viewport);
|
|
3266
|
+
}
|
|
3267
|
+
/** The words stand at the top, the middle or the bottom of the pane, to the line's left, on it or
|
|
3268
|
+
* to its right. Running up the line they turn a quarter left, so the line's top is where they
|
|
3269
|
+
* end; reading across it they stand level. */
|
|
3270
|
+
wordsPlace(viewport) {
|
|
3271
|
+
const time = this.timeX(viewport);
|
|
3272
|
+
if (time === null) return null;
|
|
3273
|
+
const x = crisp(time, this.style.lineWidth);
|
|
3274
|
+
const { textVAlign: along, textHAlign: across, textOrientation } = this.props;
|
|
3275
|
+
const off = wordsOffset(this.style.fontSize);
|
|
3276
|
+
if (textOrientation === "horizontal") {
|
|
3277
|
+
const y2 = along === "top" ? off : along === "bottom" ? viewport.height - off : viewport.height / 2;
|
|
3278
|
+
const at2 = across === "left" ? x - 4 : across === "right" ? x + 4 : x;
|
|
3279
|
+
return { x: at2, y: y2, angle: 0, align: across === "left" ? "right" : across === "right" ? "left" : "center" };
|
|
3280
|
+
}
|
|
3281
|
+
const y = along === "top" ? LEVEL_WORDS_INSET : along === "bottom" ? viewport.height - LEVEL_WORDS_INSET : viewport.height / 2;
|
|
3282
|
+
const at = across === "left" ? x - off : across === "right" ? x + off : x;
|
|
3283
|
+
return { x: at, y, angle: -Math.PI / 2, align: along === "top" ? "right" : along === "bottom" ? "left" : "center" };
|
|
3284
|
+
}
|
|
3285
|
+
wordsAcross() {
|
|
3286
|
+
if (this.props.textOrientation === "horizontal") return this.props.textVAlign === "top" ? "bottom" : this.props.textVAlign === "bottom" ? "top" : "middle";
|
|
3287
|
+
return this.props.textHAlign === "left" ? "top" : this.props.textHAlign === "right" ? "bottom" : "middle";
|
|
2541
3288
|
}
|
|
2542
3289
|
testHit(point, viewport) {
|
|
2543
|
-
const
|
|
2544
|
-
if (
|
|
2545
|
-
|
|
2546
|
-
if (x === null) return false;
|
|
2547
|
-
return Math.abs(point.x - x) <= hitTolerance(this.style.lineWidth);
|
|
3290
|
+
const time = this.timeX(viewport);
|
|
3291
|
+
if (time === null) return false;
|
|
3292
|
+
return Math.abs(point.x - time) <= hitTolerance(this.style.lineWidth) || this.hitWords(point, viewport);
|
|
2548
3293
|
}
|
|
2549
3294
|
};
|
|
2550
3295
|
var CrossLine = class extends Drawing {
|
|
2551
3296
|
type = "cross_line";
|
|
2552
3297
|
_axisViews = [
|
|
2553
|
-
|
|
2554
|
-
|
|
3298
|
+
levelLabel(
|
|
3299
|
+
this,
|
|
3300
|
+
() => {
|
|
2555
3301
|
const anchor = this.anchors[0];
|
|
2556
3302
|
const viewport = this.getViewport();
|
|
2557
|
-
|
|
2558
|
-
return viewport.yOf(anchor.price);
|
|
3303
|
+
return anchor && viewport ? viewport.yOf(anchor.price) : null;
|
|
2559
3304
|
},
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
})
|
|
3305
|
+
() => this.formatPrice(this.anchors[0]?.price ?? 0),
|
|
3306
|
+
() => this.props.showPrice
|
|
3307
|
+
)
|
|
2564
3308
|
];
|
|
2565
3309
|
_timeViews = [
|
|
2566
|
-
|
|
2567
|
-
|
|
3310
|
+
levelLabel(
|
|
3311
|
+
this,
|
|
3312
|
+
() => {
|
|
2568
3313
|
const anchor = this.anchors[0];
|
|
2569
3314
|
const viewport = this.getViewport();
|
|
2570
|
-
|
|
2571
|
-
return viewport.xOf(anchor.time);
|
|
3315
|
+
return anchor && viewport ? viewport.xOf(anchor.time) : null;
|
|
2572
3316
|
},
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
})
|
|
3317
|
+
() => this.formatTime(this.anchors[0]?.time),
|
|
3318
|
+
() => this.props.showTime
|
|
3319
|
+
)
|
|
2577
3320
|
];
|
|
2578
3321
|
defaultProps() {
|
|
2579
3322
|
return { showPrice: true, showTime: true };
|
|
@@ -2584,17 +3327,30 @@ var CrossLine = class extends Drawing {
|
|
|
2584
3327
|
timeViews() {
|
|
2585
3328
|
return this._timeViews;
|
|
2586
3329
|
}
|
|
3330
|
+
axisSpans() {
|
|
3331
|
+
const anchor = this.anchors[0];
|
|
3332
|
+
return { prices: anchor ? [anchor.price] : null, times: anchor ? [anchor.time] : null };
|
|
3333
|
+
}
|
|
2587
3334
|
requiredAnchors() {
|
|
2588
3335
|
return 1;
|
|
2589
3336
|
}
|
|
3337
|
+
handlesOnHover() {
|
|
3338
|
+
return true;
|
|
3339
|
+
}
|
|
3340
|
+
cursorAt(point, viewport) {
|
|
3341
|
+
return this.getControlPoints(viewport).some((p) => Math.hypot(p.x - point.x, p.y - point.y) <= 6.5) ? "default" : null;
|
|
3342
|
+
}
|
|
2590
3343
|
paint(ctx, viewport) {
|
|
2591
3344
|
const anchor = this.anchors[0];
|
|
2592
3345
|
if (!anchor) return;
|
|
2593
3346
|
const p = this.anchorToPixel(anchor, viewport);
|
|
2594
3347
|
if (!p) return;
|
|
3348
|
+
const width = this.style.lineWidth;
|
|
2595
3349
|
applyStroke(ctx, this.style);
|
|
2596
|
-
|
|
2597
|
-
|
|
3350
|
+
ctx.lineCap = "butt";
|
|
3351
|
+
ctx.lineJoin = "miter";
|
|
3352
|
+
strokeSegment(ctx, { x: 0, y: crisp(p.y, width) }, { x: viewport.width, y: crisp(p.y, width) });
|
|
3353
|
+
strokeSegment(ctx, { x: crisp(p.x, width), y: 0 }, { x: crisp(p.x, width), y: viewport.height });
|
|
2598
3354
|
}
|
|
2599
3355
|
testHit(point, viewport) {
|
|
2600
3356
|
const anchor = this.anchors[0];
|
|
@@ -3153,77 +3909,6 @@ function volumeProfile(bars, rows) {
|
|
|
3153
3909
|
return bins;
|
|
3154
3910
|
}
|
|
3155
3911
|
|
|
3156
|
-
// src/internal/drawings/core/savedLook.ts
|
|
3157
|
-
function endSavedLook(patch) {
|
|
3158
|
-
return "savedLook" in patch || Object.keys(patch).length === 0 ? patch : { ...patch, savedLook: null };
|
|
3159
|
-
}
|
|
3160
|
-
function savedLevels(look, key = "levels") {
|
|
3161
|
-
const levels = look[key];
|
|
3162
|
-
if (!Array.isArray(levels)) return [];
|
|
3163
|
-
return levels.filter((l) => !!l && typeof l === "object" && typeof l.value === "number").map((l) => ({
|
|
3164
|
-
value: l.value,
|
|
3165
|
-
visible: l.visible !== false,
|
|
3166
|
-
...typeof l.color === "string" ? { color: l.color } : {},
|
|
3167
|
-
...typeof l.text === "string" ? { text: l.text } : {}
|
|
3168
|
-
}));
|
|
3169
|
-
}
|
|
3170
|
-
|
|
3171
|
-
// src/internal/drawings/render/textEntry.ts
|
|
3172
|
-
var SELECTION_WASH = "rgba(255, 255, 255, 0.2)";
|
|
3173
|
-
var CARET_WIDTH = 2;
|
|
3174
|
-
var lineMiddle = (lineHeight) => lineHeight / 2 + 1;
|
|
3175
|
-
var caretHeight = (font, lineHeight) => Math.min(lineHeight, Number(/(\d+(?:\.\d+)?)px/.exec(font)?.[1]) || lineHeight);
|
|
3176
|
-
function paintTextEntry(ctx, p) {
|
|
3177
|
-
const draft = p.draft;
|
|
3178
|
-
const shown = p.placeholder ? p.placeholder.block : p.block;
|
|
3179
|
-
const lineTop = (i) => p.y + i * p.lineHeight;
|
|
3180
|
-
const left = (block, i) => p.x + lineLeft(block, i, p.align, p.width);
|
|
3181
|
-
ctx.save();
|
|
3182
|
-
ctx.setLineDash([]);
|
|
3183
|
-
ctx.direction = "ltr";
|
|
3184
|
-
if (draft && draft.selectionStart !== draft.selectionEnd && !p.placeholder) {
|
|
3185
|
-
ctx.fillStyle = SELECTION_WASH;
|
|
3186
|
-
for (const span of runSpans(p.block, draft.selectionStart, draft.selectionEnd, p.measure)) {
|
|
3187
|
-
const from = Math.round(left(p.block, span.line) + span.x0);
|
|
3188
|
-
const to = Math.ceil(left(p.block, span.line) + span.x1);
|
|
3189
|
-
if (to > from) ctx.fillRect(from, lineTop(span.line), to - from, p.lineHeight);
|
|
3190
|
-
}
|
|
3191
|
-
}
|
|
3192
|
-
ctx.font = p.font;
|
|
3193
|
-
ctx.fillStyle = p.color;
|
|
3194
|
-
ctx.textAlign = "left";
|
|
3195
|
-
ctx.textBaseline = "middle";
|
|
3196
|
-
const alpha = ctx.globalAlpha;
|
|
3197
|
-
if (p.placeholder) ctx.globalAlpha = alpha * p.placeholder.alpha;
|
|
3198
|
-
shown.lines.forEach((l, i) => {
|
|
3199
|
-
if (l.text === "") return;
|
|
3200
|
-
ctx.fillText(l.text, left(shown, i), lineTop(i) + lineMiddle(p.lineHeight));
|
|
3201
|
-
});
|
|
3202
|
-
ctx.globalAlpha = alpha;
|
|
3203
|
-
if (draft?.composition && !p.placeholder) {
|
|
3204
|
-
for (const span of runSpans(p.block, draft.composition.start, draft.composition.end, p.measure)) {
|
|
3205
|
-
const from = Math.round(left(p.block, span.line) + span.x0);
|
|
3206
|
-
const to = Math.ceil(left(p.block, span.line) + span.x1);
|
|
3207
|
-
if (to > from) ctx.fillRect(from, lineTop(span.line) + p.lineHeight - 1, to - from, 1);
|
|
3208
|
-
}
|
|
3209
|
-
}
|
|
3210
|
-
if (draft?.caret && draft.selectionStart === draft.selectionEnd) {
|
|
3211
|
-
const at = caretPlace(p.block, draft.selectionEnd, p.measure);
|
|
3212
|
-
const tall = caretHeight(p.font, p.lineHeight);
|
|
3213
|
-
ctx.fillRect(Math.round(left(p.block, at.line) + at.x) - CARET_WIDTH / 2, lineTop(at.line) + Math.round((p.lineHeight - tall) / 2), CARET_WIDTH, tall);
|
|
3214
|
-
}
|
|
3215
|
-
ctx.restore();
|
|
3216
|
-
}
|
|
3217
|
-
function paintWordsFrame(ctx, box2, color, alpha) {
|
|
3218
|
-
ctx.save();
|
|
3219
|
-
ctx.setLineDash([]);
|
|
3220
|
-
ctx.globalAlpha = ctx.globalAlpha * alpha;
|
|
3221
|
-
ctx.strokeStyle = color;
|
|
3222
|
-
ctx.lineWidth = 2;
|
|
3223
|
-
ctx.strokeRect(box2.x - 1, box2.y - 1, box2.width + 2, box2.height + 2);
|
|
3224
|
-
ctx.restore();
|
|
3225
|
-
}
|
|
3226
|
-
|
|
3227
3912
|
// src/internal/drawings/render/glyphArtwork.ts
|
|
3228
3913
|
var glyphImages = /* @__PURE__ */ new Map();
|
|
3229
3914
|
function glyphArtwork(url, onLoad) {
|
|
@@ -3528,6 +4213,10 @@ var Note = class extends Drawing {
|
|
|
3528
4213
|
const at = this.place(viewport);
|
|
3529
4214
|
return !!at && inBox(point, at.box);
|
|
3530
4215
|
}
|
|
4216
|
+
/** Over the selected note's box the cursor reads as its words. */
|
|
4217
|
+
cursorAt(point, viewport) {
|
|
4218
|
+
return this.state === "selected" && this.wordsAt(point, viewport) ? "text" : null;
|
|
4219
|
+
}
|
|
3531
4220
|
/** An empty note shows its placeholder in its own box, so it needs no hint above it. */
|
|
3532
4221
|
paintTextHint() {
|
|
3533
4222
|
}
|
|
@@ -4892,7 +5581,7 @@ var RangeMeter = class extends Drawing {
|
|
|
4892
5581
|
}
|
|
4893
5582
|
if (this.props.showDateTimeRange) {
|
|
4894
5583
|
const secs = Number(b.time) - Number(a.time);
|
|
4895
|
-
if (Number.isFinite(secs) && secs !== 0) out.push(
|
|
5584
|
+
if (Number.isFinite(secs) && secs !== 0) out.push(spanText2(Math.abs(secs)));
|
|
4896
5585
|
}
|
|
4897
5586
|
if (this.props.showVolume) {
|
|
4898
5587
|
const range = barsInRange(this.bars(), a.time, b.time);
|
|
@@ -5096,7 +5785,7 @@ var Measure = class extends Drawing {
|
|
|
5096
5785
|
const secs = Math.abs(Number(b.time) - Number(a.time));
|
|
5097
5786
|
const line22 = [];
|
|
5098
5787
|
if (bars !== null) line22.push(`${Math.abs(Math.round(bars))} bars`);
|
|
5099
|
-
if (Number.isFinite(secs) && secs > 0) line22.push(
|
|
5788
|
+
if (Number.isFinite(secs) && secs > 0) line22.push(spanText2(secs));
|
|
5100
5789
|
const lines = line22.length ? [parts[0], line22.join(", ")] : [parts[0]];
|
|
5101
5790
|
ctx.font = `600 ${this.style.fontSize}px ui-sans-serif, system-ui, sans-serif`;
|
|
5102
5791
|
ctx.textAlign = "center";
|
|
@@ -5123,7 +5812,7 @@ var Measure = class extends Drawing {
|
|
|
5123
5812
|
return inBox2(point, box(pa, pb));
|
|
5124
5813
|
}
|
|
5125
5814
|
};
|
|
5126
|
-
function
|
|
5815
|
+
function spanText2(seconds) {
|
|
5127
5816
|
const d = Math.floor(seconds / 86400);
|
|
5128
5817
|
const h = Math.floor(seconds % 86400 / 3600);
|
|
5129
5818
|
const m = Math.floor(seconds % 3600 / 60);
|
|
@@ -9785,18 +10474,19 @@ var TOOL_CATEGORIES = [
|
|
|
9785
10474
|
"annotation",
|
|
9786
10475
|
"content"
|
|
9787
10476
|
];
|
|
10477
|
+
var LINE_WORDS = { inlineText: { clickToType: true, whenEmpty: "keep" }, settingsOnDoubleClick: true };
|
|
9788
10478
|
var DEFINITIONS = [
|
|
9789
10479
|
// Lines
|
|
9790
|
-
tool(TrendLine, { type: "trend_line", name: "Trend line", category: "lines", anchors: 2, style: LINE_LOOK }),
|
|
9791
|
-
tool(Ray, { type: "ray", name: "Ray", category: "lines", anchors: 2, style: LINE_LOOK }),
|
|
9792
|
-
tool(InfoLine, { type: "info_line", name: "Info line", category: "lines", anchors: 2, style: LINE_LOOK }),
|
|
9793
|
-
tool(ExtendedLine, { type: "extended", name: "Extended line", category: "lines", anchors: 2, style: LINE_LOOK }),
|
|
9794
|
-
tool(TrendAngle, { type: "trend_angle", name: "Trend angle", category: "lines", anchors: 2, style: { ...LINE_LOOK, fontSize: 12 } }),
|
|
9795
|
-
tool(HorizontalLine, { type: "horizontal_line", name: "Horizontal line", category: "lines", anchors: 1, style: { ...LINE_LOOK, fontSize: 12 } }),
|
|
9796
|
-
tool(HorizontalRay, { type: "horizontal_ray", name: "Horizontal ray", category: "lines", anchors: 1, style: { ...LINE_LOOK, fontSize: 12 } }),
|
|
9797
|
-
tool(VerticalLine, { type: "vertical_line", name: "Vertical line", category: "lines", anchors: 1, style: LINE_LOOK }),
|
|
9798
|
-
tool(CrossLine, { type: "cross_line", name: "Cross line", category: "lines", anchors: 1, style: LINE_LOOK }),
|
|
9799
|
-
tool(Arrow, { type: "arrow", name: "Arrow", category: "lines", anchors: 2, style: LINE_LOOK }),
|
|
10480
|
+
tool(TrendLine, { type: "trend_line", name: "Trend line", category: "lines", anchors: 2, style: LINE_LOOK, ...LINE_WORDS }),
|
|
10481
|
+
tool(Ray, { type: "ray", name: "Ray", category: "lines", anchors: 2, style: LINE_LOOK, ...LINE_WORDS }),
|
|
10482
|
+
tool(InfoLine, { type: "info_line", name: "Info line", category: "lines", anchors: 2, style: LINE_LOOK, ...LINE_WORDS }),
|
|
10483
|
+
tool(ExtendedLine, { type: "extended", name: "Extended line", category: "lines", anchors: 2, style: LINE_LOOK, ...LINE_WORDS }),
|
|
10484
|
+
tool(TrendAngle, { type: "trend_angle", name: "Trend angle", category: "lines", anchors: 2, settingsOnDoubleClick: true, style: { ...LINE_LOOK, fontSize: 12 } }),
|
|
10485
|
+
tool(HorizontalLine, { type: "horizontal_line", name: "Horizontal line", category: "lines", anchors: 1, style: { ...LINE_LOOK, fontSize: 12 }, ...LINE_WORDS }),
|
|
10486
|
+
tool(HorizontalRay, { type: "horizontal_ray", name: "Horizontal ray", category: "lines", anchors: 1, settingsOnDoubleClick: true, style: { ...LINE_LOOK, fontSize: 12 } }),
|
|
10487
|
+
tool(VerticalLine, { type: "vertical_line", name: "Vertical line", category: "lines", anchors: 1, style: LINE_LOOK, ...LINE_WORDS }),
|
|
10488
|
+
tool(CrossLine, { type: "cross_line", name: "Cross line", category: "lines", anchors: 1, settingsOnDoubleClick: true, style: LINE_LOOK }),
|
|
10489
|
+
tool(Arrow, { type: "arrow", name: "Arrow", category: "lines", anchors: 2, style: LINE_LOOK, ...LINE_WORDS }),
|
|
9800
10490
|
// Shapes
|
|
9801
10491
|
tool(Rectangle, { type: "rectangle", name: "Rectangle", category: "shapes", anchors: 2, style: shapeLook("#9c27b0") }),
|
|
9802
10492
|
tool(RotatedRectangle, { type: "rotated_rectangle", name: "Rotated rectangle", category: "shapes", anchors: 3, style: shapeLook("#4caf50") }),
|
|
@@ -10540,6 +11230,10 @@ function placePanel(panel, anchor, box2, mode, gap = 2) {
|
|
|
10540
11230
|
const lift = Number.parseFloat(getComputedStyle(panel).paddingTop) || 0;
|
|
10541
11231
|
top = a.top - b.top - lift;
|
|
10542
11232
|
if (top + h > b.height) top = Math.max(0, b.height - h);
|
|
11233
|
+
} else if (mode === "above") {
|
|
11234
|
+
left = rtl ? a.right - b.left - w : a.left - b.left;
|
|
11235
|
+
top = a.top - b.top - h - gap;
|
|
11236
|
+
if (top < 0) top = a.bottom - b.top + gap;
|
|
10543
11237
|
} else {
|
|
10544
11238
|
const end = mode === "below-end" !== rtl;
|
|
10545
11239
|
left = end ? a.right - b.left - w : a.left - b.left;
|
|
@@ -11256,6 +11950,12 @@ var SAVED_LOOK_PROPS = {
|
|
|
11256
11950
|
bars_pattern: ["candles"],
|
|
11257
11951
|
fixed_range_volume_profile: PROFILE,
|
|
11258
11952
|
anchored_volume_profile: PROFILE,
|
|
11953
|
+
trend_line: SAVED_LOOK,
|
|
11954
|
+
ray: SAVED_LOOK,
|
|
11955
|
+
info_line: SAVED_LOOK,
|
|
11956
|
+
extended: SAVED_LOOK,
|
|
11957
|
+
trend_angle: SAVED_LOOK,
|
|
11958
|
+
arrow: SAVED_LOOK,
|
|
11259
11959
|
callout: ["borderWidth", ...SAVED_LOOK],
|
|
11260
11960
|
price_label: SAVED_LOOK,
|
|
11261
11961
|
pin: SAVED_LOOK,
|
|
@@ -11275,7 +11975,7 @@ var SAVED_LOOK_PROPS = {
|
|
|
11275
11975
|
};
|
|
11276
11976
|
var INERT_PROPS = {
|
|
11277
11977
|
...SAVED_LOOK_PROPS,
|
|
11278
|
-
trend_angle: ["text", "leftEnd", "rightEnd", "showDateTimeRange", "showAngle"],
|
|
11978
|
+
trend_angle: ["text", "leftEnd", "rightEnd", "showDateTimeRange", "showDistance", "showAngle", ...SAVED_LOOK],
|
|
11279
11979
|
table: ["cells", "headerRow", ...SAVED_LOOK]
|
|
11280
11980
|
};
|
|
11281
11981
|
var NO_COORDINATES_TAB = /* @__PURE__ */ new Set(["rotated_rectangle", "arc", "polyline", "ellipse", "fib_wedge", "flat_top_bottom", "disjoint_channel", "sector", "bars_pattern", "long_position", "short_position", "anchored_vwap", "text", "table", "brush", "highlighter", "path", "emoji", "sticker", "icon", "image"]);
|
|
@@ -11311,6 +12011,9 @@ export {
|
|
|
11311
12011
|
drawing,
|
|
11312
12012
|
DARK_THEME,
|
|
11313
12013
|
BUILT_IN_THEMES,
|
|
12014
|
+
CHART_FACTORY_COLORS,
|
|
12015
|
+
BRAND_UP,
|
|
12016
|
+
BRAND_DOWN,
|
|
11314
12017
|
parseCssColor,
|
|
11315
12018
|
DEFAULT_VISIBILITY,
|
|
11316
12019
|
visibilityPreset,
|
|
@@ -11319,15 +12022,20 @@ export {
|
|
|
11319
12022
|
visibleAt,
|
|
11320
12023
|
DEFAULT_STYLE,
|
|
11321
12024
|
DEFAULT_OPTIONS,
|
|
12025
|
+
SERIAL_VERSION,
|
|
11322
12026
|
drawingInksOf,
|
|
11323
12027
|
alphaOf,
|
|
11324
12028
|
withAlpha,
|
|
12029
|
+
paneIndexOf,
|
|
12030
|
+
paneTopIn,
|
|
12031
|
+
seriesPaneTop,
|
|
11325
12032
|
viewportOf,
|
|
11326
12033
|
DrawingManager,
|
|
11327
12034
|
snapToBar,
|
|
11328
12035
|
magnetSnap,
|
|
11329
12036
|
primeImageBitmap,
|
|
11330
12037
|
BAR_PRICE_SOURCES,
|
|
12038
|
+
volumeText,
|
|
11331
12039
|
fibLevelColor,
|
|
11332
12040
|
MIXED_LEVEL_COLORS,
|
|
11333
12041
|
sharedLevelColor,
|
|
@@ -11446,4 +12154,4 @@ export {
|
|
|
11446
12154
|
fitScale,
|
|
11447
12155
|
fittedSize
|
|
11448
12156
|
};
|
|
11449
|
-
//# sourceMappingURL=chunk-
|
|
12157
|
+
//# sourceMappingURL=chunk-Q3DCXQDW.js.map
|