@tranding-custom-chart/renderer-canvas 0.0.0-stage → 2.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/README.md +40 -2
- package/dist/canvas-engine.d.ts +82 -0
- package/dist/canvas-engine.d.ts.map +1 -0
- package/dist/canvas-engine.js +594 -0
- package/dist/dpi.d.ts +31 -0
- package/dist/dpi.d.ts.map +1 -0
- package/dist/dpi.js +65 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/renderers/bar-renderer.d.ts +26 -0
- package/dist/renderers/bar-renderer.d.ts.map +1 -0
- package/dist/renderers/bar-renderer.js +45 -0
- package/dist/renderers/baseline-renderer.d.ts +20 -0
- package/dist/renderers/baseline-renderer.d.ts.map +1 -0
- package/dist/renderers/baseline-renderer.js +101 -0
- package/dist/renderers/box-plot-renderer.d.ts +14 -0
- package/dist/renderers/box-plot-renderer.d.ts.map +1 -0
- package/dist/renderers/box-plot-renderer.js +76 -0
- package/dist/renderers/bubble-renderer.d.ts +14 -0
- package/dist/renderers/bubble-renderer.d.ts.map +1 -0
- package/dist/renderers/bubble-renderer.js +37 -0
- package/dist/renderers/candlestick-renderer.d.ts +18 -0
- package/dist/renderers/candlestick-renderer.d.ts.map +1 -0
- package/dist/renderers/candlestick-renderer.js +87 -0
- package/dist/renderers/columns-renderer.d.ts +21 -0
- package/dist/renderers/columns-renderer.d.ts.map +1 -0
- package/dist/renderers/columns-renderer.js +35 -0
- package/dist/renderers/crosshair-renderer.d.ts +19 -0
- package/dist/renderers/crosshair-renderer.d.ts.map +1 -0
- package/dist/renderers/crosshair-renderer.js +71 -0
- package/dist/renderers/curved-line-renderer.d.ts +17 -0
- package/dist/renderers/curved-line-renderer.d.ts.map +1 -0
- package/dist/renderers/curved-line-renderer.js +46 -0
- package/dist/renderers/drawing-renderer.d.ts +19 -0
- package/dist/renderers/drawing-renderer.d.ts.map +1 -0
- package/dist/renderers/drawing-renderer.js +487 -0
- package/dist/renderers/grid-renderer.d.ts +26 -0
- package/dist/renderers/grid-renderer.d.ts.map +1 -0
- package/dist/renderers/grid-renderer.js +103 -0
- package/dist/renderers/heatmap-renderer.d.ts +16 -0
- package/dist/renderers/heatmap-renderer.d.ts.map +1 -0
- package/dist/renderers/heatmap-renderer.js +80 -0
- package/dist/renderers/high-low-renderer.d.ts +19 -0
- package/dist/renderers/high-low-renderer.d.ts.map +1 -0
- package/dist/renderers/high-low-renderer.js +28 -0
- package/dist/renderers/hlc-area-renderer.d.ts +22 -0
- package/dist/renderers/hlc-area-renderer.d.ts.map +1 -0
- package/dist/renderers/hlc-area-renderer.js +72 -0
- package/dist/renderers/hollow-candlestick-renderer.d.ts +26 -0
- package/dist/renderers/hollow-candlestick-renderer.d.ts.map +1 -0
- package/dist/renderers/hollow-candlestick-renderer.js +71 -0
- package/dist/renderers/horizontal-bar-renderer.d.ts +25 -0
- package/dist/renderers/horizontal-bar-renderer.d.ts.map +1 -0
- package/dist/renderers/horizontal-bar-renderer.js +52 -0
- package/dist/renderers/last-price-renderer.d.ts +17 -0
- package/dist/renderers/last-price-renderer.d.ts.map +1 -0
- package/dist/renderers/last-price-renderer.js +46 -0
- package/dist/renderers/line-renderer.d.ts +23 -0
- package/dist/renderers/line-renderer.d.ts.map +1 -0
- package/dist/renderers/line-renderer.js +80 -0
- package/dist/renderers/line-with-markers-renderer.d.ts +13 -0
- package/dist/renderers/line-with-markers-renderer.d.ts.map +1 -0
- package/dist/renderers/line-with-markers-renderer.js +35 -0
- package/dist/renderers/marker-renderer.d.ts +16 -0
- package/dist/renderers/marker-renderer.d.ts.map +1 -0
- package/dist/renderers/marker-renderer.js +91 -0
- package/dist/renderers/mixed-renderer.d.ts +18 -0
- package/dist/renderers/mixed-renderer.d.ts.map +1 -0
- package/dist/renderers/mixed-renderer.js +123 -0
- package/dist/renderers/ohlc-bar-renderer.d.ts +21 -0
- package/dist/renderers/ohlc-bar-renderer.d.ts.map +1 -0
- package/dist/renderers/ohlc-bar-renderer.js +73 -0
- package/dist/renderers/pie-renderer.d.ts +26 -0
- package/dist/renderers/pie-renderer.d.ts.map +1 -0
- package/dist/renderers/pie-renderer.js +71 -0
- package/dist/renderers/polar-area-renderer.d.ts +13 -0
- package/dist/renderers/polar-area-renderer.d.ts.map +1 -0
- package/dist/renderers/polar-area-renderer.js +44 -0
- package/dist/renderers/radar-renderer.d.ts +16 -0
- package/dist/renderers/radar-renderer.d.ts.map +1 -0
- package/dist/renderers/radar-renderer.js +99 -0
- package/dist/renderers/scatter-renderer.d.ts +14 -0
- package/dist/renderers/scatter-renderer.d.ts.map +1 -0
- package/dist/renderers/scatter-renderer.js +42 -0
- package/dist/renderers/stacked-bar-renderer.d.ts +14 -0
- package/dist/renderers/stacked-bar-renderer.d.ts.map +1 -0
- package/dist/renderers/stacked-bar-renderer.js +52 -0
- package/dist/renderers/step-line-renderer.d.ts +19 -0
- package/dist/renderers/step-line-renderer.d.ts.map +1 -0
- package/dist/renderers/step-line-renderer.js +52 -0
- package/dist/renderers/supertrend-renderer.d.ts +18 -0
- package/dist/renderers/supertrend-renderer.d.ts.map +1 -0
- package/dist/renderers/supertrend-renderer.js +80 -0
- package/dist/renderers/volume-profile-renderer.d.ts +25 -0
- package/dist/renderers/volume-profile-renderer.d.ts.map +1 -0
- package/dist/renderers/volume-profile-renderer.js +83 -0
- package/dist/renderers/watermark-renderer.d.ts +15 -0
- package/dist/renderers/watermark-renderer.d.ts.map +1 -0
- package/dist/renderers/watermark-renderer.js +26 -0
- package/dist/scheduler.d.ts +25 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +77 -0
- package/package.json +33 -4
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file packages/chart-renderer-canvas/src/renderers/crosshair-renderer.ts
|
|
3
|
+
* Interactive crosshair guidelines, price/time axis badges, and highlight markers.
|
|
4
|
+
*/
|
|
5
|
+
import { snapLine } from '../dpi.js';
|
|
6
|
+
export function renderCrosshair(options) {
|
|
7
|
+
const { ctx, bounds, padding, x, y, theme, dpr, xLabel, yLabel, showHorizontalLine = true } = options;
|
|
8
|
+
const left = padding.left;
|
|
9
|
+
const right = bounds.width - padding.right;
|
|
10
|
+
const top = padding.top;
|
|
11
|
+
const bottom = bounds.height - padding.bottom;
|
|
12
|
+
// X must fall within chart plot area
|
|
13
|
+
if (x < left || x > right)
|
|
14
|
+
return;
|
|
15
|
+
// If drawing horizontal line, Y must also fall within bounds
|
|
16
|
+
if (showHorizontalLine && (y < top || y > bottom))
|
|
17
|
+
return;
|
|
18
|
+
const snappedX = snapLine(x, dpr);
|
|
19
|
+
ctx.save();
|
|
20
|
+
ctx.strokeStyle = theme.crosshairLine;
|
|
21
|
+
ctx.lineWidth = 1;
|
|
22
|
+
ctx.setLineDash([3, 3]);
|
|
23
|
+
// Vertical guideline
|
|
24
|
+
ctx.beginPath();
|
|
25
|
+
ctx.moveTo(snappedX, top);
|
|
26
|
+
ctx.lineTo(snappedX, bottom);
|
|
27
|
+
ctx.stroke();
|
|
28
|
+
// Horizontal guideline
|
|
29
|
+
if (showHorizontalLine && y >= top && y <= bottom) {
|
|
30
|
+
const snappedY = snapLine(y, dpr);
|
|
31
|
+
ctx.beginPath();
|
|
32
|
+
ctx.moveTo(left, snappedY);
|
|
33
|
+
ctx.lineTo(right, snappedY);
|
|
34
|
+
ctx.stroke();
|
|
35
|
+
}
|
|
36
|
+
ctx.setLineDash([]);
|
|
37
|
+
ctx.font = '10px monospace';
|
|
38
|
+
ctx.textAlign = 'center';
|
|
39
|
+
ctx.textBaseline = 'middle';
|
|
40
|
+
// Y-Axis Price Badge (Right margin)
|
|
41
|
+
if (yLabel && showHorizontalLine && y >= top && y <= bottom) {
|
|
42
|
+
const textWidth = ctx.measureText(yLabel).width;
|
|
43
|
+
const badgeWidth = textWidth + 12;
|
|
44
|
+
const badgeHeight = 18;
|
|
45
|
+
const badgeX = right + 2;
|
|
46
|
+
const badgeY = y - badgeHeight / 2;
|
|
47
|
+
ctx.fillStyle = theme.surfaceBorder;
|
|
48
|
+
ctx.beginPath();
|
|
49
|
+
ctx.roundRect?.(badgeX, badgeY, badgeWidth, badgeHeight, 3);
|
|
50
|
+
ctx.fill();
|
|
51
|
+
ctx.fillStyle = theme.textPrimary;
|
|
52
|
+
ctx.textAlign = 'left';
|
|
53
|
+
ctx.fillText(yLabel, badgeX + 6, y);
|
|
54
|
+
}
|
|
55
|
+
// X-Axis Time Badge (Bottom margin)
|
|
56
|
+
if (xLabel) {
|
|
57
|
+
const textWidth = ctx.measureText(xLabel).width;
|
|
58
|
+
const badgeWidth = textWidth + 12;
|
|
59
|
+
const badgeHeight = 18;
|
|
60
|
+
const badgeX = Math.max(left, Math.min(right - badgeWidth, x - badgeWidth / 2));
|
|
61
|
+
const badgeY = bottom + 2;
|
|
62
|
+
ctx.fillStyle = theme.surfaceBorder;
|
|
63
|
+
ctx.beginPath();
|
|
64
|
+
ctx.roundRect?.(badgeX, badgeY, badgeWidth, badgeHeight, 3);
|
|
65
|
+
ctx.fill();
|
|
66
|
+
ctx.fillStyle = theme.textPrimary;
|
|
67
|
+
ctx.textAlign = 'center';
|
|
68
|
+
ctx.fillText(xLabel, badgeX + badgeWidth / 2, badgeY + badgeHeight / 2);
|
|
69
|
+
}
|
|
70
|
+
ctx.restore();
|
|
71
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file packages/chart-renderer-canvas/src/renderers/curved-line-renderer.ts
|
|
3
|
+
* Canvas 2D renderer for Catmull-Rom cubic Bezier curved lines and gradient areas.
|
|
4
|
+
*/
|
|
5
|
+
import { type Point } from '@tranding-custom-chart/core';
|
|
6
|
+
export interface CurvedLineOptions {
|
|
7
|
+
readonly strokeColor?: string | undefined;
|
|
8
|
+
readonly strokeWidth?: number | undefined;
|
|
9
|
+
readonly tension?: number | undefined;
|
|
10
|
+
readonly areaFill?: {
|
|
11
|
+
readonly gradientTopColor: string;
|
|
12
|
+
readonly gradientBottomColor: string;
|
|
13
|
+
readonly baselineY: number;
|
|
14
|
+
} | undefined;
|
|
15
|
+
}
|
|
16
|
+
export declare function renderCurvedLine(ctx: CanvasRenderingContext2D, points: readonly Point[], options?: CurvedLineOptions): void;
|
|
17
|
+
//# sourceMappingURL=curved-line-renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"curved-line-renderer.d.ts","sourceRoot":"","sources":["../../src/renderers/curved-line-renderer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAmC,KAAK,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAE1F,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,QAAQ,CAAC,EACd;QACE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;QAClC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;QACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;KAC5B,GACD,SAAS,CAAC;CACf;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,wBAAwB,EAC7B,MAAM,EAAE,SAAS,KAAK,EAAE,EACxB,OAAO,GAAE,iBAAsB,GAC9B,IAAI,CAqDN"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file packages/chart-renderer-canvas/src/renderers/curved-line-renderer.ts
|
|
3
|
+
* Canvas 2D renderer for Catmull-Rom cubic Bezier curved lines and gradient areas.
|
|
4
|
+
*/
|
|
5
|
+
import { computeCatmullRomBezierSegments } from '@tranding-custom-chart/core';
|
|
6
|
+
export function renderCurvedLine(ctx, points, options = {}) {
|
|
7
|
+
if (points.length < 2)
|
|
8
|
+
return;
|
|
9
|
+
const { strokeColor = '#3b82f6', strokeWidth = 2, tension = 0.8, areaFill, } = options;
|
|
10
|
+
const segments = computeCatmullRomBezierSegments(points, tension);
|
|
11
|
+
if (segments.length === 0)
|
|
12
|
+
return;
|
|
13
|
+
ctx.save();
|
|
14
|
+
// 1. Optional Area Fill
|
|
15
|
+
if (areaFill) {
|
|
16
|
+
const first = points[0];
|
|
17
|
+
const last = points[points.length - 1];
|
|
18
|
+
const minY = Math.min(...points.map((p) => p.y));
|
|
19
|
+
const gradient = ctx.createLinearGradient(0, minY, 0, areaFill.baselineY);
|
|
20
|
+
gradient.addColorStop(0, areaFill.gradientTopColor);
|
|
21
|
+
gradient.addColorStop(1, areaFill.gradientBottomColor);
|
|
22
|
+
ctx.beginPath();
|
|
23
|
+
ctx.moveTo(first.x, first.y);
|
|
24
|
+
for (const seg of segments) {
|
|
25
|
+
ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.p2.x, seg.p2.y);
|
|
26
|
+
}
|
|
27
|
+
ctx.lineTo(last.x, areaFill.baselineY);
|
|
28
|
+
ctx.lineTo(first.x, areaFill.baselineY);
|
|
29
|
+
ctx.closePath();
|
|
30
|
+
ctx.fillStyle = gradient;
|
|
31
|
+
ctx.fill();
|
|
32
|
+
}
|
|
33
|
+
// 2. Continuous Curved Line Stroke
|
|
34
|
+
ctx.beginPath();
|
|
35
|
+
const first = points[0];
|
|
36
|
+
ctx.moveTo(first.x, first.y);
|
|
37
|
+
for (const seg of segments) {
|
|
38
|
+
ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.p2.x, seg.p2.y);
|
|
39
|
+
}
|
|
40
|
+
ctx.lineWidth = strokeWidth;
|
|
41
|
+
ctx.strokeStyle = strokeColor;
|
|
42
|
+
ctx.lineCap = 'round';
|
|
43
|
+
ctx.lineJoin = 'round';
|
|
44
|
+
ctx.stroke();
|
|
45
|
+
ctx.restore();
|
|
46
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file packages/chart-renderer-canvas/src/renderers/drawing-renderer.ts
|
|
3
|
+
* Interactive technical drawing renderer: Trendlines, Horizontal Support/Resistance Rays,
|
|
4
|
+
* and Price/Bar Range Measurement tools.
|
|
5
|
+
*/
|
|
6
|
+
import type { Dimension, DrawingHandleType, DrawingItem, Padding, Scale } from '@tranding-custom-chart/core';
|
|
7
|
+
export interface DrawingRenderOptions {
|
|
8
|
+
readonly ctx: CanvasRenderingContext2D;
|
|
9
|
+
readonly drawings: readonly DrawingItem[];
|
|
10
|
+
readonly xScale: Scale<number>;
|
|
11
|
+
readonly yScale: Scale<number>;
|
|
12
|
+
readonly bounds: Dimension;
|
|
13
|
+
readonly padding: Padding;
|
|
14
|
+
readonly dpr: number;
|
|
15
|
+
readonly selectedDrawingId?: string | null | undefined;
|
|
16
|
+
readonly activeHandle?: DrawingHandleType | null | undefined;
|
|
17
|
+
}
|
|
18
|
+
export declare function renderDrawings(options: DrawingRenderOptions): void;
|
|
19
|
+
//# sourceMappingURL=drawing-renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drawing-renderer.d.ts","sourceRoot":"","sources":["../../src/renderers/drawing-renderer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,iBAAiB,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAE7G,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,GAAG,EAAE,wBAAwB,CAAC;IACvC,QAAQ,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAC;IAC1C,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACvD,QAAQ,CAAC,YAAY,CAAC,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS,CAAC;CAC9D;AA4CD,wBAAgB,cAAc,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAkflE"}
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file packages/chart-renderer-canvas/src/renderers/drawing-renderer.ts
|
|
3
|
+
* Interactive technical drawing renderer: Trendlines, Horizontal Support/Resistance Rays,
|
|
4
|
+
* and Price/Bar Range Measurement tools.
|
|
5
|
+
*/
|
|
6
|
+
function drawControlHandle(ctx, x, y, isActive, isLocked) {
|
|
7
|
+
ctx.save();
|
|
8
|
+
ctx.beginPath();
|
|
9
|
+
const radius = isActive ? 7 : 5;
|
|
10
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
11
|
+
ctx.fillStyle = isLocked ? '#f59e0b' : '#ffffff';
|
|
12
|
+
ctx.fill();
|
|
13
|
+
ctx.strokeStyle = isLocked ? '#78350f' : '#2563eb';
|
|
14
|
+
ctx.lineWidth = 2;
|
|
15
|
+
ctx.stroke();
|
|
16
|
+
ctx.restore();
|
|
17
|
+
}
|
|
18
|
+
function drawSelectionBadge(ctx, x, y, label, isLocked) {
|
|
19
|
+
ctx.save();
|
|
20
|
+
ctx.font = 'bold 9px sans-serif';
|
|
21
|
+
const width = ctx.measureText(label).width + 12;
|
|
22
|
+
const height = 16;
|
|
23
|
+
ctx.fillStyle = isLocked ? 'rgba(120, 53, 15, 0.9)' : 'rgba(30, 41, 59, 0.9)';
|
|
24
|
+
ctx.strokeStyle = isLocked ? '#f59e0b' : '#3b82f6';
|
|
25
|
+
ctx.lineWidth = 1;
|
|
26
|
+
ctx.fillRect(x - width / 2, y - height - 4, width, height);
|
|
27
|
+
ctx.strokeRect(x - width / 2, y - height - 4, width, height);
|
|
28
|
+
ctx.fillStyle = '#ffffff';
|
|
29
|
+
ctx.textAlign = 'center';
|
|
30
|
+
ctx.textBaseline = 'middle';
|
|
31
|
+
ctx.fillText(label, x, y - height / 2 - 4);
|
|
32
|
+
ctx.restore();
|
|
33
|
+
}
|
|
34
|
+
export function renderDrawings(options) {
|
|
35
|
+
const { ctx, drawings, xScale, yScale, bounds, padding, selectedDrawingId, activeHandle } = options;
|
|
36
|
+
if (drawings.length === 0)
|
|
37
|
+
return;
|
|
38
|
+
const chartLeft = padding.left;
|
|
39
|
+
const chartRight = bounds.width - padding.right;
|
|
40
|
+
ctx.save();
|
|
41
|
+
for (const item of drawings) {
|
|
42
|
+
if (item.visible === false)
|
|
43
|
+
continue;
|
|
44
|
+
const isSelected = selectedDrawingId === item.id;
|
|
45
|
+
const isLocked = Boolean(item.locked);
|
|
46
|
+
const strokeWidth = item.strokeWidth ?? (isSelected ? 2.5 : 2);
|
|
47
|
+
const opacity = item.opacity ?? 1.0;
|
|
48
|
+
ctx.save();
|
|
49
|
+
ctx.globalAlpha = opacity;
|
|
50
|
+
if (isSelected) {
|
|
51
|
+
ctx.shadowColor = isLocked ? 'rgba(245, 158, 11, 0.6)' : 'rgba(59, 130, 246, 0.6)';
|
|
52
|
+
ctx.shadowBlur = 8;
|
|
53
|
+
}
|
|
54
|
+
if (item.type === 'trendline') {
|
|
55
|
+
const x1 = xScale.scale(item.p1.index);
|
|
56
|
+
const y1 = yScale.scale(item.p1.price);
|
|
57
|
+
const x2 = xScale.scale(item.p2.index);
|
|
58
|
+
const y2 = yScale.scale(item.p2.price);
|
|
59
|
+
const color = item.color ?? '#60a5fa';
|
|
60
|
+
// Line
|
|
61
|
+
ctx.strokeStyle = color;
|
|
62
|
+
ctx.lineWidth = strokeWidth;
|
|
63
|
+
if (item.lineDash && item.lineDash.length > 0) {
|
|
64
|
+
ctx.setLineDash([...item.lineDash]);
|
|
65
|
+
}
|
|
66
|
+
ctx.beginPath();
|
|
67
|
+
ctx.moveTo(x1, y1);
|
|
68
|
+
ctx.lineTo(x2, y2);
|
|
69
|
+
ctx.stroke();
|
|
70
|
+
ctx.setLineDash([]);
|
|
71
|
+
ctx.shadowBlur = 0;
|
|
72
|
+
// Handles
|
|
73
|
+
if (isSelected) {
|
|
74
|
+
drawControlHandle(ctx, x1, y1, activeHandle === 'p1', isLocked);
|
|
75
|
+
drawControlHandle(ctx, x2, y2, activeHandle === 'p2', isLocked);
|
|
76
|
+
const midX = (x1 + x2) / 2;
|
|
77
|
+
const midY = (y1 + y2) / 2;
|
|
78
|
+
drawSelectionBadge(ctx, midX, midY, isLocked ? '🔒 Trendline (Locked)' : 'Trendline', isLocked);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
ctx.fillStyle = '#ffffff';
|
|
82
|
+
for (const [hx, hy] of [[x1, y1], [x2, y2]]) {
|
|
83
|
+
ctx.beginPath();
|
|
84
|
+
ctx.arc(hx, hy, 3.5, 0, Math.PI * 2);
|
|
85
|
+
ctx.fill();
|
|
86
|
+
ctx.stroke();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else if (item.type === 'horizontal_line') {
|
|
91
|
+
const y = yScale.scale(item.price);
|
|
92
|
+
const color = item.color ?? '#f59e0b';
|
|
93
|
+
ctx.strokeStyle = color;
|
|
94
|
+
ctx.lineWidth = strokeWidth;
|
|
95
|
+
ctx.setLineDash(item.lineDash && item.lineDash.length > 0 ? [...item.lineDash] : [4, 4]);
|
|
96
|
+
ctx.beginPath();
|
|
97
|
+
ctx.moveTo(chartLeft, y);
|
|
98
|
+
ctx.lineTo(chartRight, y);
|
|
99
|
+
ctx.stroke();
|
|
100
|
+
ctx.setLineDash([]);
|
|
101
|
+
ctx.shadowBlur = 0;
|
|
102
|
+
// Handles if selected
|
|
103
|
+
if (isSelected) {
|
|
104
|
+
drawControlHandle(ctx, chartLeft + 15, y, false, isLocked);
|
|
105
|
+
drawControlHandle(ctx, (chartLeft + chartRight) / 2, y, activeHandle === 'body', isLocked);
|
|
106
|
+
drawControlHandle(ctx, chartRight - 15, y, false, isLocked);
|
|
107
|
+
}
|
|
108
|
+
// Label badge
|
|
109
|
+
const label = item.label ?? `Level: ₹${item.price.toFixed(2)}${isLocked ? ' 🔒' : ''}`;
|
|
110
|
+
ctx.font = '10px monospace';
|
|
111
|
+
ctx.fillStyle = color;
|
|
112
|
+
ctx.fillRect(chartRight + 2, y - 9, ctx.measureText(label).width + 8, 18);
|
|
113
|
+
ctx.fillStyle = '#ffffff';
|
|
114
|
+
ctx.textAlign = 'left';
|
|
115
|
+
ctx.textBaseline = 'middle';
|
|
116
|
+
ctx.fillText(label, chartRight + 6, y);
|
|
117
|
+
}
|
|
118
|
+
else if (item.type === 'measure') {
|
|
119
|
+
const x1 = xScale.scale(item.p1.index);
|
|
120
|
+
const y1 = yScale.scale(item.p1.price);
|
|
121
|
+
const x2 = xScale.scale(item.p2.index);
|
|
122
|
+
const y2 = yScale.scale(item.p2.price);
|
|
123
|
+
const left = Math.min(x1, x2);
|
|
124
|
+
const top = Math.min(y1, y2);
|
|
125
|
+
const boxWidth = Math.abs(x2 - x1);
|
|
126
|
+
const boxHeight = Math.abs(y2 - y1);
|
|
127
|
+
const deltaPrice = item.p2.price - item.p1.price;
|
|
128
|
+
const pctChange = (deltaPrice / (item.p1.price || 1)) * 100;
|
|
129
|
+
const deltaBars = Math.abs(item.p2.index - item.p1.index);
|
|
130
|
+
const isPositive = deltaPrice >= 0;
|
|
131
|
+
const boxBg = isPositive ? 'rgba(22, 121, 76, 0.20)' : 'rgba(180, 35, 24, 0.20)';
|
|
132
|
+
const boxBorder = isPositive ? '#16794c' : '#b42318';
|
|
133
|
+
// Shaded measuring rectangle
|
|
134
|
+
ctx.fillStyle = boxBg;
|
|
135
|
+
ctx.fillRect(left, top, boxWidth, boxHeight);
|
|
136
|
+
ctx.strokeStyle = boxBorder;
|
|
137
|
+
ctx.lineWidth = strokeWidth;
|
|
138
|
+
ctx.setLineDash(item.lineDash && item.lineDash.length > 0 ? [...item.lineDash] : [2, 2]);
|
|
139
|
+
ctx.strokeRect(left, top, boxWidth, boxHeight);
|
|
140
|
+
ctx.setLineDash([]);
|
|
141
|
+
// Arrow diagonal line
|
|
142
|
+
ctx.strokeStyle = boxBorder;
|
|
143
|
+
ctx.lineWidth = 1.5;
|
|
144
|
+
ctx.beginPath();
|
|
145
|
+
ctx.moveTo(x1, y1);
|
|
146
|
+
ctx.lineTo(x2, y2);
|
|
147
|
+
ctx.stroke();
|
|
148
|
+
ctx.shadowBlur = 0;
|
|
149
|
+
// Measurement Info Badge in center
|
|
150
|
+
const badgeX = left + boxWidth / 2;
|
|
151
|
+
const badgeY = top + boxHeight / 2;
|
|
152
|
+
const sign = isPositive ? '+' : '';
|
|
153
|
+
const textLine1 = `${sign}₹${deltaPrice.toFixed(2)} (${sign}${pctChange.toFixed(2)}%)`;
|
|
154
|
+
const textLine2 = `${deltaBars} bars${isLocked ? ' 🔒' : ''}`;
|
|
155
|
+
ctx.font = 'bold 11px monospace';
|
|
156
|
+
const w1 = ctx.measureText(textLine1).width;
|
|
157
|
+
ctx.font = '10px sans-serif';
|
|
158
|
+
const w2 = ctx.measureText(textLine2).width;
|
|
159
|
+
const cardWidth = Math.max(w1, w2) + 16;
|
|
160
|
+
const cardHeight = 36;
|
|
161
|
+
ctx.fillStyle = '#181b19';
|
|
162
|
+
ctx.strokeStyle = boxBorder;
|
|
163
|
+
ctx.lineWidth = 1;
|
|
164
|
+
ctx.fillRect(badgeX - cardWidth / 2, badgeY - cardHeight / 2, cardWidth, cardHeight);
|
|
165
|
+
ctx.strokeRect(badgeX - cardWidth / 2, badgeY - cardHeight / 2, cardWidth, cardHeight);
|
|
166
|
+
ctx.textAlign = 'center';
|
|
167
|
+
ctx.textBaseline = 'middle';
|
|
168
|
+
ctx.fillStyle = isPositive ? '#34d399' : '#f87171';
|
|
169
|
+
ctx.font = 'bold 11px monospace';
|
|
170
|
+
ctx.fillText(textLine1, badgeX, badgeY - 7);
|
|
171
|
+
ctx.fillStyle = '#858e89';
|
|
172
|
+
ctx.font = '10px sans-serif';
|
|
173
|
+
ctx.fillText(textLine2, badgeX, badgeY + 8);
|
|
174
|
+
if (isSelected) {
|
|
175
|
+
drawControlHandle(ctx, x1, y1, activeHandle === 'p1', isLocked);
|
|
176
|
+
drawControlHandle(ctx, x2, y2, activeHandle === 'p2', isLocked);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else if (item.type === 'vertical_line') {
|
|
180
|
+
const x = xScale.scale(item.index);
|
|
181
|
+
const color = item.color ?? '#8b5cf6';
|
|
182
|
+
ctx.strokeStyle = color;
|
|
183
|
+
ctx.lineWidth = strokeWidth;
|
|
184
|
+
ctx.setLineDash(item.lineDash && item.lineDash.length > 0 ? [...item.lineDash] : [4, 4]);
|
|
185
|
+
ctx.beginPath();
|
|
186
|
+
ctx.moveTo(x, padding.top);
|
|
187
|
+
ctx.lineTo(x, bounds.height - padding.bottom);
|
|
188
|
+
ctx.stroke();
|
|
189
|
+
ctx.setLineDash([]);
|
|
190
|
+
ctx.shadowBlur = 0;
|
|
191
|
+
if (isSelected) {
|
|
192
|
+
drawControlHandle(ctx, x, padding.top + 15, false, isLocked);
|
|
193
|
+
drawControlHandle(ctx, x, bounds.height / 2, activeHandle === 'body', isLocked);
|
|
194
|
+
drawControlHandle(ctx, x, bounds.height - padding.bottom - 15, false, isLocked);
|
|
195
|
+
}
|
|
196
|
+
const label = item.label ?? `Bar #${item.index}${isLocked ? ' 🔒' : ''}`;
|
|
197
|
+
ctx.font = '10px monospace';
|
|
198
|
+
ctx.fillStyle = color;
|
|
199
|
+
ctx.fillRect(x - 25, bounds.height - padding.bottom + 2, 50, 16);
|
|
200
|
+
ctx.fillStyle = '#ffffff';
|
|
201
|
+
ctx.textAlign = 'center';
|
|
202
|
+
ctx.textBaseline = 'middle';
|
|
203
|
+
ctx.fillText(label, x, bounds.height - padding.bottom + 10);
|
|
204
|
+
}
|
|
205
|
+
else if (item.type === 'rectangle') {
|
|
206
|
+
const x1 = xScale.scale(item.p1.index);
|
|
207
|
+
const y1 = yScale.scale(item.p1.price);
|
|
208
|
+
const x2 = xScale.scale(item.p2.index);
|
|
209
|
+
const y2 = yScale.scale(item.p2.price);
|
|
210
|
+
const left = Math.min(x1, x2);
|
|
211
|
+
const top = Math.min(y1, y2);
|
|
212
|
+
const width = Math.abs(x2 - x1);
|
|
213
|
+
const height = Math.abs(y2 - y1);
|
|
214
|
+
const borderColor = item.color ?? '#3b82f6';
|
|
215
|
+
const fillColor = item.fillColor ?? 'rgba(59, 130, 246, 0.15)';
|
|
216
|
+
ctx.fillStyle = fillColor;
|
|
217
|
+
ctx.fillRect(left, top, width, height);
|
|
218
|
+
ctx.strokeStyle = borderColor;
|
|
219
|
+
ctx.lineWidth = strokeWidth;
|
|
220
|
+
if (item.lineDash && item.lineDash.length > 0) {
|
|
221
|
+
ctx.setLineDash([...item.lineDash]);
|
|
222
|
+
}
|
|
223
|
+
ctx.strokeRect(left, top, width, height);
|
|
224
|
+
ctx.setLineDash([]);
|
|
225
|
+
ctx.shadowBlur = 0;
|
|
226
|
+
// Corner handles
|
|
227
|
+
const corners = [
|
|
228
|
+
[left, top, 'p1'],
|
|
229
|
+
[left + width, top, 'p2'],
|
|
230
|
+
[left, top + height, 'p1'],
|
|
231
|
+
[left + width, top + height, 'p2'],
|
|
232
|
+
];
|
|
233
|
+
for (const [cx, cy, hType] of corners) {
|
|
234
|
+
if (isSelected) {
|
|
235
|
+
drawControlHandle(ctx, cx, cy, activeHandle === hType, isLocked);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
ctx.beginPath();
|
|
239
|
+
ctx.arc(cx, cy, 3, 0, Math.PI * 2);
|
|
240
|
+
ctx.fillStyle = '#ffffff';
|
|
241
|
+
ctx.fill();
|
|
242
|
+
ctx.stroke();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (isSelected) {
|
|
246
|
+
drawSelectionBadge(ctx, left + width / 2, top, isLocked ? '🔒 Rectangle' : 'Rectangle', isLocked);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else if (item.type === 'fibonacci') {
|
|
250
|
+
const x1 = xScale.scale(item.p1.index);
|
|
251
|
+
const x2 = xScale.scale(item.p2.index);
|
|
252
|
+
const startX = Math.min(x1, x2);
|
|
253
|
+
const endX = Math.max(Math.max(x1, x2) + 50, chartRight);
|
|
254
|
+
const levels = item.levels ?? [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0];
|
|
255
|
+
const pLow = Math.min(item.p1.price, item.p2.price);
|
|
256
|
+
const pHigh = Math.max(item.p1.price, item.p2.price);
|
|
257
|
+
const pDiff = pHigh - pLow;
|
|
258
|
+
const isRetraceUp = item.p1.price < item.p2.price;
|
|
259
|
+
const levelColors = {
|
|
260
|
+
0: '#94a3b8',
|
|
261
|
+
0.236: '#ef4444',
|
|
262
|
+
0.382: '#f59e0b',
|
|
263
|
+
0.5: '#10b981',
|
|
264
|
+
0.618: '#eab308',
|
|
265
|
+
0.786: '#3b82f6',
|
|
266
|
+
1.0: '#94a3b8',
|
|
267
|
+
};
|
|
268
|
+
const y382 = yScale.scale(isRetraceUp ? pHigh - 0.382 * pDiff : pLow + 0.382 * pDiff);
|
|
269
|
+
const y618 = yScale.scale(isRetraceUp ? pHigh - 0.618 * pDiff : pLow + 0.618 * pDiff);
|
|
270
|
+
ctx.fillStyle = 'rgba(234, 179, 8, 0.12)';
|
|
271
|
+
ctx.fillRect(startX, Math.min(y382, y618), endX - startX, Math.abs(y618 - y382));
|
|
272
|
+
// Trend anchor diagonal
|
|
273
|
+
ctx.strokeStyle = 'rgba(148, 163, 184, 0.4)';
|
|
274
|
+
ctx.lineWidth = 1;
|
|
275
|
+
ctx.setLineDash([2, 2]);
|
|
276
|
+
ctx.beginPath();
|
|
277
|
+
ctx.moveTo(x1, yScale.scale(item.p1.price));
|
|
278
|
+
ctx.lineTo(x2, yScale.scale(item.p2.price));
|
|
279
|
+
ctx.stroke();
|
|
280
|
+
ctx.setLineDash([]);
|
|
281
|
+
ctx.shadowBlur = 0;
|
|
282
|
+
for (const ratio of levels) {
|
|
283
|
+
const price = isRetraceUp ? pHigh - ratio * pDiff : pLow + ratio * pDiff;
|
|
284
|
+
const y = yScale.scale(price);
|
|
285
|
+
const color = levelColors[ratio] ?? '#94a3b8';
|
|
286
|
+
ctx.strokeStyle = color;
|
|
287
|
+
ctx.lineWidth = ratio === 0.618 || ratio === 0.5 ? 1.5 : 1;
|
|
288
|
+
ctx.beginPath();
|
|
289
|
+
ctx.moveTo(startX, y);
|
|
290
|
+
ctx.lineTo(endX, y);
|
|
291
|
+
ctx.stroke();
|
|
292
|
+
const labelText = `${ratio} (₹${price.toFixed(1)})`;
|
|
293
|
+
ctx.font = '9px monospace';
|
|
294
|
+
ctx.fillStyle = color;
|
|
295
|
+
ctx.textAlign = 'left';
|
|
296
|
+
ctx.textBaseline = 'bottom';
|
|
297
|
+
ctx.fillText(labelText, startX + 4, y - 2);
|
|
298
|
+
}
|
|
299
|
+
if (isSelected) {
|
|
300
|
+
drawControlHandle(ctx, x1, yScale.scale(item.p1.price), activeHandle === 'p1', isLocked);
|
|
301
|
+
drawControlHandle(ctx, x2, yScale.scale(item.p2.price), activeHandle === 'p2', isLocked);
|
|
302
|
+
drawSelectionBadge(ctx, (x1 + x2) / 2, yScale.scale(item.p1.price), isLocked ? '🔒 Fibonacci' : 'Fibonacci', isLocked);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
else if (item.type === 'ray') {
|
|
306
|
+
const x1 = xScale.scale(item.p1.index);
|
|
307
|
+
const y1 = yScale.scale(item.p1.price);
|
|
308
|
+
const x2 = xScale.scale(item.p2.index);
|
|
309
|
+
const y2 = yScale.scale(item.p2.price);
|
|
310
|
+
const color = item.color ?? '#38bdf8';
|
|
311
|
+
const dx = x2 - x1;
|
|
312
|
+
const dy = y2 - y1;
|
|
313
|
+
let targetX = chartRight;
|
|
314
|
+
let targetY = y2;
|
|
315
|
+
if (Math.abs(dx) > 0.001) {
|
|
316
|
+
const slope = dy / dx;
|
|
317
|
+
targetX = dx > 0 ? chartRight : chartLeft;
|
|
318
|
+
targetY = y1 + slope * (targetX - x1);
|
|
319
|
+
}
|
|
320
|
+
ctx.strokeStyle = color;
|
|
321
|
+
ctx.lineWidth = strokeWidth;
|
|
322
|
+
if (item.lineDash && item.lineDash.length > 0) {
|
|
323
|
+
ctx.setLineDash([...item.lineDash]);
|
|
324
|
+
}
|
|
325
|
+
ctx.beginPath();
|
|
326
|
+
ctx.moveTo(x1, y1);
|
|
327
|
+
ctx.lineTo(targetX, targetY);
|
|
328
|
+
ctx.stroke();
|
|
329
|
+
ctx.setLineDash([]);
|
|
330
|
+
ctx.shadowBlur = 0;
|
|
331
|
+
if (isSelected) {
|
|
332
|
+
drawControlHandle(ctx, x1, y1, activeHandle === 'p1', isLocked);
|
|
333
|
+
drawControlHandle(ctx, x2, y2, activeHandle === 'p2', isLocked);
|
|
334
|
+
drawSelectionBadge(ctx, x1, y1, isLocked ? '🔒 Ray' : 'Ray', isLocked);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
ctx.fillStyle = '#ffffff';
|
|
338
|
+
ctx.beginPath();
|
|
339
|
+
ctx.arc(x1, y1, 4, 0, Math.PI * 2);
|
|
340
|
+
ctx.fill();
|
|
341
|
+
ctx.stroke();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else if (item.type === 'channel') {
|
|
345
|
+
const x1 = xScale.scale(item.p1.index);
|
|
346
|
+
const y1 = yScale.scale(item.p1.price);
|
|
347
|
+
const x2 = xScale.scale(item.p2.index);
|
|
348
|
+
const y2 = yScale.scale(item.p2.price);
|
|
349
|
+
const offsetPrice = item.channelWidthPrice ?? (Math.abs(item.p2.price - item.p1.price) * 0.4 || 15);
|
|
350
|
+
const y1Offset = yScale.scale(item.p1.price - offsetPrice);
|
|
351
|
+
const y2Offset = yScale.scale(item.p2.price - offsetPrice);
|
|
352
|
+
const color = item.color ?? '#818cf8';
|
|
353
|
+
const fillColor = item.fillColor ?? 'rgba(129, 140, 248, 0.15)';
|
|
354
|
+
ctx.fillStyle = fillColor;
|
|
355
|
+
ctx.beginPath();
|
|
356
|
+
ctx.moveTo(x1, y1);
|
|
357
|
+
ctx.lineTo(x2, y2);
|
|
358
|
+
ctx.lineTo(x2, y2Offset);
|
|
359
|
+
ctx.lineTo(x1, y1Offset);
|
|
360
|
+
ctx.closePath();
|
|
361
|
+
ctx.fill();
|
|
362
|
+
ctx.strokeStyle = color;
|
|
363
|
+
ctx.lineWidth = strokeWidth;
|
|
364
|
+
if (item.lineDash && item.lineDash.length > 0) {
|
|
365
|
+
ctx.setLineDash([...item.lineDash]);
|
|
366
|
+
}
|
|
367
|
+
ctx.beginPath();
|
|
368
|
+
ctx.moveTo(x1, y1);
|
|
369
|
+
ctx.lineTo(x2, y2);
|
|
370
|
+
ctx.moveTo(x1, y1Offset);
|
|
371
|
+
ctx.lineTo(x2, y2Offset);
|
|
372
|
+
ctx.stroke();
|
|
373
|
+
ctx.setLineDash([]);
|
|
374
|
+
ctx.shadowBlur = 0;
|
|
375
|
+
if (isSelected) {
|
|
376
|
+
drawControlHandle(ctx, x1, y1, activeHandle === 'p1', isLocked);
|
|
377
|
+
drawControlHandle(ctx, x2, y2, activeHandle === 'p2', isLocked);
|
|
378
|
+
drawControlHandle(ctx, x1, y1Offset, activeHandle === 'channelOffset', isLocked);
|
|
379
|
+
drawControlHandle(ctx, x2, y2Offset, activeHandle === 'channelOffset', isLocked);
|
|
380
|
+
drawSelectionBadge(ctx, (x1 + x2) / 2, (y1 + y2) / 2, isLocked ? '🔒 Channel' : 'Channel', isLocked);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
else if (item.type === 'text') {
|
|
384
|
+
const x = xScale.scale(item.p1.index);
|
|
385
|
+
const y = yScale.scale(item.p1.price);
|
|
386
|
+
const text = item.text || 'Note';
|
|
387
|
+
const color = item.color ?? '#f8fafc';
|
|
388
|
+
const bgColor = item.bgColor ?? '#1e293b';
|
|
389
|
+
ctx.font = '11px sans-serif';
|
|
390
|
+
const textWidth = ctx.measureText(text).width;
|
|
391
|
+
const padX = 6;
|
|
392
|
+
const padY = 4;
|
|
393
|
+
const boxW = textWidth + padX * 2;
|
|
394
|
+
const boxH = 20;
|
|
395
|
+
ctx.fillStyle = bgColor;
|
|
396
|
+
ctx.strokeStyle = isSelected ? (isLocked ? '#f59e0b' : '#3b82f6') : '#475569';
|
|
397
|
+
ctx.lineWidth = isSelected ? 2 : 1;
|
|
398
|
+
ctx.fillRect(x - boxW / 2, y - boxH - 6, boxW, boxH);
|
|
399
|
+
ctx.strokeRect(x - boxW / 2, y - boxH - 6, boxW, boxH);
|
|
400
|
+
ctx.fillStyle = color;
|
|
401
|
+
ctx.textAlign = 'center';
|
|
402
|
+
ctx.textBaseline = 'middle';
|
|
403
|
+
ctx.fillText(text, x, y - boxH / 2 - 6);
|
|
404
|
+
// Anchor triangle
|
|
405
|
+
ctx.fillStyle = bgColor;
|
|
406
|
+
ctx.beginPath();
|
|
407
|
+
ctx.moveTo(x - 4, y - 6);
|
|
408
|
+
ctx.lineTo(x + 4, y - 6);
|
|
409
|
+
ctx.lineTo(x, y - 1);
|
|
410
|
+
ctx.closePath();
|
|
411
|
+
ctx.fill();
|
|
412
|
+
ctx.shadowBlur = 0;
|
|
413
|
+
if (isSelected) {
|
|
414
|
+
drawControlHandle(ctx, x, y - 1, activeHandle === 'p1', isLocked);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else if (item.type === 'arrow') {
|
|
418
|
+
const x = xScale.scale(item.p1.index);
|
|
419
|
+
const y = yScale.scale(item.p1.price);
|
|
420
|
+
const dir = item.direction ?? 'up';
|
|
421
|
+
const color = item.color ?? (dir === 'up' ? '#10b981' : '#ef4444');
|
|
422
|
+
ctx.fillStyle = color;
|
|
423
|
+
ctx.strokeStyle = color;
|
|
424
|
+
ctx.lineWidth = strokeWidth;
|
|
425
|
+
ctx.beginPath();
|
|
426
|
+
if (dir === 'up') {
|
|
427
|
+
ctx.moveTo(x, y + 18);
|
|
428
|
+
ctx.lineTo(x, y + 4);
|
|
429
|
+
ctx.stroke();
|
|
430
|
+
ctx.beginPath();
|
|
431
|
+
ctx.moveTo(x - 5, y + 8);
|
|
432
|
+
ctx.lineTo(x + 5, y + 8);
|
|
433
|
+
ctx.lineTo(x, y + 1);
|
|
434
|
+
ctx.closePath();
|
|
435
|
+
ctx.fill();
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
ctx.moveTo(x, y - 18);
|
|
439
|
+
ctx.lineTo(x, y - 4);
|
|
440
|
+
ctx.stroke();
|
|
441
|
+
ctx.beginPath();
|
|
442
|
+
ctx.moveTo(x - 5, y - 8);
|
|
443
|
+
ctx.lineTo(x + 5, y - 8);
|
|
444
|
+
ctx.lineTo(x, y - 1);
|
|
445
|
+
ctx.closePath();
|
|
446
|
+
ctx.fill();
|
|
447
|
+
}
|
|
448
|
+
ctx.shadowBlur = 0;
|
|
449
|
+
if (item.label) {
|
|
450
|
+
ctx.font = 'bold 10px sans-serif';
|
|
451
|
+
ctx.textAlign = 'center';
|
|
452
|
+
ctx.fillText(item.label, x, dir === 'up' ? y + 28 : y - 22);
|
|
453
|
+
}
|
|
454
|
+
if (isSelected) {
|
|
455
|
+
drawControlHandle(ctx, x, y, activeHandle === 'p1', isLocked);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
else if (item.type === 'freehand') {
|
|
459
|
+
const pts = item.points;
|
|
460
|
+
if (pts.length > 0) {
|
|
461
|
+
const color = item.color ?? '#ec4899';
|
|
462
|
+
ctx.strokeStyle = color;
|
|
463
|
+
ctx.lineWidth = strokeWidth;
|
|
464
|
+
if (item.lineDash && item.lineDash.length > 0) {
|
|
465
|
+
ctx.setLineDash([...item.lineDash]);
|
|
466
|
+
}
|
|
467
|
+
ctx.beginPath();
|
|
468
|
+
const startX = xScale.scale(pts[0].index);
|
|
469
|
+
const startY = yScale.scale(pts[0].price);
|
|
470
|
+
ctx.moveTo(startX, startY);
|
|
471
|
+
for (let i = 1; i < pts.length; i++) {
|
|
472
|
+
ctx.lineTo(xScale.scale(pts[i].index), yScale.scale(pts[i].price));
|
|
473
|
+
}
|
|
474
|
+
ctx.stroke();
|
|
475
|
+
ctx.setLineDash([]);
|
|
476
|
+
ctx.shadowBlur = 0;
|
|
477
|
+
if (isSelected) {
|
|
478
|
+
drawControlHandle(ctx, startX, startY, activeHandle === 'p1', isLocked);
|
|
479
|
+
const endPt = pts[pts.length - 1];
|
|
480
|
+
drawControlHandle(ctx, xScale.scale(endPt.index), yScale.scale(endPt.price), activeHandle === 'p2', isLocked);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
ctx.restore();
|
|
485
|
+
}
|
|
486
|
+
ctx.restore();
|
|
487
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file packages/chart-renderer-canvas/src/renderers/grid-renderer.ts
|
|
3
|
+
* Crisp gridlines, axes, and tick text labels.
|
|
4
|
+
*/
|
|
5
|
+
import type { ChartTheme, Dimension, LineStyle, Padding, Scale } from '@tranding-custom-chart/core';
|
|
6
|
+
export interface GridRenderOptions {
|
|
7
|
+
readonly ctx: CanvasRenderingContext2D;
|
|
8
|
+
readonly bounds: Dimension;
|
|
9
|
+
readonly padding: Padding;
|
|
10
|
+
readonly xScale: Scale<number>;
|
|
11
|
+
readonly yScale: Scale<number>;
|
|
12
|
+
readonly theme: ChartTheme;
|
|
13
|
+
readonly dpr: number;
|
|
14
|
+
readonly xTickFormatter?: ((val: number) => string) | undefined;
|
|
15
|
+
readonly yTickFormatter?: ((val: number) => string) | undefined;
|
|
16
|
+
readonly showXGrid?: boolean | undefined;
|
|
17
|
+
readonly showYGrid?: boolean | undefined;
|
|
18
|
+
readonly gridStyle?: LineStyle | undefined;
|
|
19
|
+
/** Y-Axis label position: 'left', 'right' (default), or 'both' for dual axes */
|
|
20
|
+
readonly yAxisPosition?: 'left' | 'right' | 'both' | undefined;
|
|
21
|
+
/** Secondary Y-Scale for dual-axis mixed charts */
|
|
22
|
+
readonly secondaryYScale?: Scale<number> | undefined;
|
|
23
|
+
readonly secondaryYTickFormatter?: ((val: number) => string) | undefined;
|
|
24
|
+
}
|
|
25
|
+
export declare function renderGridAndAxes(options: GridRenderOptions): void;
|
|
26
|
+
//# sourceMappingURL=grid-renderer.d.ts.map
|