@funnest/charts 0.6.9 → 0.6.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/chart.d.ts CHANGED
@@ -6,6 +6,7 @@ import { type Drawing, type DrawingDefinition, type DrawingType } from './object
6
6
  import { type AlertLineOptions, type ExecutionMarker, type LineHandle, type OrderLineOptions, type PositionLineOptions, type TradingLabelOptions } from './objects/trading';
7
7
  import { type ChartSettings, type ChartState } from './settings';
8
8
  import { type TradingAdapter } from './objects/trading-sync';
9
+ import type { ChartOverlay } from './objects/types';
9
10
  import { type ChartType, type CrosshairInfo, type PaneId } from './render/renderer';
10
11
  import { type ThemeTokens } from './theme';
11
12
  import type { Bar, ConnectionState, Datafeed, Quote, Resolution, SymbolInfo, TickSize } from './types';
@@ -166,6 +167,7 @@ export declare class Chart {
166
167
  private readonly interaction;
167
168
  private readonly cleanups;
168
169
  private readonly drawings;
170
+ private readonly overlays;
169
171
  private readonly trading;
170
172
  private readonly tradingSync;
171
173
  private tooltipKey;
@@ -411,6 +413,14 @@ export declare class Chart {
411
413
  addOrderLine(options: OrderLineOptions): LineHandle<OrderLineOptions>;
412
414
  /** Price alert line (dotted, gray, "알림" label). Pass `onMove` to make it draggable and `onCancel` for an ×. */
413
415
  addAlertLine(options: AlertLineOptions): LineHandle<AlertLineOptions>;
416
+ /**
417
+ * Paints `overlay` on the price pane every frame (see ChartOverlay). `redraw()` asks for a
418
+ * new frame after its data changed; `remove()` takes it off.
419
+ */
420
+ addOverlay(overlay: ChartOverlay): {
421
+ redraw(): void;
422
+ remove(): void;
423
+ };
414
424
  /** Fill markers (▲ buy under the bar, ▼ sell above; ⚡ at the price for liquidations). Replaces the previous set. */
415
425
  setExecutions(markers: readonly ExecutionMarker[]): void;
416
426
  /** What position/order/alert line labels show and where (direction, price, size, P&L, ×, alignment, line width). */
package/dist/index.d.ts CHANGED
@@ -13,9 +13,9 @@ export type { FormatPriceOptions } from './price';
13
13
  export { decimalsOf, formatCompact, formatPrice, krxTickSize, resolveTickSize, roundToTick } from './price';
14
14
  export { isIntradayResolution, resolutionToMs } from './resolution';
15
15
  export type { ChartType } from './render/renderer';
16
- export type { BuiltinDrawingType, Drawing, DrawingDefinition, DrawingGeometry, DrawingPoint, DrawingRenderContext, DrawingType, } from './objects/drawings';
16
+ export type { BuiltinDrawingType, Drawing, DrawingDefinition, DrawingGeometry, DrawingPoint, DrawingRenderContext, DrawingStyle, DrawingColorSlot, DrawingType, } from './objects/drawings';
17
17
  export { BUILTIN_DRAWINGS, distToSegment, FIB_LEVELS } from './objects/drawings';
18
- export type { PaneCoords } from './objects/types';
18
+ export type { ChartOverlay, ObjectRenderContext, PaneCoords } from './objects/types';
19
19
  export type { Painter } from './render/painter';
20
20
  export type { Palette } from './render/renderer';
21
21
  export { snapToOhlc } from './objects/magnet';
package/dist/index.js CHANGED
@@ -1221,6 +1221,7 @@ var IndicatorManager = class {
1221
1221
  };
1222
1222
 
1223
1223
  // src/objects/drawings.ts
1224
+ var DASHES = { solid: [], dashed: [6, 4], dotted: [1.5, 3] };
1224
1225
  var FIB_LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
1225
1226
  var FIB_COLORS = ["#8B95A1", "#F04452", "#FF9F0A", "#12B886", "#3182F6", "#7048E8", "#8B95A1"];
1226
1227
  var HANDLE_RADIUS = 5;
@@ -1240,7 +1241,7 @@ var BUILTIN_DRAWINGS = [
1240
1241
  group: "line",
1241
1242
  clicks: 2,
1242
1243
  distance: (g, x, y) => distToSegment(x, y, ...p0(g), ...p1(g)),
1243
- render: (_d, { painter, color, ...g }) => painter.line(...p0(g), ...p1(g), color, 2)
1244
+ render: (_d, { painter, color, ...g }) => painter.line(...p0(g), ...p1(g), color, g.width(2), g.dash())
1244
1245
  },
1245
1246
  {
1246
1247
  type: "horizontal",
@@ -1252,7 +1253,9 @@ var BUILTIN_DRAWINGS = [
1252
1253
  distance: (g, _x, y) => Math.abs(y - p0(g)[1]),
1253
1254
  render: (d, { painter, color, coords, badge, ...g }) => {
1254
1255
  const y = p0(g)[1];
1255
- painter.hLine(y, 0, coords.plotWidth, color);
1256
+ const dash = g.dash();
1257
+ if (g.width(1) === 1 && !dash.length) painter.hLine(y, 0, coords.plotWidth, color);
1258
+ else painter.line(0, y, coords.plotWidth, y, color, g.width(1), dash);
1256
1259
  badge(y, coords.formatPrice(d.points[0].price), color);
1257
1260
  }
1258
1261
  },
@@ -1262,6 +1265,7 @@ var BUILTIN_DRAWINGS = [
1262
1265
  group: "fib",
1263
1266
  section: { ko: "\uD53C\uBCF4\uB098\uCE58", en: "Fibonacci" },
1264
1267
  clicks: 2,
1268
+ colors: FIB_LEVELS.map((level, i) => ({ key: String(level), name: { ko: String(level), en: String(level) }, color: FIB_COLORS[i] })),
1265
1269
  distance: (g, x, y) => {
1266
1270
  const [x1, y1] = p0(g);
1267
1271
  const [x2, y2] = p1(g);
@@ -1282,7 +1286,7 @@ var BUILTIN_DRAWINGS = [
1282
1286
  FIB_LEVELS.forEach((level, i) => {
1283
1287
  const price = b + (a - b) * level;
1284
1288
  const y = c.priceToY(price);
1285
- const levelColor = d.color ?? FIB_COLORS[i];
1289
+ const levelColor = d.style?.colors?.[String(level)] ?? d.color ?? FIB_COLORS[i];
1286
1290
  if (i > 0) {
1287
1291
  const prevY = c.priceToY(b + (a - b) * FIB_LEVELS[i - 1]);
1288
1292
  ctx.save();
@@ -1314,6 +1318,7 @@ var DrawingManager = class {
1314
1318
  tool = null;
1315
1319
  selectedId = null;
1316
1320
  draft = null;
1321
+ freehandLast = null;
1317
1322
  drag = null;
1318
1323
  get selected() {
1319
1324
  return this.drawings.find((d) => d.id === this.selectedId) ?? null;
@@ -1368,6 +1373,13 @@ var DrawingManager = class {
1368
1373
  const def = this.tool ? this.registry.get(this.tool) : void 0;
1369
1374
  if (!def) return;
1370
1375
  const point = c.pointAt(x, y);
1376
+ if (def.freehand) {
1377
+ if (this.draft) return this.finish();
1378
+ this.draft = { drawing: { id: newId("d"), type: def.type, points: [point] }, placed: [point] };
1379
+ this.freehandLast = [x, y];
1380
+ this.onChange("preview");
1381
+ return;
1382
+ }
1371
1383
  if (!this.draft) {
1372
1384
  const placed = [point];
1373
1385
  this.draft = { drawing: { id: newId("d"), type: def.type, points: this.expand(def, placed, c) }, placed };
@@ -1384,11 +1396,25 @@ var DrawingManager = class {
1384
1396
  const draft = this.draft;
1385
1397
  const def = draft && this.registry.get(draft.drawing.type);
1386
1398
  if (!draft || !def) return;
1399
+ if (def.freehand) {
1400
+ const last = this.freehandLast;
1401
+ if (last && Math.hypot(x - last[0], y - last[1]) < 3) return;
1402
+ if (draft.drawing.points.length >= 4e3) return;
1403
+ this.freehandLast = [x, y];
1404
+ draft.drawing.points.push(c.pointAt(x, y));
1405
+ this.onChange("preview");
1406
+ return;
1407
+ }
1387
1408
  draft.drawing.points = this.expand(def, [...draft.placed, c.pointAt(x, y)], c);
1388
1409
  this.onChange("preview");
1389
1410
  }
1390
1411
  /** Pointer released: a drag since the first point places the next one (drag-to-draw). */
1391
1412
  releaseAt(c, x, y, moved) {
1413
+ const def = this.draft && this.registry.get(this.draft.drawing.type);
1414
+ if (def?.freehand) {
1415
+ if (moved && this.draft.drawing.points.length > 1) this.finish();
1416
+ return;
1417
+ }
1392
1418
  if (this.draft && moved) this.createAt(c, x, y);
1393
1419
  }
1394
1420
  finish() {
@@ -1407,7 +1433,7 @@ var DrawingManager = class {
1407
1433
  handles(c, d) {
1408
1434
  const def = this.registry.get(d.type);
1409
1435
  const g = this.geometry(c, d);
1410
- return def?.handles ? def.handles(g) : g.points;
1436
+ return def?.handles ? def.handles(g) : def?.freehand ? [] : g.points;
1411
1437
  }
1412
1438
  hitTest(c, x, y, tolerance) {
1413
1439
  const selected = this.selected;
@@ -1426,6 +1452,7 @@ var DrawingManager = class {
1426
1452
  const d = this.drawings.find((x2) => x2.id === id);
1427
1453
  if (!d) return;
1428
1454
  this.select(id);
1455
+ if (d.style?.locked) return;
1429
1456
  this.drag = { id, part, x, y, origin: this.geometry(c, d).points };
1430
1457
  }
1431
1458
  dragTo(c, x, y) {
@@ -1448,21 +1475,33 @@ var DrawingManager = class {
1448
1475
  this.drag = null;
1449
1476
  }
1450
1477
  // ── Rendering ─────────────────────────────────────────────────────
1451
- render(rc, badges) {
1478
+ render(rc, badges, bars = [], barIndexAt = () => -1) {
1452
1479
  const all = this.draft ? [...this.drawings, this.draft.drawing] : this.drawings;
1453
1480
  const c = rc.coords;
1454
1481
  for (const d of all) {
1455
1482
  const def = this.registry.get(d.type);
1456
1483
  if (!def) continue;
1457
1484
  const color = d.color ?? rc.palette.accent;
1458
- def.render(d, {
1459
- ...this.geometry(c, d),
1460
- painter: rc.painter,
1461
- palette: rc.palette,
1462
- color,
1463
- badge: (y, text, bg, fg = "#FFFFFF") => badges.push({ y, text, bg, fg })
1464
- });
1465
- if (d.id === this.selectedId || d === this.draft?.drawing) {
1485
+ const style = d.style ?? {};
1486
+ rc.painter.ctx.save();
1487
+ try {
1488
+ def.render(d, {
1489
+ ...this.geometry(c, d),
1490
+ painter: rc.painter,
1491
+ palette: rc.palette,
1492
+ color,
1493
+ style,
1494
+ colorOf: (key) => style.colors?.[key] ?? d.color ?? def.colors?.find((c2) => c2.key === key)?.color ?? color,
1495
+ width: (fallback) => Math.min(8, Math.max(0.5, style.width ?? fallback)),
1496
+ dash: (fallback = []) => style.dash ? DASHES[style.dash] : [...fallback],
1497
+ bars,
1498
+ barIndexAt,
1499
+ badge: (y, text, bg, fg = "#FFFFFF") => badges.push({ y, text, bg, fg })
1500
+ });
1501
+ } finally {
1502
+ rc.painter.ctx.restore();
1503
+ }
1504
+ if (d.id === this.selectedId && !style.locked || d === this.draft?.drawing) {
1466
1505
  for (const [px, py] of this.handles(c, d)) rc.painter.circle(px, py, HANDLE_RADIUS, rc.palette.background, color);
1467
1506
  }
1468
1507
  }
@@ -2409,8 +2448,17 @@ function drawMain(layer, m) {
2409
2448
  ctx.clip();
2410
2449
  const o = m.objects;
2411
2450
  const rc = { painter: p, coords: o.coords, palette: pal };
2451
+ for (const overlay of o.overlays) {
2452
+ ctx.save();
2453
+ try {
2454
+ overlay.render(rc);
2455
+ } catch (error) {
2456
+ console.error("[fnc] overlay", error);
2457
+ }
2458
+ ctx.restore();
2459
+ }
2412
2460
  o.trading.render(rc, m.bars, o.barIndexAt, (i) => m.timeScale.indexToX(i), o.tradingLabels, badges);
2413
- o.drawings.render(rc, badges);
2461
+ o.drawings.render(rc, badges, m.bars, o.barIndexAt);
2414
2462
  ctx.restore();
2415
2463
  const last = f.settings.lastPriceLine ? m.bars[m.bars.length - 1] : void 0;
2416
2464
  const lastColor = last && last.close >= last.open ? pal.up : pal.down;
@@ -2747,6 +2795,7 @@ var Chart = class {
2747
2795
  interaction;
2748
2796
  cleanups = [];
2749
2797
  drawings;
2798
+ overlays = [];
2750
2799
  trading;
2751
2800
  tradingSync;
2752
2801
  tooltipKey = null;
@@ -3723,6 +3772,22 @@ var Chart = class {
3723
3772
  addAlertLine(options) {
3724
3773
  return this.trading.addAlert(options);
3725
3774
  }
3775
+ /**
3776
+ * Paints `overlay` on the price pane every frame (see ChartOverlay). `redraw()` asks for a
3777
+ * new frame after its data changed; `remove()` takes it off.
3778
+ */
3779
+ addOverlay(overlay) {
3780
+ this.overlays.push(overlay);
3781
+ this.invalidate();
3782
+ return {
3783
+ redraw: () => this.invalidate(),
3784
+ remove: () => {
3785
+ const i = this.overlays.indexOf(overlay);
3786
+ if (i >= 0) this.overlays.splice(i, 1);
3787
+ this.invalidate();
3788
+ }
3789
+ };
3790
+ }
3726
3791
  /** Fill markers (▲ buy under the bar, ▼ sell above; ⚡ at the price for liquidations). Replaces the previous set. */
3727
3792
  setExecutions(markers) {
3728
3793
  this.trading.setExecutions(markers);
@@ -3829,6 +3894,7 @@ var Chart = class {
3829
3894
  replaySelecting: this.replaySelecting,
3830
3895
  objects: {
3831
3896
  coords: this.paneCoords(),
3897
+ overlays: this.overlays,
3832
3898
  drawings: this.drawings,
3833
3899
  trading: this.trading,
3834
3900
  tradingLabels: TRADING_LABELS[this.locale],
@@ -1,3 +1,4 @@
1
+ import type { Bar } from '../types';
1
2
  import type { Locale } from '../model/time-format';
2
3
  import type { Painter } from '../render/painter';
3
4
  import type { Palette } from '../render/renderer';
@@ -11,11 +12,37 @@ export interface DrawingPoint {
11
12
  time: number;
12
13
  price: number;
13
14
  }
15
+ /** How a drawing looks beyond its color (all optional; tools read them through the render context). */
16
+ export interface DrawingStyle {
17
+ /** Main line width in CSS px (1–4). */
18
+ width?: number;
19
+ /** Main line pattern. */
20
+ dash?: 'solid' | 'dashed' | 'dotted';
21
+ /** Text tools (and notes on others): what it says. */
22
+ text?: string;
23
+ /** Text size in CSS px. */
24
+ fontSize?: number;
25
+ /** Can't be dragged or edited until unlocked (still selectable). */
26
+ locked?: boolean;
27
+ /** Colors of the tool's parts (`DrawingDefinition.colors` keys), where set apart from the rest. */
28
+ colors?: Record<string, string>;
29
+ }
30
+ /**
31
+ * A part of a tool with its own color (a Fibonacci level, a position's target zone…). Its color
32
+ * is `style.colors[key]`, else the drawing's color (picking one recolors every part), else `color`.
33
+ */
34
+ export interface DrawingColorSlot {
35
+ key: string;
36
+ name: Record<Locale, string>;
37
+ /** Default color. */
38
+ color: string;
39
+ }
14
40
  export interface Drawing {
15
41
  id: string;
16
42
  type: DrawingType;
17
43
  points: DrawingPoint[];
18
44
  color?: string;
45
+ style?: DrawingStyle;
19
46
  }
20
47
  /** What a drawing definition gets to measure and paint itself. Pixels are relative to the price pane. */
21
48
  export interface DrawingGeometry {
@@ -28,6 +55,18 @@ export interface DrawingRenderContext extends DrawingGeometry {
28
55
  palette: Palette;
29
56
  /** `drawing.color`, or the theme accent. */
30
57
  color: string;
58
+ /** `drawing.style` (empty object when none). */
59
+ style: DrawingStyle;
60
+ /** A part's color (see `DrawingDefinition.colors`): its own, else the drawing's, else the default. */
61
+ colorOf(key: string): string;
62
+ /** The main line width: the drawing's style, else the tool's default. */
63
+ width(fallback: number): number;
64
+ /** The main line dash pattern for canvas: the drawing's style, else the tool's default. */
65
+ dash(fallback?: readonly number[]): number[];
66
+ /** The chart's bars (oldest first), for tools that measure them (VWAP, volume profile…). */
67
+ bars: readonly Bar[];
68
+ /** Index of the last bar at or before `time` (-1 before the first). */
69
+ barIndexAt(time: number): number;
31
70
  /** Adds a price-axis badge (drawn above the axis labels, clamped to the pane). */
32
71
  badge(y: number, text: string, bg: string, fg?: string): void;
33
72
  }
@@ -61,6 +100,15 @@ export interface DrawingDefinition {
61
100
  constrain?(points: DrawingPoint[], index: number): DrawingPoint[];
62
101
  /** Drag handle positions; dragging handle i moves point i. Default: the points. */
63
102
  handles?(geometry: DrawingGeometry): [number, number][];
103
+ /**
104
+ * Drawn by dragging: every pointer move adds a point (a brush); releasing (or clicking again)
105
+ * finishes it. `clicks` is ignored.
106
+ */
107
+ freehand?: boolean;
108
+ /** Has text (`style.text`): the widget offers a text field for it when selected. */
109
+ text?: boolean;
110
+ /** Parts with their own colors: the widget lets each be recolored (read them with `rc.colorOf`). */
111
+ colors?: DrawingColorSlot[];
64
112
  /** Distance in px from (x, y) to the shape, for hit testing. */
65
113
  distance(geometry: DrawingGeometry, x: number, y: number): number;
66
114
  render(drawing: Drawing, rc: DrawingRenderContext): void;
@@ -84,6 +132,7 @@ export declare class DrawingManager {
84
132
  tool: DrawingType | null;
85
133
  selectedId: string | null;
86
134
  private draft;
135
+ private freehandLast;
87
136
  private drag;
88
137
  constructor(onChange: (change: DrawingsChange) => void, registry?: Map<string, DrawingDefinition>);
89
138
  get selected(): Drawing | null;
@@ -109,5 +158,5 @@ export declare class DrawingManager {
109
158
  beginDrag(c: PaneCoords, id: string, part: 'body' | number, x: number, y: number): void;
110
159
  dragTo(c: PaneCoords, x: number, y: number): void;
111
160
  endDrag(moved: boolean): void;
112
- render(rc: ObjectRenderContext, badges: AxisBadge[]): void;
161
+ render(rc: ObjectRenderContext, badges: AxisBadge[], bars?: readonly Bar[], barIndexAt?: (time: number) => number): void;
113
162
  }
@@ -22,6 +22,14 @@ export interface ObjectRenderContext {
22
22
  coords: PaneCoords;
23
23
  palette: Palette;
24
24
  }
25
+ /**
26
+ * Something an app paints on the price pane every frame (a liquidation map, volume profile…),
27
+ * clipped to the pane, above the candles and indicators, under trading lines and drawings.
28
+ * Added with chart.addOverlay(); it reads its own data and the pane's coordinates.
29
+ */
30
+ export interface ChartOverlay {
31
+ render(rc: ObjectRenderContext): void;
32
+ }
25
33
  export type ObjectHit = {
26
34
  kind: 'drawing';
27
35
  id: string;
@@ -5,7 +5,7 @@ import type { Bar } from '../types';
5
5
  import type { CanvasLayer } from './canvas-layer';
6
6
  import type { DrawingManager } from '../objects/drawings';
7
7
  import type { TradingLabels, TradingLayer } from '../objects/trading';
8
- import type { PaneCoords } from '../objects/types';
8
+ import type { ChartOverlay, PaneCoords } from '../objects/types';
9
9
  import type { IndicatorOutput } from '../indicators/definitions';
10
10
  import type { ChartSettings } from '../settings';
11
11
  export type ChartType = 'candles' | 'line' | 'area';
@@ -95,6 +95,7 @@ export interface RenderModel {
95
95
  replaySelecting?: boolean;
96
96
  objects: {
97
97
  coords: PaneCoords;
98
+ overlays: readonly ChartOverlay[];
98
99
  drawings: DrawingManager;
99
100
  trading: TradingLayer;
100
101
  tradingLabels: TradingLabels;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnest/charts",
3
- "version": "0.6.9",
3
+ "version": "0.6.12",
4
4
  "description": "Canvas charting engine for financial charts",
5
5
  "keywords": [
6
6
  "chart",