@qfo/qfchart 0.6.8 → 0.7.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.
@@ -18,7 +18,6 @@ export class DrawingLineRenderer implements SeriesRenderer {
18
18
  // (since multiple objects at the same bar would overwrite each other in the
19
19
  // sparse array). Handle both array-of-objects and single-object entries.
20
20
  const lineObjects: any[] = [];
21
- const lineData: number[][] = [];
22
21
 
23
22
  for (let i = 0; i < dataArray.length; i++) {
24
23
  const val = dataArray[i];
@@ -28,75 +27,82 @@ export class DrawingLineRenderer implements SeriesRenderer {
28
27
  for (const ln of items) {
29
28
  if (ln && typeof ln === 'object' && !ln._deleted) {
30
29
  lineObjects.push(ln);
31
- // Apply padding offset for bar_index-based coordinates
32
- const xOff = ln.xloc === 'bar_index' ? offset : 0;
33
- lineData.push([ln.x1 + xOff, ln.y1, ln.x2 + xOff, ln.y2]);
34
30
  }
35
31
  }
36
32
  }
37
33
 
38
- if (lineData.length === 0) {
34
+ if (lineObjects.length === 0) {
39
35
  return { name: seriesName, type: 'custom', xAxisIndex, yAxisIndex, data: [], silent: true };
40
36
  }
41
37
 
38
+ // Compute y-range for axis scaling
39
+ let yMin = Infinity, yMax = -Infinity;
40
+ for (const ln of lineObjects) {
41
+ if (ln.y1 < yMin) yMin = ln.y1;
42
+ if (ln.y1 > yMax) yMax = ln.y1;
43
+ if (ln.y2 < yMin) yMin = ln.y2;
44
+ if (ln.y2 > yMax) yMax = ln.y2;
45
+ }
46
+
47
+ // Use a SINGLE data entry spanning the full x-range so renderItem is always called.
48
+ // ECharts filters a data item only when ALL its x-dimensions are on the same side
49
+ // of the visible window. With dims 0=0 and 1=lastBar the item always straddles
50
+ // the viewport, so renderItem fires exactly once regardless of scroll position.
51
+ // Dims 2/3 are yMin/yMax for axis scaling.
52
+ const totalBars = (context.candlestickData?.length || 0) + offset;
53
+ const lastBarIndex = Math.max(0, totalBars - 1);
54
+
42
55
  return {
43
56
  name: seriesName,
44
57
  type: 'custom',
45
58
  xAxisIndex,
46
59
  yAxisIndex,
47
60
  renderItem: (params: any, api: any) => {
48
- const idx = params.dataIndex;
49
- const ln = lineObjects[idx];
50
- if (!ln || ln._deleted) return;
51
-
52
- const x1 = api.value(0);
53
- const y1 = api.value(1);
54
- const x2 = api.value(2);
55
- const y2 = api.value(3);
56
-
57
- let p1 = api.coord([x1, y1]);
58
- let p2 = api.coord([x2, y2]);
59
-
60
- // Handle extend (none | left | right | both)
61
- const extend = ln.extend || 'none';
62
- if (extend !== 'none') {
63
- const cs = params.coordSys;
64
- const left = cs.x;
65
- const right = cs.x + cs.width;
66
- const top = cs.y;
67
- const bottom = cs.y + cs.height;
68
- [p1, p2] = this.extendLine(p1, p2, extend, left, right, top, bottom);
69
- }
70
-
71
61
  const children: any[] = [];
72
- const color = ln.color || defaultColor;
73
- const lineWidth = ln.width || 1;
74
-
75
- // Main line segment
76
- children.push({
77
- type: 'line',
78
- shape: { x1: p1[0], y1: p1[1], x2: p2[0], y2: p2[1] },
79
- style: {
80
- stroke: color,
81
- lineWidth: lineWidth,
82
- lineDash: this.getDashPattern(ln.style),
83
- },
84
- });
85
-
86
- // Arrow heads based on style
87
- const style = ln.style || 'style_solid';
88
- if (style === 'style_arrow_left' || style === 'style_arrow_both') {
89
- const arrow = this.arrowHead(p2, p1, lineWidth, color);
90
- if (arrow) children.push(arrow);
91
- }
92
- if (style === 'style_arrow_right' || style === 'style_arrow_both') {
93
- const arrow = this.arrowHead(p1, p2, lineWidth, color);
94
- if (arrow) children.push(arrow);
62
+
63
+ for (const ln of lineObjects) {
64
+ if (ln._deleted) continue;
65
+ const xOff = (ln.xloc === 'bar_index' || ln.xloc === 'bi') ? offset : 0;
66
+
67
+ let p1 = api.coord([ln.x1 + xOff, ln.y1]);
68
+ let p2 = api.coord([ln.x2 + xOff, ln.y2]);
69
+
70
+ // Handle extend (none | left | right | both)
71
+ const extend = ln.extend || 'none';
72
+ if (extend !== 'none') {
73
+ const cs = params.coordSys;
74
+ [p1, p2] = this.extendLine(p1, p2, extend, cs.x, cs.x + cs.width, cs.y, cs.y + cs.height);
75
+ }
76
+
77
+ const color = ln.color || defaultColor;
78
+ const lineWidth = ln.width || 1;
79
+
80
+ children.push({
81
+ type: 'line',
82
+ shape: { x1: p1[0], y1: p1[1], x2: p2[0], y2: p2[1] },
83
+ style: {
84
+ stroke: color,
85
+ lineWidth,
86
+ lineDash: this.getDashPattern(ln.style),
87
+ },
88
+ });
89
+
90
+ const style = ln.style || 'style_solid';
91
+ if (style === 'style_arrow_left' || style === 'style_arrow_both') {
92
+ const arrow = this.arrowHead(p2, p1, lineWidth, color);
93
+ if (arrow) children.push(arrow);
94
+ }
95
+ if (style === 'style_arrow_right' || style === 'style_arrow_both') {
96
+ const arrow = this.arrowHead(p1, p2, lineWidth, color);
97
+ if (arrow) children.push(arrow);
98
+ }
95
99
  }
96
100
 
97
101
  return { type: 'group', children };
98
102
  },
99
- data: lineData,
103
+ data: [[0, lastBarIndex, yMin, yMax]],
104
+ clip: true,
105
+ encode: { x: [0, 1], y: [2, 3] },
100
106
  z: 15,
101
107
  silent: true,
102
108
  emphasis: { disabled: true },
@@ -37,18 +37,18 @@ export class LabelRenderer implements SeriesRenderer {
37
37
  const shape = this.styleToShape(styleRaw);
38
38
 
39
39
  // Determine X position using label's own x coordinate
40
- const xPos = lbl.xloc === 'bar_index' ? (lbl.x + offset) : lbl.x;
40
+ const xPos = (lbl.xloc === 'bar_index' || lbl.xloc === 'bi') ? (lbl.x + offset) : lbl.x;
41
41
 
42
42
  // Determine Y value based on yloc
43
43
  let yValue = lbl.y;
44
44
  let symbolOffset: (string | number)[] = [0, 0];
45
45
 
46
- if (yloc === 'abovebar') {
46
+ if (yloc === 'abovebar' || yloc === 'AboveBar' || yloc === 'ab') {
47
47
  if (candlestickData && candlestickData[xPos]) {
48
48
  yValue = candlestickData[xPos].high;
49
49
  }
50
50
  symbolOffset = [0, '-150%'];
51
- } else if (yloc === 'belowbar') {
51
+ } else if (yloc === 'belowbar' || yloc === 'BelowBar' || yloc === 'bl') {
52
52
  if (candlestickData && candlestickData[xPos]) {
53
53
  yValue = candlestickData[xPos].low;
54
54
  }
@@ -64,7 +64,8 @@ export class LabelRenderer implements SeriesRenderer {
64
64
 
65
65
  // Dynamically size the bubble to fit text content
66
66
  let finalSize: number | number[];
67
- const isBubble = shape === 'labeldown' || shape === 'labelup' ||
67
+ const isBubble = shape === 'labeldown' || shape === 'shape_label_down' ||
68
+ shape === 'labelup' || shape === 'shape_label_up' ||
68
69
  shape === 'labelleft' || shape === 'labelright';
69
70
  // Track label text offset for centering text within the body
70
71
  // (excluding the pointer area)
@@ -150,8 +151,8 @@ export class LabelRenderer implements SeriesRenderer {
150
151
  fontSize: fontSize,
151
152
  fontWeight: 'bold',
152
153
  align: isInsideLabel ? 'center'
153
- : textalign === 'align_left' ? 'left'
154
- : textalign === 'align_right' ? 'right'
154
+ : (textalign === 'align_left' || textalign === 'left') ? 'left'
155
+ : (textalign === 'align_right' || textalign === 'right') ? 'right'
155
156
  : 'center',
156
157
  verticalAlign: 'middle',
157
158
  padding: [2, 6],
@@ -247,10 +248,10 @@ export class LabelRenderer implements SeriesRenderer {
247
248
  case 'text_outline':
248
249
  case 'none':
249
250
  // Text only, positioned based on yloc
250
- return yloc === 'abovebar' ? 'top' : yloc === 'belowbar' ? 'bottom' : 'top';
251
+ return (yloc === 'abovebar' || yloc === 'AboveBar' || yloc === 'ab') ? 'top' : (yloc === 'belowbar' || yloc === 'BelowBar' || yloc === 'bl') ? 'bottom' : 'top';
251
252
  default:
252
253
  // For simple shapes (circle, diamond, etc.), text goes outside
253
- return yloc === 'belowbar' ? 'bottom' : 'top';
254
+ return (yloc === 'belowbar' || yloc === 'BelowBar' || yloc === 'bl') ? 'bottom' : 'top';
254
255
  }
255
256
  }
256
257
 
@@ -16,7 +16,6 @@ export class LinefillRenderer implements SeriesRenderer {
16
16
  // Same aggregation pattern as DrawingLineRenderer — objects are stored
17
17
  // as an array in a single data entry.
18
18
  const fillObjects: any[] = [];
19
- const fillData: number[][] = [];
20
19
 
21
20
  for (let i = 0; i < dataArray.length; i++) {
22
21
  const val = dataArray[i];
@@ -31,93 +30,82 @@ export class LinefillRenderer implements SeriesRenderer {
31
30
  if (!line1 || !line2 || line1._deleted || line2._deleted) continue;
32
31
 
33
32
  fillObjects.push(lf);
34
-
35
- // Store all 8 coordinates for the two lines
36
- const xOff1 = line1.xloc === 'bar_index' ? offset : 0;
37
- const xOff2 = line2.xloc === 'bar_index' ? offset : 0;
38
- fillData.push([
39
- line1.x1 + xOff1, line1.y1,
40
- line1.x2 + xOff1, line1.y2,
41
- line2.x1 + xOff2, line2.y1,
42
- line2.x2 + xOff2, line2.y2,
43
- ]);
44
33
  }
45
34
  }
46
35
 
47
- if (fillData.length === 0) {
36
+ if (fillObjects.length === 0) {
48
37
  return { name: seriesName, type: 'custom', xAxisIndex, yAxisIndex, data: [], silent: true };
49
38
  }
50
39
 
40
+ // Compute y-range for axis scaling
41
+ let yMin = Infinity, yMax = -Infinity;
42
+ for (const lf of fillObjects) {
43
+ for (const y of [lf.line1.y1, lf.line1.y2, lf.line2.y1, lf.line2.y2]) {
44
+ if (y < yMin) yMin = y;
45
+ if (y > yMax) yMax = y;
46
+ }
47
+ }
48
+
49
+ // Use a SINGLE data entry spanning the full x-range so renderItem is always called.
50
+ // ECharts filters a data item only when ALL its x-dimensions are on the same side
51
+ // of the visible window. With dims 0=0 and 1=lastBar the item always straddles
52
+ // the viewport, so renderItem fires exactly once regardless of scroll position.
53
+ // Dims 2/3 are yMin/yMax for axis scaling.
54
+ const totalBars = (context.candlestickData?.length || 0) + offset;
55
+ const lastBarIndex = Math.max(0, totalBars - 1);
56
+
51
57
  return {
52
58
  name: seriesName,
53
59
  type: 'custom',
54
60
  xAxisIndex,
55
61
  yAxisIndex,
56
62
  renderItem: (params: any, api: any) => {
57
- const idx = params.dataIndex;
58
- const lf = fillObjects[idx];
59
- if (!lf || lf._deleted) return;
60
-
61
- const line1 = lf.line1;
62
- const line2 = lf.line2;
63
- if (!line1 || !line2 || line1._deleted || line2._deleted) return;
64
-
65
- // Get data values: line1 start, line1 end, line2 start, line2 end
66
- const l1x1 = api.value(0);
67
- const l1y1 = api.value(1);
68
- const l1x2 = api.value(2);
69
- const l1y2 = api.value(3);
70
- const l2x1 = api.value(4);
71
- const l2y1 = api.value(5);
72
- const l2x2 = api.value(6);
73
- const l2y2 = api.value(7);
74
-
75
- // Convert to pixel coordinates
76
- let p1Start = api.coord([l1x1, l1y1]);
77
- let p1End = api.coord([l1x2, l1y2]);
78
- let p2Start = api.coord([l2x1, l2y1]);
79
- let p2End = api.coord([l2x2, l2y2]);
80
-
81
- // Handle line extensions if lines are extended, extend the fill too
82
- const extend1 = line1.extend || 'none';
83
- const extend2 = line2.extend || 'none';
84
- if (extend1 !== 'none' || extend2 !== 'none') {
85
- const cs = params.coordSys;
86
- const left = cs.x;
87
- const right = cs.x + cs.width;
88
- const top = cs.y;
89
- const bottom = cs.y + cs.height;
90
-
91
- if (extend1 !== 'none') {
92
- [p1Start, p1End] = this.extendLine(p1Start, p1End, extend1, left, right, top, bottom);
93
- }
94
- if (extend2 !== 'none') {
95
- [p2Start, p2End] = this.extendLine(p2Start, p2End, extend2, left, right, top, bottom);
63
+ const children: any[] = [];
64
+
65
+ for (const lf of fillObjects) {
66
+ if (lf._deleted) continue;
67
+ const line1 = lf.line1;
68
+ const line2 = lf.line2;
69
+ if (!line1 || !line2 || line1._deleted || line2._deleted) continue;
70
+
71
+ const xOff1 = (line1.xloc === 'bar_index' || line1.xloc === 'bi') ? offset : 0;
72
+ const xOff2 = (line2.xloc === 'bar_index' || line2.xloc === 'bi') ? offset : 0;
73
+
74
+ let p1Start = api.coord([line1.x1 + xOff1, line1.y1]);
75
+ let p1End = api.coord([line1.x2 + xOff1, line1.y2]);
76
+ let p2Start = api.coord([line2.x1 + xOff2, line2.y1]);
77
+ let p2End = api.coord([line2.x2 + xOff2, line2.y2]);
78
+
79
+ // Handle line extensions
80
+ const extend1 = line1.extend || 'none';
81
+ const extend2 = line2.extend || 'none';
82
+ if (extend1 !== 'none' || extend2 !== 'none') {
83
+ const cs = params.coordSys;
84
+ const csLeft = cs.x, csRight = cs.x + cs.width;
85
+ const csTop = cs.y, csBottom = cs.y + cs.height;
86
+ if (extend1 !== 'none') {
87
+ [p1Start, p1End] = this.extendLine(p1Start, p1End, extend1, csLeft, csRight, csTop, csBottom);
88
+ }
89
+ if (extend2 !== 'none') {
90
+ [p2Start, p2End] = this.extendLine(p2Start, p2End, extend2, csLeft, csRight, csTop, csBottom);
91
+ }
96
92
  }
93
+
94
+ const { color: fillColor, opacity: fillOpacity } = ColorUtils.parseColor(lf.color || 'rgba(128, 128, 128, 0.2)');
95
+
96
+ children.push({
97
+ type: 'polygon',
98
+ shape: { points: [p1Start, p1End, p2End, p2Start] },
99
+ style: { fill: fillColor, opacity: fillOpacity },
100
+ silent: true,
101
+ });
97
102
  }
98
103
 
99
- // Parse color
100
- const { color: fillColor, opacity: fillOpacity } = ColorUtils.parseColor(lf.color || 'rgba(128, 128, 128, 0.2)');
101
-
102
- // Create a polygon: line1.start → line1.end → line2.end → line2.start
103
- return {
104
- type: 'polygon',
105
- shape: {
106
- points: [
107
- p1Start,
108
- p1End,
109
- p2End,
110
- p2Start,
111
- ],
112
- },
113
- style: {
114
- fill: fillColor,
115
- opacity: fillOpacity,
116
- },
117
- silent: true,
118
- };
104
+ return { type: 'group', children };
119
105
  },
120
- data: fillData,
106
+ data: [[0, lastBarIndex, yMin, yMax]],
107
+ clip: true,
108
+ encode: { x: [0, 1], y: [2, 3] },
121
109
  z: 10, // Behind lines (z=15) but above other elements
122
110
  silent: true,
123
111
  emphasis: { disabled: true },
@@ -0,0 +1,197 @@
1
+ import { SeriesRenderer, RenderContext } from './SeriesRenderer';
2
+ import { ColorUtils } from '../../utils/ColorUtils';
3
+
4
+ /**
5
+ * Renderer for Pine Script polyline.* drawing objects.
6
+ * Each polyline is defined by an array of chart.point objects, connected
7
+ * sequentially with straight or curved segments, optionally closed and filled.
8
+ *
9
+ * Style name: 'drawing_polyline'
10
+ */
11
+ export class PolylineRenderer implements SeriesRenderer {
12
+ render(context: RenderContext): any {
13
+ const { seriesName, xAxisIndex, yAxisIndex, dataArray, dataIndexOffset } = context;
14
+ const offset = dataIndexOffset || 0;
15
+
16
+ // Collect all non-deleted polyline objects from the sparse dataArray.
17
+ // Same aggregation pattern as DrawingLineRenderer — objects are stored
18
+ // as an array in a single data entry.
19
+ const polyObjects: any[] = [];
20
+
21
+ for (let i = 0; i < dataArray.length; i++) {
22
+ const val = dataArray[i];
23
+ if (!val) continue;
24
+
25
+ const items = Array.isArray(val) ? val : [val];
26
+ for (const pl of items) {
27
+ if (pl && typeof pl === 'object' && !pl._deleted && pl.points && pl.points.length >= 2) {
28
+ polyObjects.push(pl);
29
+ }
30
+ }
31
+ }
32
+
33
+ if (polyObjects.length === 0) {
34
+ return { name: seriesName, type: 'custom', xAxisIndex, yAxisIndex, data: [], silent: true };
35
+ }
36
+
37
+ // Compute y-range across all polylines for axis scaling
38
+ let yMin = Infinity, yMax = -Infinity;
39
+ for (const pl of polyObjects) {
40
+ for (const pt of pl.points) {
41
+ const p = pt.price ?? 0;
42
+ if (p < yMin) yMin = p;
43
+ if (p > yMax) yMax = p;
44
+ }
45
+ }
46
+
47
+ // Use a SINGLE data entry spanning the full x-range so renderItem is always called.
48
+ // ECharts filters a data item only when ALL its x-dimensions are on the same side
49
+ // of the visible window. With dims 0=0 and 1=lastBar the item always straddles
50
+ // the viewport, so renderItem fires exactly once regardless of scroll position.
51
+ // Dims 2/3 are yMin/yMax for axis scaling.
52
+ const totalBars = (context.candlestickData?.length || 0) + offset;
53
+ const lastBarIndex = Math.max(0, totalBars - 1);
54
+
55
+ return {
56
+ name: seriesName,
57
+ type: 'custom',
58
+ xAxisIndex,
59
+ yAxisIndex,
60
+ renderItem: (params: any, api: any) => {
61
+ const children: any[] = [];
62
+
63
+ for (const pl of polyObjects) {
64
+ if (pl._deleted) continue;
65
+ const points = pl.points;
66
+ if (!points || points.length < 2) continue;
67
+
68
+ const useBi = pl.xloc === 'bi' || pl.xloc === 'bar_index';
69
+ const xOff = useBi ? offset : 0;
70
+
71
+ // Convert chart.point objects to pixel coordinates
72
+ const pixelPoints: number[][] = [];
73
+ for (const pt of points) {
74
+ const x = useBi ? (pt.index ?? 0) + xOff : (pt.time ?? 0);
75
+ const y = pt.price ?? 0;
76
+ pixelPoints.push(api.coord([x, y]));
77
+ }
78
+
79
+ if (pixelPoints.length < 2) continue;
80
+
81
+ const lineColor = pl.line_color || '#2962ff';
82
+ const lineWidth = pl.line_width || 1;
83
+ const dashPattern = this.getDashPattern(pl.line_style);
84
+
85
+ // Fill shape (rendered behind stroke)
86
+ if (pl.fill_color && pl.fill_color !== '' && pl.fill_color !== 'na') {
87
+ const { color: fillColor, opacity: fillOpacity } = ColorUtils.parseColor(pl.fill_color);
88
+
89
+ if (pl.curved) {
90
+ const pathData = this.buildCurvedPath(pixelPoints, pl.closed);
91
+ children.push({
92
+ type: 'path',
93
+ shape: { pathData: pathData + ' Z' },
94
+ style: { fill: fillColor, opacity: fillOpacity, stroke: 'none' },
95
+ silent: true,
96
+ });
97
+ } else {
98
+ children.push({
99
+ type: 'polygon',
100
+ shape: { points: pixelPoints },
101
+ style: { fill: fillColor, opacity: fillOpacity, stroke: 'none' },
102
+ silent: true,
103
+ });
104
+ }
105
+ }
106
+
107
+ // Stroke (line segments)
108
+ if (pl.curved) {
109
+ const pathData = this.buildCurvedPath(pixelPoints, pl.closed);
110
+ children.push({
111
+ type: 'path',
112
+ shape: { pathData },
113
+ style: { fill: 'none', stroke: lineColor, lineWidth, lineDash: dashPattern },
114
+ silent: true,
115
+ });
116
+ } else {
117
+ const allPoints = pl.closed ? [...pixelPoints, pixelPoints[0]] : pixelPoints;
118
+ children.push({
119
+ type: 'polyline',
120
+ shape: { points: allPoints },
121
+ style: { fill: 'none', stroke: lineColor, lineWidth, lineDash: dashPattern },
122
+ silent: true,
123
+ });
124
+ }
125
+ }
126
+
127
+ return { type: 'group', children };
128
+ },
129
+ data: [[0, lastBarIndex, yMin, yMax]],
130
+ clip: true,
131
+ encode: { x: [0, 1], y: [2, 3] },
132
+ z: 12,
133
+ silent: true,
134
+ emphasis: { disabled: true },
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Build an SVG path string for a smooth curve through all points
140
+ * using Catmull-Rom → cubic bezier conversion.
141
+ */
142
+ private buildCurvedPath(points: number[][], closed: boolean): string {
143
+ const n = points.length;
144
+ if (n < 2) return '';
145
+ if (n === 2) {
146
+ return `M ${points[0][0]} ${points[0][1]} L ${points[1][0]} ${points[1][1]}`;
147
+ }
148
+
149
+ // Catmull-Rom tension (0.5 = centripetal)
150
+ const tension = 0.5;
151
+ let path = `M ${points[0][0]} ${points[0][1]}`;
152
+
153
+ // For closed curves, wrap around; for open, duplicate first/last
154
+ const getPoint = (i: number): number[] => {
155
+ if (closed) {
156
+ return points[((i % n) + n) % n];
157
+ }
158
+ if (i < 0) return points[0];
159
+ if (i >= n) return points[n - 1];
160
+ return points[i];
161
+ };
162
+
163
+ const segmentCount = closed ? n : n - 1;
164
+
165
+ for (let i = 0; i < segmentCount; i++) {
166
+ const p0 = getPoint(i - 1);
167
+ const p1 = getPoint(i);
168
+ const p2 = getPoint(i + 1);
169
+ const p3 = getPoint(i + 2);
170
+
171
+ // Convert Catmull-Rom to cubic bezier control points
172
+ const cp1x = p1[0] + (p2[0] - p0[0]) * tension / 3;
173
+ const cp1y = p1[1] + (p2[1] - p0[1]) * tension / 3;
174
+ const cp2x = p2[0] - (p3[0] - p1[0]) * tension / 3;
175
+ const cp2y = p2[1] - (p3[1] - p1[1]) * tension / 3;
176
+
177
+ path += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${p2[0]} ${p2[1]}`;
178
+ }
179
+
180
+ if (closed) {
181
+ path += ' Z';
182
+ }
183
+
184
+ return path;
185
+ }
186
+
187
+ private getDashPattern(style: string): number[] | undefined {
188
+ switch (style) {
189
+ case 'style_dotted':
190
+ return [2, 2];
191
+ case 'style_dashed':
192
+ return [6, 4];
193
+ default:
194
+ return undefined;
195
+ }
196
+ }
197
+ }