@qfo/qfchart 0.8.0 → 0.8.1

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.
Files changed (50) hide show
  1. package/dist/index.d.ts +319 -12
  2. package/dist/qfchart.min.browser.js +32 -16
  3. package/dist/qfchart.min.es.js +32 -16
  4. package/package.json +1 -1
  5. package/src/QFChart.ts +98 -262
  6. package/src/components/AbstractPlugin.ts +234 -104
  7. package/src/components/DrawingEditor.ts +297 -248
  8. package/src/components/DrawingRendererRegistry.ts +13 -0
  9. package/src/components/GraphicBuilder.ts +2 -2
  10. package/src/components/LayoutManager.ts +41 -35
  11. package/src/components/SeriesBuilder.ts +10 -10
  12. package/src/components/TooltipFormatter.ts +1 -1
  13. package/src/index.ts +17 -6
  14. package/src/plugins/ABCDPatternTool/ABCDPatternDrawingRenderer.ts +112 -0
  15. package/src/plugins/ABCDPatternTool/ABCDPatternTool.ts +136 -0
  16. package/src/plugins/ABCDPatternTool/index.ts +2 -0
  17. package/src/plugins/CypherPatternTool/CypherPatternDrawingRenderer.ts +80 -0
  18. package/src/plugins/CypherPatternTool/CypherPatternTool.ts +84 -0
  19. package/src/plugins/CypherPatternTool/index.ts +2 -0
  20. package/src/plugins/FibSpeedResistanceFanTool/FibSpeedResistanceFanDrawingRenderer.ts +163 -0
  21. package/src/plugins/FibSpeedResistanceFanTool/FibSpeedResistanceFanTool.ts +210 -0
  22. package/src/plugins/FibSpeedResistanceFanTool/index.ts +2 -0
  23. package/src/plugins/FibTrendExtensionTool/FibTrendExtensionDrawingRenderer.ts +141 -0
  24. package/src/plugins/FibTrendExtensionTool/FibTrendExtensionTool.ts +188 -0
  25. package/src/plugins/FibTrendExtensionTool/index.ts +2 -0
  26. package/src/plugins/FibonacciChannelTool/FibonacciChannelDrawingRenderer.ts +128 -0
  27. package/src/plugins/FibonacciChannelTool/FibonacciChannelTool.ts +231 -0
  28. package/src/plugins/FibonacciChannelTool/index.ts +2 -0
  29. package/src/plugins/FibonacciTool/FibonacciDrawingRenderer.ts +107 -0
  30. package/src/plugins/{FibonacciTool.ts → FibonacciTool/FibonacciTool.ts} +195 -192
  31. package/src/plugins/FibonacciTool/index.ts +2 -0
  32. package/src/plugins/HeadAndShouldersTool/HeadAndShouldersDrawingRenderer.ts +95 -0
  33. package/src/plugins/HeadAndShouldersTool/HeadAndShouldersTool.ts +97 -0
  34. package/src/plugins/HeadAndShouldersTool/index.ts +2 -0
  35. package/src/plugins/LineTool/LineDrawingRenderer.ts +49 -0
  36. package/src/plugins/{LineTool.ts → LineTool/LineTool.ts} +161 -190
  37. package/src/plugins/LineTool/index.ts +2 -0
  38. package/src/plugins/{MeasureTool.ts → MeasureTool/MeasureTool.ts} +324 -344
  39. package/src/plugins/MeasureTool/index.ts +1 -0
  40. package/src/plugins/ThreeDrivesPatternTool/ThreeDrivesPatternDrawingRenderer.ts +106 -0
  41. package/src/plugins/ThreeDrivesPatternTool/ThreeDrivesPatternTool.ts +98 -0
  42. package/src/plugins/ThreeDrivesPatternTool/index.ts +2 -0
  43. package/src/plugins/ToolGroup.ts +211 -0
  44. package/src/plugins/TrianglePatternTool/TrianglePatternDrawingRenderer.ts +107 -0
  45. package/src/plugins/TrianglePatternTool/TrianglePatternTool.ts +98 -0
  46. package/src/plugins/TrianglePatternTool/index.ts +2 -0
  47. package/src/plugins/XABCDPatternTool/XABCDPatternDrawingRenderer.ts +178 -0
  48. package/src/plugins/XABCDPatternTool/XABCDPatternTool.ts +213 -0
  49. package/src/plugins/XABCDPatternTool/index.ts +2 -0
  50. package/src/types.ts +37 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qfo/qfchart",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Professional financial charting library built on Apache ECharts with candlestick charts, technical indicators, and interactive drawing tools",
