@funnest/charts 0.6.8 → 0.6.11

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, 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
  },
@@ -1260,6 +1263,7 @@ var BUILTIN_DRAWINGS = [
1260
1263
  type: "fibonacci",
1261
1264
  name: { ko: "\uD53C\uBCF4\uB098\uCE58 \uB418\uB3CC\uB9BC", en: "Fib retracement" },
1262
1265
  group: "fib",
1266
+ section: { ko: "\uD53C\uBCF4\uB098\uCE58", en: "Fibonacci" },
1263
1267
  clicks: 2,
1264
1268
  distance: (g, x, y) => {
1265
1269
  const [x1, y1] = p0(g);
@@ -1313,6 +1317,7 @@ var DrawingManager = class {
1313
1317
  tool = null;
1314
1318
  selectedId = null;
1315
1319
  draft = null;
1320
+ freehandLast = null;
1316
1321
  drag = null;
1317
1322
  get selected() {
1318
1323
  return this.drawings.find((d) => d.id === this.selectedId) ?? null;
@@ -1367,6 +1372,13 @@ var DrawingManager = class {
1367
1372
  const def = this.tool ? this.registry.get(this.tool) : void 0;
1368
1373
  if (!def) return;
1369
1374
  const point = c.pointAt(x, y);
1375
+ if (def.freehand) {
1376
+ if (this.draft) return this.finish();
1377
+ this.draft = { drawing: { id: newId("d"), type: def.type, points: [point] }, placed: [point] };
1378
+ this.freehandLast = [x, y];
1379
+ this.onChange("preview");
1380
+ return;
1381
+ }
1370
1382
  if (!this.draft) {
1371
1383
  const placed = [point];
1372
1384
  this.draft = { drawing: { id: newId("d"), type: def.type, points: this.expand(def, placed, c) }, placed };
@@ -1383,11 +1395,25 @@ var DrawingManager = class {
1383
1395
  const draft = this.draft;
1384
1396
  const def = draft && this.registry.get(draft.drawing.type);
1385
1397
  if (!draft || !def) return;
1398
+ if (def.freehand) {
1399
+ const last = this.freehandLast;
1400
+ if (last && Math.hypot(x - last[0], y - last[1]) < 3) return;
1401
+ if (draft.drawing.points.length >= 4e3) return;
1402
+ this.freehandLast = [x, y];
1403
+ draft.drawing.points.push(c.pointAt(x, y));
1404
+ this.onChange("preview");
1405
+ return;
1406
+ }
1386
1407
  draft.drawing.points = this.expand(def, [...draft.placed, c.pointAt(x, y)], c);
1387
1408
  this.onChange("preview");
1388
1409
  }
1389
1410
  /** Pointer released: a drag since the first point places the next one (drag-to-draw). */
1390
1411
  releaseAt(c, x, y, moved) {
1412
+ const def = this.draft && this.registry.get(this.draft.drawing.type);
1413
+ if (def?.freehand) {
1414
+ if (moved && this.draft.drawing.points.length > 1) this.finish();
1415
+ return;
1416
+ }
1391
1417
  if (this.draft && moved) this.createAt(c, x, y);
1392
1418
  }
1393
1419
  finish() {
@@ -1406,7 +1432,7 @@ var DrawingManager = class {
1406
1432
  handles(c, d) {
1407
1433
  const def = this.registry.get(d.type);
1408
1434
  const g = this.geometry(c, d);
1409
- return def?.handles ? def.handles(g) : g.points;
1435
+ return def?.handles ? def.handles(g) : def?.freehand ? [] : g.points;
1410
1436
  }
1411
1437
  hitTest(c, x, y, tolerance) {
1412
1438
  const selected = this.selected;
@@ -1425,6 +1451,7 @@ var DrawingManager = class {
1425
1451
  const d = this.drawings.find((x2) => x2.id === id);
1426
1452
  if (!d) return;
1427
1453
  this.select(id);
1454
+ if (d.style?.locked) return;
1428
1455
  this.drag = { id, part, x, y, origin: this.geometry(c, d).points };
1429
1456
  }
1430
1457
  dragTo(c, x, y) {
@@ -1447,21 +1474,32 @@ var DrawingManager = class {
1447
1474
  this.drag = null;
1448
1475
  }
1449
1476
  // ── Rendering ─────────────────────────────────────────────────────
1450
- render(rc, badges) {
1477
+ render(rc, badges, bars = [], barIndexAt = () => -1) {
1451
1478
  const all = this.draft ? [...this.drawings, this.draft.drawing] : this.drawings;
1452
1479
  const c = rc.coords;
1453
1480
  for (const d of all) {
1454
1481
  const def = this.registry.get(d.type);
1455
1482
  if (!def) continue;
1456
1483
  const color = d.color ?? rc.palette.accent;
1457
- def.render(d, {
1458
- ...this.geometry(c, d),
1459
- painter: rc.painter,
1460
- palette: rc.palette,
1461
- color,
1462
- badge: (y, text, bg, fg = "#FFFFFF") => badges.push({ y, text, bg, fg })
1463
- });
1464
- if (d.id === this.selectedId || d === this.draft?.drawing) {
1484
+ const style = d.style ?? {};
1485
+ rc.painter.ctx.save();
1486
+ try {
1487
+ def.render(d, {
1488
+ ...this.geometry(c, d),
1489
+ painter: rc.painter,
1490
+ palette: rc.palette,
1491
+ color,
1492
+ style,
1493
+ width: (fallback) => Math.min(8, Math.max(0.5, style.width ?? fallback)),
1494
+ dash: (fallback = []) => style.dash ? DASHES[style.dash] : [...fallback],
1495
+ bars,
1496
+ barIndexAt,
1497
+ badge: (y, text, bg, fg = "#FFFFFF") => badges.push({ y, text, bg, fg })
1498
+ });
1499
+ } finally {
1500
+ rc.painter.ctx.restore();
1501
+ }
1502
+ if (d.id === this.selectedId && !style.locked || d === this.draft?.drawing) {
1465
1503
  for (const [px, py] of this.handles(c, d)) rc.painter.circle(px, py, HANDLE_RADIUS, rc.palette.background, color);
1466
1504
  }
1467
1505
  }
@@ -2408,8 +2446,17 @@ function drawMain(layer, m) {
2408
2446
  ctx.clip();
2409
2447
  const o = m.objects;
2410
2448
  const rc = { painter: p, coords: o.coords, palette: pal };
2449
+ for (const overlay of o.overlays) {
2450
+ ctx.save();
2451
+ try {
2452
+ overlay.render(rc);
2453
+ } catch (error) {
2454
+ console.error("[fnc] overlay", error);
2455
+ }
2456
+ ctx.restore();
2457
+ }
2411
2458
  o.trading.render(rc, m.bars, o.barIndexAt, (i) => m.timeScale.indexToX(i), o.tradingLabels, badges);
2412
- o.drawings.render(rc, badges);
2459
+ o.drawings.render(rc, badges, m.bars, o.barIndexAt);
2413
2460
  ctx.restore();
2414
2461
  const last = f.settings.lastPriceLine ? m.bars[m.bars.length - 1] : void 0;
2415
2462
  const lastColor = last && last.close >= last.open ? pal.up : pal.down;
@@ -2746,6 +2793,7 @@ var Chart = class {
2746
2793
  interaction;
2747
2794
  cleanups = [];
2748
2795
  drawings;
2796
+ overlays = [];
2749
2797
  trading;
2750
2798
  tradingSync;
2751
2799
  tooltipKey = null;
@@ -3722,6 +3770,22 @@ var Chart = class {
3722
3770
  addAlertLine(options) {
3723
3771
  return this.trading.addAlert(options);
3724
3772
  }
3773
+ /**
3774
+ * Paints `overlay` on the price pane every frame (see ChartOverlay). `redraw()` asks for a
3775
+ * new frame after its data changed; `remove()` takes it off.
3776
+ */
3777
+ addOverlay(overlay) {
3778
+ this.overlays.push(overlay);
3779
+ this.invalidate();
3780
+ return {
3781
+ redraw: () => this.invalidate(),
3782
+ remove: () => {
3783
+ const i = this.overlays.indexOf(overlay);
3784
+ if (i >= 0) this.overlays.splice(i, 1);
3785
+ this.invalidate();
3786
+ }
3787
+ };
3788
+ }
3725
3789
  /** Fill markers (▲ buy under the bar, ▼ sell above; ⚡ at the price for liquidations). Replaces the previous set. */
3726
3790
  setExecutions(markers) {
3727
3791
  this.trading.setExecutions(markers);
@@ -3828,6 +3892,7 @@ var Chart = class {
3828
3892
  replaySelecting: this.replaySelecting,
3829
3893
  objects: {
3830
3894
  coords: this.paneCoords(),
3895
+ overlays: this.overlays,
3831
3896
  drawings: this.drawings,
3832
3897
  trading: this.trading,
3833
3898
  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,25 @@ 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
+ }
14
28
  export interface Drawing {
15
29
  id: string;
16
30
  type: DrawingType;
17
31
  points: DrawingPoint[];
18
32
  color?: string;
33
+ style?: DrawingStyle;
19
34
  }
20
35
  /** What a drawing definition gets to measure and paint itself. Pixels are relative to the price pane. */
21
36
  export interface DrawingGeometry {
@@ -28,6 +43,16 @@ export interface DrawingRenderContext extends DrawingGeometry {
28
43
  palette: Palette;
29
44
  /** `drawing.color`, or the theme accent. */
30
45
  color: string;
46
+ /** `drawing.style` (empty object when none). */
47
+ style: DrawingStyle;
48
+ /** The main line width: the drawing's style, else the tool's default. */
49
+ width(fallback: number): number;
50
+ /** The main line dash pattern for canvas: the drawing's style, else the tool's default. */
51
+ dash(fallback?: readonly number[]): number[];
52
+ /** The chart's bars (oldest first), for tools that measure them (VWAP, volume profile…). */
53
+ bars: readonly Bar[];
54
+ /** Index of the last bar at or before `time` (-1 before the first). */
55
+ barIndexAt(time: number): number;
31
56
  /** Adds a price-axis badge (drawn above the axis labels, clamped to the pane). */
32
57
  badge(y: number, text: string, bg: string, fg?: string): void;
33
58
  }
@@ -45,6 +70,11 @@ export interface DrawingDefinition {
45
70
  icon?: string;
46
71
  /** Tools with the same group share one drawing-bar button with a menu. Default: the type. */
47
72
  group?: string;
73
+ /**
74
+ * A heading in that menu (피보나치, 갠, 엘리어트 파동…): consecutive tools with the same section
75
+ * are listed under it, with a divider between sections. Default: no heading.
76
+ */
77
+ section?: Record<Locale, string>;
48
78
  /** Clicks (or drag ends) needed to place it. */
49
79
  clicks: number;
50
80
  /**
@@ -56,6 +86,13 @@ export interface DrawingDefinition {
56
86
  constrain?(points: DrawingPoint[], index: number): DrawingPoint[];
57
87
  /** Drag handle positions; dragging handle i moves point i. Default: the points. */
58
88
  handles?(geometry: DrawingGeometry): [number, number][];
89
+ /**
90
+ * Drawn by dragging: every pointer move adds a point (a brush); releasing (or clicking again)
91
+ * finishes it. `clicks` is ignored.
92
+ */
93
+ freehand?: boolean;
94
+ /** Has text (`style.text`): the widget offers a text field for it when selected. */
95
+ text?: boolean;
59
96
  /** Distance in px from (x, y) to the shape, for hit testing. */
60
97
  distance(geometry: DrawingGeometry, x: number, y: number): number;
61
98
  render(drawing: Drawing, rc: DrawingRenderContext): void;
@@ -79,6 +116,7 @@ export declare class DrawingManager {
79
116
  tool: DrawingType | null;
80
117
  selectedId: string | null;
81
118
  private draft;
119
+ private freehandLast;
82
120
  private drag;
83
121
  constructor(onChange: (change: DrawingsChange) => void, registry?: Map<string, DrawingDefinition>);
84
122
  get selected(): Drawing | null;
@@ -104,5 +142,5 @@ export declare class DrawingManager {
104
142
  beginDrag(c: PaneCoords, id: string, part: 'body' | number, x: number, y: number): void;
105
143
  dragTo(c: PaneCoords, x: number, y: number): void;
106
144
  endDrag(moved: boolean): void;
107
- render(rc: ObjectRenderContext, badges: AxisBadge[]): void;
145
+ render(rc: ObjectRenderContext, badges: AxisBadge[], bars?: readonly Bar[], barIndexAt?: (time: number) => number): void;
108
146
  }
@@ -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.8",
3
+ "version": "0.6.11",
4
4
  "description": "Canvas charting engine for financial charts",
5
5
  "keywords": [
6
6
  "chart",