5
5
  "keywords": [
6
6
  "chart",
package/src/QFChart.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as echarts from 'echarts';
2
- import { OHLCV, IndicatorPlot, QFChartOptions, Indicator as IndicatorInterface, ChartContext, Plugin } from './types';
2
+ import { OHLCV, IndicatorPlot, QFChartOptions, Indicator as IndicatorInterface, ChartContext, Plugin, DrawingRenderer } from './types';
3
3
  import { Indicator } from './components/Indicator';
4
4
  import { LayoutManager, LayoutResult, PaneBoundary } from './components/LayoutManager';
5
5
  import { SeriesBuilder } from './components/SeriesBuilder';
@@ -7,6 +7,7 @@ import { GraphicBuilder } from './components/GraphicBuilder';
7
7
  import { TooltipFormatter } from './components/TooltipFormatter';
8
8
  import { PluginManager } from './components/PluginManager';
9
9
  import { DrawingEditor } from './components/DrawingEditor';
10
+ import { DrawingRendererRegistry } from './components/DrawingRendererRegistry';
10
11
  import { EventBus } from './utils/EventBus';
11
12
  import { AxisUtils } from './utils/AxisUtils';
12
13
  import { TableOverlayRenderer } from './components/TableOverlayRenderer';
@@ -29,6 +30,7 @@ export class QFChart implements ChartContext {
29
30
 
30
31
  // Drawing System
31
32
  private drawings: import('./types').DrawingElement[] = [];
33
+ private drawingRenderers: DrawingRendererRegistry = new DrawingRendererRegistry();
32
34
 
33
35
  public coordinateConversion = {
34
36
  pixelToData: (point: { x: number; y: number }) => {
@@ -117,7 +119,7 @@ export class QFChart implements ChartContext {
117
119
  constructor(container: HTMLElement, options: QFChartOptions = {}) {
118
120
  this.rootContainer = container;
119
121
  this.options = {
120
- title: 'Market',
122
+ title: undefined,
121
123
  height: '600px',
122
124
  backgroundColor: '#1e293b',
123
125
  upColor: '#00da3c',
@@ -214,6 +216,8 @@ export class QFChart implements ChartContext {
214
216
  this.pluginManager = new PluginManager(this, this.toolbarContainer);
215
217
  this.drawingEditor = new DrawingEditor(this);
216
218
 
219
+
220
+
217
221
  // Bind global chart/ZRender events to the EventBus
218
222
  this.chart.on('dataZoom', (params: any) => {
219
223
  this.events.emit('chart:dataZoom', params);
@@ -507,8 +511,8 @@ export class QFChart implements ChartContext {
507
511
  });
508
512
  // Set cursor
509
513
  this.chart.getZr().setCursorStyle('move');
510
- } else if (info.targetName?.startsWith('point')) {
511
- const pointIdx = info.targetName === 'point-start' ? 0 : 1;
514
+ } else if (info.targetName?.startsWith('point-')) {
515
+ const pointIdx = parseInt(info.targetName.split('-')[1]) || 0;
512
516
  this.events.emit('drawing:point:hover', {
513
517
  id: info.drawing.id,
514
518
  pointIndex: pointIdx,
@@ -549,8 +553,8 @@ export class QFChart implements ChartContext {
549
553
 
550
554
  if (info.targetName === 'line') {
551
555
  this.events.emit('drawing:mouseout', { id: info.drawing.id });
552
- } else if (info.targetName?.startsWith('point')) {
553
- const pointIdx = info.targetName === 'point-start' ? 0 : 1;
556
+ } else if (info.targetName?.startsWith('point-')) {
557
+ const pointIdx = parseInt(info.targetName.split('-')[1]) || 0;
554
558
  this.events.emit('drawing:point:mouseout', {
555
559
  id: info.drawing.id,
556
560
  pointIndex: pointIdx,
@@ -573,8 +577,8 @@ export class QFChart implements ChartContext {
573
577
  x,
574
578
  y,
575
579
  });
576
- } else if (info.targetName?.startsWith('point')) {
577
- const pointIdx = info.targetName === 'point-start' ? 0 : 1;
580
+ } else if (info.targetName?.startsWith('point-')) {
581
+ const pointIdx = parseInt(info.targetName.split('-')[1]) || 0;
578
582
  this.events.emit('drawing:point:mousedown', {
579
583
  id: info.drawing.id,
580
584
  pointIndex: pointIdx,
@@ -597,8 +601,8 @@ export class QFChart implements ChartContext {
597
601
 
598
602
  if (info.targetName === 'line') {
599
603
  this.events.emit('drawing:click', { id: info.drawing.id });
600
- } else if (info.targetName?.startsWith('point')) {
601
- const pointIdx = info.targetName === 'point-start' ? 0 : 1;
604
+ } else if (info.targetName?.startsWith('point-')) {
605
+ const pointIdx = parseInt(info.targetName.split('-')[1]) || 0;
602
606
  this.events.emit('drawing:point:click', {
603
607
  id: info.drawing.id,
604
608
  pointIndex: pointIdx,
@@ -686,6 +690,64 @@ export class QFChart implements ChartContext {
686
690
  this.pluginManager.register(plugin);
687
691
  }
688
692
 
693
+ public registerDrawingRenderer(renderer: DrawingRenderer): void {
694
+ this.drawingRenderers.register(renderer);
695
+ }
696
+
697
+ public snapToCandle(point: { x: number; y: number }): { x: number; y: number } {
698
+ // Find which pane the point is in
699
+ const dataCoord = this.coordinateConversion.pixelToData(point);
700
+ if (!dataCoord) return point;
701
+
702
+ const paneIndex = dataCoord.paneIndex || 0;
703
+ // Only snap on the main pane (candlestick data)
704
+ if (paneIndex !== 0) return point;
705
+
706
+ // Get the nearest candle by time index
707
+ const realIndex = Math.round(dataCoord.timeIndex);
708
+ if (realIndex < 0 || realIndex >= this.marketData.length) return point;
709
+
710
+ const candle = this.marketData[realIndex];
711
+ if (!candle) return point;
712
+
713
+ // Snap X to the exact candle center
714
+ const snappedX = this.chart.convertToPixel(
715
+ { gridIndex: paneIndex },
716
+ [realIndex + this.dataIndexOffset, candle.close],
717
+ );
718
+ if (!snappedX) return point;
719
+ const snapPxX = snappedX[0];
720
+
721
+ // Find closest OHLC value by Y distance
722
+ const ohlc = [candle.open, candle.high, candle.low, candle.close];
723
+ let bestValue = ohlc[0];
724
+ let bestDist = Infinity;
725
+
726
+ for (const val of ohlc) {
727
+ const px = this.chart.convertToPixel(
728
+ { gridIndex: paneIndex },
729
+ [realIndex + this.dataIndexOffset, val],
730
+ );
731
+ if (px) {
732
+ const dist = Math.abs(px[1] - point.y);
733
+ if (dist < bestDist) {
734
+ bestDist = dist;
735
+ bestValue = val;
736
+ }
737
+ }
738
+ }
739
+
740
+ const snappedY = this.chart.convertToPixel(
741
+ { gridIndex: paneIndex },
742
+ [realIndex + this.dataIndexOffset, bestValue],
743
+ );
744
+
745
+ return {
746
+ x: snapPxX,
747
+ y: snappedY ? snappedY[1] : point.y,
748
+ };
749
+ }
750
+
689
751
  // --- Drawing System ---
690
752
 
691
753
  public addDrawing(drawing: import('./types').DrawingElement): void {
@@ -1049,7 +1111,7 @@ export class QFChart implements ChartContext {
1049
1111
  this.chart.setOption({
1050
1112
  series: [
1051
1113
  {
1052
- name: this.options.title || 'Market',
1114
+ id: '__candlestick__',
1053
1115
  markLine: {
1054
1116
  data: [
1055
1117
  {
@@ -1593,260 +1655,34 @@ export class QFChart implements ChartContext {
1593
1655
  const drawing = drawings[params.dataIndex];
1594
1656
  if (!drawing) return;
1595
1657
 
1596
- const start = drawing.points[0];
1597
- const end = drawing.points[1];
1598
-
1599
- if (!start || !end) return;
1658
+ const renderer = this.drawingRenderers.get(drawing.type);
1659
+ if (!renderer) return;
1600
1660
 
1601
- // Convert real data indices to padded space for ECharts rendering
1602
1661
  const drawingOffset = this.dataIndexOffset;
1603
- const p1 = api.coord([start.timeIndex + drawingOffset, start.value]);
1604
- const p2 = api.coord([end.timeIndex + drawingOffset, end.value]);
1605
-
1606
- const isSelected = drawing.id === this.selectedDrawingId;
1607
-
1608
- if (drawing.type === 'line') {
1609
- return {
1610
- type: 'group',
1611
- children: [
1612
- {
1613
- type: 'line',
1614
- name: 'line',
1615
- shape: {
1616
- x1: p1[0],
1617
- y1: p1[1],
1618
- x2: p2[0],
1619
- y2: p2[1],
1620
- },
1621
- style: {
1622
- stroke: drawing.style?.color || '#3b82f6',
1623
- lineWidth: drawing.style?.lineWidth || 2,
1624
- },
1625
- },
1626
- {
1627
- type: 'circle',
1628
- name: 'point-start',
1629
- shape: { cx: p1[0], cy: p1[1], r: 4 },
1630
- style: {
1631
- fill: '#fff',
1632
- stroke: drawing.style?.color || '#3b82f6',
1633
- lineWidth: 1,
1634
- opacity: isSelected ? 1 : 0, // Show if selected
1635
- },
1636
- },
1637
- {
1638
- type: 'circle',
1639
- name: 'point-end',
1640
- shape: { cx: p2[0], cy: p2[1], r: 4 },
1641
- style: {
1642
- fill: '#fff',
1643
- stroke: drawing.style?.color || '#3b82f6',
1644
- lineWidth: 1,
1645
- opacity: isSelected ? 1 : 0, // Show if selected
1646
- },
1647
- },
1648
- ],
1649
- };
1650
- } else if (drawing.type === 'fibonacci') {
1651
- const x1 = p1[0];
1652
- const y1 = p1[1];
1653
- const x2 = p2[0];
1654
- const y2 = p2[1];
1655
-
1656
- const startX = Math.min(x1, x2);
1657
- const endX = Math.max(x1, x2);
1658
- const width = endX - startX;
1659
- const diffY = y2 - y1;
1660
-
1661
- const levels = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
1662
- const colors = ['#787b86', '#f44336', '#ff9800', '#4caf50', '#2196f3', '#00bcd4', '#787b86'];
1663
-
1664
- const children: any[] = [];
1665
-
1666
- // 1. Diagonal Line
1667
- children.push({
1668
- type: 'line',
1669
- name: 'line', // Use 'line' name to enable dragging logic in DrawingEditor
1670
- shape: { x1, y1, x2, y2 },
1671
- style: {
1672
- stroke: '#999',
1673
- lineWidth: 1,
1674
- lineDash: [4, 4],
1675
- },
1676
- });
1677
-
1678
- // 2. Control Points (invisible by default)
1679
- children.push({
1680
- type: 'circle',
1681
- name: 'point-start',
1682
- shape: { cx: x1, cy: y1, r: 4 },
1683
- style: {
1684
- fill: '#fff',
1685
- stroke: drawing.style?.color || '#3b82f6',
1686
- lineWidth: 1,
1687
- opacity: isSelected ? 1 : 0,
1688
- },
1689
- z: 100, // Ensure on top
1690
- });
1691
- children.push({
1692
- type: 'circle',
1693
- name: 'point-end',
1694
- shape: { cx: x2, cy: y2, r: 4 },
1695
- style: {
1696
- fill: '#fff',
1697
- stroke: drawing.style?.color || '#3b82f6',
1698
- lineWidth: 1,
1699
- opacity: isSelected ? 1 : 0,
1700
- },
1701
- z: 100,
1702
- });
1703
-
1704
- // 3. Levels and Backgrounds
1705
- levels.forEach((level, index) => {
1706
- const levelY = y2 - diffY * level;
1707
- const color = colors[index % colors.length];
1708
-
1709
- // Horizontal Line
1710
- children.push({
1711
- type: 'line',
1712
- name: 'fib-line', // distinct name, maybe we don't want to drag by clicking these lines? or yes? 'line' triggers drag. 'fib-line' won't unless we update logic.
1713
- // The user asked for "fib levels between start and end".
1714
- shape: { x1: startX, y1: levelY, x2: endX, y2: levelY },
1715
- style: { stroke: color, lineWidth: 1 },
1716
- silent: true, // Make internal lines silent so clicks pass to background/diagonal?
1717
- });
1718
-
1719
- const startVal = drawing.points[0].value;
1720
- const endVal = drawing.points[1].value;
1721
- const valDiff = endVal - startVal;
1722
- const price = endVal - valDiff * level;
1723
-
1724
- children.push({
1725
- type: 'text',
1726
- style: {
1727
- text: `${level} (${price.toFixed(2)})`,
1728
- x: startX + 5,
1729
- y: levelY - 10,
1730
- fill: color,
1731
- fontSize: 10,
1732
- },
1733
- silent: true,
1734
- });
1735
-
1736
- // Background
1737
- if (index < levels.length - 1) {
1738
- const nextLevel = levels[index + 1];
1739
- const nextY = y2 - diffY * nextLevel;
1740
- const rectH = Math.abs(nextY - levelY);
1741
- const rectY = Math.min(levelY, nextY);
1742
-
1743
- children.push({
1744
- type: 'rect',
1745
- shape: { x: startX, y: rectY, width, height: rectH },
1746
- style: {
1747
- fill: colors[(index + 1) % colors.length],
1748
- opacity: 0.1,
1749
- },
1750
- silent: true, // Let clicks pass through?
1751
- });
1752
- }
1753
- });
1754
-
1755
- const backgrounds: any[] = [];
1756
- const linesAndText: any[] = [];
1757
-
1758
- levels.forEach((level, index) => {
1759
- const levelY = y2 - diffY * level;
1760
- const color = colors[index % colors.length];
1761
-
1762
- linesAndText.push({
1763
- type: 'line',
1764
- shape: { x1: startX, y1: levelY, x2: endX, y2: levelY },
1765
- style: { stroke: color, lineWidth: 1 },
1766
- silent: true,
1767
- });
1768
-
1769
- const startVal = drawing.points[0].value;
1770
- const endVal = drawing.points[1].value;
1771
- const valDiff = endVal - startVal;
1772
- const price = endVal - valDiff * level;
1773
-
1774
- linesAndText.push({
1775
- type: 'text',
1776
- style: {
1777
- text: `${level} (${price.toFixed(2)})`,
1778
- x: startX + 5,
1779
- y: levelY - 10,
1780
- fill: color,
1781
- fontSize: 10,
1782
- },
1783
- silent: true,
1784
- });
1785
-
1786
- if (index < levels.length - 1) {
1787
- const nextLevel = levels[index + 1];
1788
- const nextY = y2 - diffY * nextLevel;
1789
- const rectH = Math.abs(nextY - levelY);
1790
- const rectY = Math.min(levelY, nextY);
1791
-
1792
- backgrounds.push({
1793
- type: 'rect',
1794
- name: 'line', // Enable dragging by clicking background!
1795
- shape: { x: startX, y: rectY, width, height: rectH },
1796
- style: {
1797
- fill: colors[(index + 1) % colors.length],
1798
- opacity: 0.1,
1799
- },
1800
- });
1801
- }
1802
- });
1803
-
1804
- return {
1805
- type: 'group',
1806
- children: [
1807
- ...backgrounds,
1808
- ...linesAndText,
1809
- {
1810
- type: 'line',
1811
- name: 'line',
1812
- shape: { x1, y1, x2, y2 },
1813
- style: { stroke: '#999', lineWidth: 1, lineDash: [4, 4] },
1814
- },
1815
- {
1816
- type: 'circle',
1817
- name: 'point-start',
1818
- shape: { cx: x1, cy: y1, r: 4 },
1819
- style: {
1820
- fill: '#fff',
1821
- stroke: drawing.style?.color || '#3b82f6',
1822
- lineWidth: 1,
1823
- opacity: isSelected ? 1 : 0,
1824
- },
1825
- z: 100,
1826
- },
1827
- {
1828
- type: 'circle',
1829
- name: 'point-end',
1830
- shape: { cx: x2, cy: y2, r: 4 },
1831
- style: {
1832
- fill: '#fff',
1833
- stroke: drawing.style?.color || '#3b82f6',
1834
- lineWidth: 1,
1835
- opacity: isSelected ? 1 : 0,
1836
- },
1837
- z: 100,
1838
- },
1839
- ],
1840
- };
1841
- }
1662
+ const pixelPoints = drawing.points.map(
1663
+ (p) => api.coord([p.timeIndex + drawingOffset, p.value]) as [number, number],
1664
+ );
1665
+
1666
+ return renderer.render({
1667
+ drawing,
1668
+ pixelPoints,
1669
+ isSelected: drawing.id === this.selectedDrawingId,
1670
+ api,
1671
+ });
1842
1672
  },
1843
- data: drawings.map((d) => [
1844
- d.points[0].timeIndex + this.dataIndexOffset,
1845
- d.points[0].value,
1846
- d.points[1].timeIndex + this.dataIndexOffset,
1847
- d.points[1].value,
1848
- ]),
1849
- encode: { x: [0, 2], y: [1, 3] },
1673
+ data: drawings.map((d) => {
1674
+ const row: number[] = [];
1675
+ d.points.forEach((p) => {
1676
+ row.push(p.timeIndex + this.dataIndexOffset, p.value);
1677
+ });
1678
+ return row;
1679
+ }),
1680
+ encode: (() => {
1681
+ const maxPoints = drawings.reduce((max, d) => Math.max(max, d.points.length), 0);
1682
+ const xDims = Array.from({ length: maxPoints }, (_, i) => i * 2);
1683
+ const yDims = Array.from({ length: maxPoints }, (_, i) => i * 2 + 1);
1684
+ return { x: xDims, y: yDims };
1685
+ })(),
1850
1686
  z: 100,
1851
1687
  silent: false,
1852
1688
  });