@stellar-expert/ui-framework 1.19.2 → 1.20.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.
@@ -0,0 +1,55 @@
1
+ import {Series} from './series'
2
+ import {pick, isNumber, clamp} from '../core/utilities'
3
+ import {animateColumns} from '../core/animate'
4
+
5
+ export class ColumnSeries extends Series {
6
+ render(group) {
7
+ this.group = group
8
+ const bars = []
9
+ const renderer = this.chart.renderer
10
+ const color = this.getColor()
11
+ const xAxis = this.xAxis
12
+ const yAxis = this.yAxis
13
+ const colOpts = this.chart.options.plotOptions.column || {}
14
+ const groupPadding = pick(colOpts.groupPadding, this.chart.options.plotOptions.series.groupPadding, 0.1)
15
+ const pointPadding = pick(colOpts.pointPadding, this.chart.options.plotOptions.series.pointPadding, 0.1)
16
+ const count = this.columnCount || 1
17
+ const idx = this.columnIndex || 0
18
+ const pointRange = this.chart.pointRange || ((xAxis.max - xAxis.min) / 50)
19
+
20
+ //baseline (threshold) pixel for un-stacked columns
21
+ const tVal = isNumber(this.threshold) ? this.threshold : yAxis.min
22
+ const baselineY = yAxis.toPixels(clamp(tVal, yAxis.min, yAxis.max))
23
+
24
+ const onCategory = !!xAxis.categories
25
+ for (const p of this.plotPoints) {
26
+ if (!isNumber(p.y))
27
+ continue
28
+ //column centered on the point x (slot spans [x-pointRange/2, x+pointRange/2]) so columns
29
+ //line up with the line/area plotted at x and with the axis tick at x
30
+ const center = xAxis.toPixels(p.x)
31
+ const categoryWidth = Math.abs(xAxis.toPixels(p.x + (onCategory ? 1 : pointRange)) - xAxis.toPixels(p.x))
32
+ const groupWidth = categoryWidth * (1 - 2 * groupPadding)
33
+ const offsetWidth = groupWidth / count
34
+ const colWidth = Math.max(1, offsetWidth * (1 - 2 * pointPadding))
35
+ const groupLeft = center - groupWidth / 2
36
+ const colX = groupLeft + idx * offsetWidth + (offsetWidth - colWidth) / 2
37
+
38
+ const top = isNumber(p.stackY) ? yAxis.toPixels(p.stackY) : yAxis.toPixels(p.y)
39
+ const bottom = isNumber(p.stackLow) ? yAxis.toPixels(p.stackLow) : baselineY
40
+ const y = Math.min(top, bottom)
41
+ const h = Math.max(0, Math.abs(bottom - top))
42
+ const rect = renderer.rect(colX, y, colWidth, h, {})
43
+ .css({fill: color})
44
+ .add(group)
45
+ //colTopY = pixel of the WHOLE column's top (stack total) so a stacked column fills as one unit.
46
+ //toPixels already applies the (log) transform — don't clamp the data value against the axis'
47
+ //internal min/max, which are in transformed space on a log axis
48
+ const total = isNumber(p.stackTotal) ? p.stackTotal : p.y
49
+ const colTopY = yAxis.toPixels(total)
50
+ bars.push({el: rect, topY: y, botY: y + h, baselineY, colTopY})
51
+ }
52
+ if (this.chart.animateThisRender)
53
+ animateColumns(bars)
54
+ }
55
+ }
@@ -0,0 +1,21 @@
1
+ import {LineSeries} from './line-series'
2
+ import {AreaSeries} from './area-series'
3
+ import {ColumnSeries} from './column-series'
4
+ import {CandlestickSeries} from './candlestick-series'
5
+
6
+ //series type registry — extended in later phases (spline-smoothing, candlestick, variablepie, ...)
7
+ export const seriesTypes = {
8
+ line: LineSeries,
9
+ spline: LineSeries,
10
+ area: AreaSeries,
11
+ areaspline: AreaSeries,
12
+ column: ColumnSeries,
13
+ bar: ColumnSeries,
14
+ candlestick: CandlestickSeries,
15
+ ohlc: CandlestickSeries
16
+ }
17
+
18
+ export function createSeries(chart, options) {
19
+ const Ctor = seriesTypes[options.type] || LineSeries
20
+ return new Ctor(chart, options)
21
+ }
@@ -0,0 +1,21 @@
1
+ import {pick} from '../core/utilities'
2
+ import {revealLeftToRight} from '../core/animate'
3
+ import {Series} from './series'
4
+
5
+ export class LineSeries extends Series {
6
+ render(group) {
7
+ this.group = group
8
+ const renderer = this.chart.renderer
9
+ const color = this.getColor()
10
+ const lineWidth = pick(this.options.lineWidth,
11
+ this.chart.options.plotOptions.series.lineWidth, 2)
12
+ const d = this.linePath()
13
+ if (d) {
14
+ this.graph = renderer.path(d, {'stroke-width': lineWidth, fill: 'none', 'stroke-linejoin': 'round', 'stroke-linecap': 'round'})
15
+ .css({stroke: color})
16
+ .add(group)
17
+ if (this.chart.animateThisRender)
18
+ revealLeftToRight(this.chart, group)
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,161 @@
1
+ import {pick, isNumber} from '../core/utilities'
2
+
3
+ //Catmull-Rom -> cubic bezier smoothing for spline series
4
+ function smoothSegmentPath(pts) {
5
+ let d = `M ${pts[0].plotX} ${pts[0].plotY} `
6
+ for (let i = 0; i < pts.length - 1; i++) {
7
+ const p0 = pts[i - 1] || pts[i]
8
+ const p1 = pts[i]
9
+ const p2 = pts[i + 1]
10
+ const p3 = pts[i + 2] || pts[i + 1]
11
+ const c1x = p1.plotX + (p2.plotX - p0.plotX) / 6
12
+ const c1y = p1.plotY + (p2.plotY - p0.plotY) / 6
13
+ const c2x = p2.plotX - (p3.plotX - p1.plotX) / 6
14
+ const c2y = p2.plotY - (p3.plotY - p1.plotY) / 6
15
+ d += `C ${c1x} ${c1y} ${c2x} ${c2y} ${p2.plotX} ${p2.plotY} `
16
+ }
17
+ return d
18
+ }
19
+
20
+ export class Series {
21
+ constructor(chart, options) {
22
+ this.chart = chart
23
+ this.options = options
24
+ this.type = options.type || 'line'
25
+ this.name = options.name
26
+ this.visible = options.visible !== false
27
+ this.index = 0 //creation order, assigned by chart
28
+ //area/column rest on a zero baseline by default (the series `threshold`), pulling the axis to 0
29
+ this.threshold = options.threshold !== undefined ? options.threshold
30
+ : (/^(area|areaspline|column|bar)$/.test(this.type) ? 0 : null)
31
+ this.setData(options.data || [])
32
+ }
33
+
34
+ setData(data) {
35
+ this.xData = []
36
+ this.yData = []
37
+ this.points = []
38
+ for (const p of data) {
39
+ let x, y, point
40
+ if (Array.isArray(p)) {
41
+ if (p.length >= 5) {
42
+ //OHLC tuple [x, open, high, low, close]
43
+ x = p[0]
44
+ y = p[4]
45
+ point = {x, y, open: p[1], high: p[2], low: p[3], close: p[4]}
46
+ } else {
47
+ x = p[0]
48
+ y = p[1]
49
+ point = {x, y}
50
+ }
51
+ } else if (p && typeof p === 'object') {
52
+ x = pick(p.x, this.points.length)
53
+ y = p.y
54
+ point = {...p, x, y}
55
+ } else {
56
+ x = this.points.length
57
+ y = p
58
+ point = {x, y}
59
+ }
60
+ this.xData.push(x)
61
+ this.yData.push(y)
62
+ point.series = this
63
+ this.points.push(point)
64
+ }
65
+ //points actually drawn — replaced by the grouped set when data grouping is active
66
+ this.plotPoints = this.points
67
+ //live data update (e.g. lazy-loaded finer candles on zoom) — repaint. Skipped during the initial
68
+ //series construction, when the chart hasn't rendered yet.
69
+ if (this.chart && this.chart.hasRendered)
70
+ this.chart.redraw()
71
+ }
72
+
73
+ bindAxes() {
74
+ this.xAxis = this.chart.xAxis[0]
75
+ const yIdx = isNumber(this.options.yAxis) ? this.options.yAxis : 0
76
+ this.yAxis = this.chart.yAxis[yIdx] || this.chart.yAxis[0]
77
+ this.xAxis.series.push(this)
78
+ this.yAxis.series.push(this)
79
+ }
80
+
81
+ get colorIndex() {
82
+ return pick(this.options.colorIndex, this.index)
83
+ }
84
+
85
+ get zIndex() {
86
+ return pick(this.options.index, this.index)
87
+ }
88
+
89
+ //resolve a plot option through the option cascade: per-series → plotOptions[type] → plotOptions.series
90
+ //(e.g. `step`/`connectNulls` are commonly set once on plotOptions.series and inherited by every series)
91
+ resolveOption(name, fallback) {
92
+ const po = this.chart.options.plotOptions || {}
93
+ return pick(this.options[name], (po[this.type] || {})[name], (po.series || {})[name], fallback)
94
+ }
95
+
96
+ getColor() {
97
+ if (this.options.color)
98
+ return this.options.color
99
+ const colors = this.chart.options.colors || []
100
+ return colors[this.colorIndex % colors.length] || '#08B5E5'
101
+ }
102
+
103
+ translate() {
104
+ for (const pt of this.plotPoints) {
105
+ pt.plotX = this.xAxis.toPixels(pt.x)
106
+ pt.plotY = isNumber(pt.y) ? this.yAxis.toPixels(isNumber(pt.stackY) ? pt.stackY : pt.y) : null
107
+ pt.plotBottom = isNumber(pt.stackLow) ? this.yAxis.toPixels(pt.stackLow) : null
108
+ }
109
+ }
110
+
111
+ //build an SVG path "d" from plotted points (M/L), breaking on nulls unless connectNulls
112
+ linePath(closePathToBaseline) {
113
+ const connectNulls = this.resolveOption('connectNulls', false)
114
+ const step = this.resolveOption('step')
115
+ const segments = []
116
+ let current = []
117
+ for (const pt of this.plotPoints) {
118
+ if (pt.plotY === null) {
119
+ if (!connectNulls) {
120
+ if (current.length) segments.push(current)
121
+ current = []
122
+ continue
123
+ }
124
+ continue
125
+ }
126
+ current.push(pt)
127
+ }
128
+ if (current.length) segments.push(current)
129
+ this.segments = segments
130
+
131
+ const smooth = this.type === 'spline' || this.type === 'areaspline'
132
+ let d = ''
133
+ for (const seg of segments) {
134
+ if (smooth && seg.length > 2) {
135
+ d += smoothSegmentPath(seg)
136
+ } else {
137
+ seg.forEach((pt, i) => {
138
+ if (i === 0) {
139
+ d += `M ${pt.plotX} ${pt.plotY} `
140
+ } else if (step === 'left') {
141
+ d += `L ${pt.plotX} ${seg[i - 1].plotY} L ${pt.plotX} ${pt.plotY} `
142
+ } else if (step === 'right') {
143
+ d += `L ${seg[i - 1].plotX} ${pt.plotY} L ${pt.plotX} ${pt.plotY} `
144
+ } else {
145
+ d += `L ${pt.plotX} ${pt.plotY} `
146
+ }
147
+ })
148
+ }
149
+ }
150
+ return d.trim()
151
+ }
152
+
153
+ render() {
154
+ //overridden by subclasses
155
+ }
156
+
157
+ destroy() {
158
+ if (this.group)
159
+ this.group.destroy()
160
+ }
161
+ }
@@ -0,0 +1,141 @@
1
+ //Downsample raw series points into calendar-aligned time buckets (stock data grouping).
2
+ import {isNumber} from '../core/utilities'
3
+
4
+ const day = 24 * 3600 * 1000
5
+ const week = 7 * day
6
+
7
+ const UNIT_MS = {
8
+ millisecond: 1,
9
+ second: 1000,
10
+ minute: 60000,
11
+ hour: 3600000,
12
+ day,
13
+ week,
14
+ month: 30 * day,
15
+ year: 365 * day
16
+ }
17
+
18
+ function chooseInterval(units, approxInterval) {
19
+ //flatten the unit/multiple options into ascending candidate intervals
20
+ const candidates = []
21
+ for (const [unitName, multiples] of units)
22
+ for (const mult of multiples)
23
+ candidates.push({unitName, mult, ms: UNIT_MS[unitName] * mult})
24
+ candidates.sort((a, b) => a.ms - b.ms)
25
+ if (!candidates.length)
26
+ return {unitName: 'day', mult: 1, ms: day}
27
+ //pick the candidate NEAREST to the target (midpoint threshold) — not the first one >= target,
28
+ //which over-coarsens (e.g. picks a 6-month bucket where 2-month fits better)
29
+ for (let i = 0; i < candidates.length; i++) {
30
+ const next = candidates[i + 1]
31
+ if (!next || approxInterval <= (candidates[i].ms + next.ms) / 2)
32
+ return candidates[i]
33
+ }
34
+ return candidates[candidates.length - 1]
35
+ }
36
+
37
+ function bucketStart(x, unitName, mult) {
38
+ if (unitName === 'month') {
39
+ const d = new Date(x)
40
+ const m = Math.floor(d.getUTCMonth() / mult) * mult
41
+ return Date.UTC(d.getUTCFullYear(), m, 1)
42
+ }
43
+ if (unitName === 'year') {
44
+ const d = new Date(x)
45
+ const y = Math.floor(d.getUTCFullYear() / mult) * mult
46
+ return Date.UTC(y, 0, 1)
47
+ }
48
+ const ms = UNIT_MS[unitName] * mult
49
+ if (unitName === 'week') {
50
+ //epoch (1970-01-01) is a Thursday, so a plain floor would start weeks on Thursday. Weeks start
51
+ //on Monday — anchor to 1970-01-05 (the first Monday) so buckets align to week boundaries.
52
+ const MONDAY_ANCHOR = 4 * day
53
+ return Math.floor((x - MONDAY_ANCHOR) / ms) * ms + MONDAY_ANCHOR
54
+ }
55
+ return Math.floor(x / ms) * ms
56
+ }
57
+
58
+ function aggregate(values, approximation) {
59
+ if (!values.length)
60
+ return null
61
+ switch (approximation) {
62
+ case 'sum':
63
+ return values.reduce((a, b) => a + b, 0)
64
+ case 'high':
65
+ return Math.max(...values)
66
+ case 'low':
67
+ return Math.min(...values)
68
+ case 'open':
69
+ return values[0]
70
+ case 'close':
71
+ return values[values.length - 1]
72
+ case 'average':
73
+ default:
74
+ return values.reduce((a, b) => a + b, 0) / values.length
75
+ }
76
+ }
77
+
78
+ /**
79
+ * @param {{x:number,y:number}[]} rawPoints - chronological raw points
80
+ * @param {{units:Array, groupPixelWidth:number, approximation:string}} grouping
81
+ * @param {number} xMin
82
+ * @param {number} xMax
83
+ * @param {number} plotWidth
84
+ * @param {string} defaultApproximation
85
+ * @return {{x:number,y:number}[]}
86
+ */
87
+ export function groupSeriesData(rawPoints, grouping, xMin, xMax, plotWidth, defaultApproximation) {
88
+ if (!rawPoints.length)
89
+ return rawPoints
90
+ const groupPixelWidth = grouping.groupPixelWidth || 16
91
+ const groupCount = Math.max(1, Math.floor(plotWidth / groupPixelWidth))
92
+ const approxInterval = (xMax - xMin) / groupCount
93
+ const units = grouping.units || [['millisecond', [1]], ['second', [1]], ['minute', [1]], ['hour', [1]], ['day', [1]], ['week', [1]], ['month', [1]], ['year', [1]]]
94
+ const {unitName, mult} = chooseInterval(units, approxInterval)
95
+ const approximation = grouping.approximation || defaultApproximation || 'average'
96
+
97
+ //OHLC series (candlestick/ohlc) aggregate open=first, high=max, low=min, close=last per bucket
98
+ //instead of a single value — detect it from the point shape
99
+ const isOHLC = rawPoints.some(p => isNumber(p.open) && isNumber(p.close))
100
+
101
+ const grouped = []
102
+ let curStart = null
103
+ let bucketPts = []
104
+ let bucketFirst = null
105
+
106
+ const flush = () => {
107
+ if (!bucketPts.length)
108
+ return
109
+ //carry the bucket unit/multiple so the tooltip can show the group range (e.g. "September-October 2021")
110
+ const meta = {x: curStart, series: bucketFirst.series, groupUnit: unitName, groupMult: mult}
111
+ if (isOHLC) {
112
+ const open = bucketPts[0].open
113
+ const close = bucketPts[bucketPts.length - 1].close
114
+ grouped.push({
115
+ ...meta,
116
+ open,
117
+ high: Math.max(...bucketPts.map(p => p.high)),
118
+ low: Math.min(...bucketPts.map(p => p.low)),
119
+ close,
120
+ y: close
121
+ })
122
+ } else {
123
+ grouped.push({...meta, y: aggregate(bucketPts.map(p => p.y), approximation)})
124
+ }
125
+ }
126
+
127
+ for (const p of rawPoints) {
128
+ if (isOHLC ? !isNumber(p.close) : !isNumber(p.y))
129
+ continue
130
+ const start = bucketStart(p.x, unitName, mult)
131
+ if (curStart === null || start !== curStart) {
132
+ flush()
133
+ curStart = start
134
+ bucketPts = []
135
+ bucketFirst = p
136
+ }
137
+ bucketPts.push(p)
138
+ }
139
+ flush()
140
+ return grouped
141
+ }
@@ -0,0 +1,205 @@
1
+ import {isNumber, clamp} from '../core/utilities'
2
+ import {animateLineDraw} from '../core/animate'
3
+ import {getDateTimeTickPositions, dateTimeLabelFormat, dateFormat} from '../core/time'
4
+
5
+ //Navigator look: a thin red overview line, the SELECTED window masked with a light fill,
6
+ //a light outline, and small grip handles centred vertically on each edge.
7
+ const NAV_LINE = '#df0000'
8
+ const MASK_FILL = 'rgba(102,133,194,0.3)'
9
+ const OUTLINE = 'rgba(204,204,204,0.6)'
10
+ const HANDLE_FILL = '#f2f2f2'
11
+ const HANDLE_STROKE = '#999'
12
+ const HANDLE_W = 8
13
+ const HANDLE_H = 15
14
+
15
+ //logical (pre display-padding) axis extremes — what the user actually selected
16
+ const viewMin = xAxis => isNumber(xAxis.logicalMin) ? xAxis.logicalMin : xAxis.min
17
+ const viewMax = xAxis => isNumber(xAxis.logicalMax) ? xAxis.logicalMax : xAxis.max
18
+
19
+ //Bottom navigator strip: a mini overview of the full series with a draggable selection window
20
+ //that drives the main x-axis extremes.
21
+ export class Navigator {
22
+ constructor(chart) {
23
+ this.chart = chart
24
+ }
25
+
26
+ render(group, top, height) {
27
+ const chart = this.chart
28
+ const renderer = chart.renderer
29
+ const xAxis = chart.xAxis[0]
30
+ const series = chart.series.find(s => s.points.length)
31
+ if (!series)
32
+ return
33
+ const left = chart.plotLeft
34
+ const width = chart.plotWidth
35
+ this.top = top
36
+ this.height = height
37
+ this.left = left
38
+ this.width = width
39
+
40
+ const navSeries = chart.options.navigator && chart.options.navigator.series
41
+ const pts = (navSeries && navSeries.data) ? toPoints(navSeries.data) : series.points
42
+ const dMin = pts[0].x
43
+ const dMax = pts[pts.length - 1].x
44
+ this.dataMin = dMin
45
+ this.dataMax = dMax
46
+ const xToPx = v => left + ((v - dMin) / ((dMax - dMin) || 1)) * width
47
+
48
+ let yMin = Infinity
49
+ let yMax = -Infinity
50
+ for (const p of pts) {
51
+ const v = isNumber(p.close) ? p.close : p.y
52
+ if (isNumber(v)) {
53
+ if (v < yMin) yMin = v
54
+ if (v > yMax) yMax = v
55
+ }
56
+ }
57
+ const yToPx = v => top + height - ((v - yMin) / ((yMax - yMin) || 1)) * (height - 4) - 2
58
+
59
+ //faint background
60
+ renderer.rect(left, top, width, height, {}).css({fill: 'var(--color-border-shadow)', 'fill-opacity': 0.15}).add(group)
61
+ //time breakpoints across the full overview range, so it's clear where the selected window sits
62
+ this.renderTimeAxis(group, top, height, dMin, dMax, xToPx)
63
+ //overview as a thin line (no fill)
64
+ let d = ''
65
+ let started = false
66
+ for (const p of pts) {
67
+ const v = isNumber(p.close) ? p.close : p.y
68
+ if (isNumber(v)) {
69
+ d += `${started ? 'L' : 'M'} ${xToPx(p.x).toFixed(1)} ${yToPx(v).toFixed(1)} `
70
+ started = true
71
+ }
72
+ }
73
+ const lineEl = renderer.path(d.trim(), {'stroke-width': 1, fill: 'none'}).css({stroke: NAV_LINE}).add(group)
74
+
75
+ //selection window edges (clamped into the strip) — use the LOGICAL extremes (pre display-padding)
76
+ const x0 = clamp(xToPx(viewMin(xAxis)), left, left + width)
77
+ const x1 = clamp(xToPx(viewMax(xAxis)), left, left + width)
78
+ //mask the SELECTED region with a light fill, leave the outside clear
79
+ renderer.rect(x0, top, Math.max(0, x1 - x0), height, {}).css({fill: MASK_FILL, 'pointer-events': 'none'}).add(group)
80
+ //outline around the whole strip and the selected window
81
+ renderer.rect(left, top, width, height, {}).css({fill: 'none', stroke: OUTLINE, 'stroke-width': 1, 'pointer-events': 'none'}).add(group)
82
+ renderer.rect(x0, top, Math.max(0, x1 - x0), height, {}).css({fill: 'none', stroke: OUTLINE, 'stroke-width': 1, 'pointer-events': 'none'}).add(group)
83
+
84
+ //draggable selection band (pan) — drawn under the handles so edge drags still resize
85
+ const band = renderer.rect(x0, top, Math.max(0, x1 - x0), height, {}).css({fill: 'transparent', cursor: 'grab'}).add(group)
86
+ this.bindBand(band)
87
+ //grip handles, centred vertically on each edge
88
+ const hy = top + (height - HANDLE_H) / 2
89
+ const h0 = this.drawHandle(group, x0, hy)
90
+ const h1 = this.drawHandle(group, x1, hy)
91
+ this.bindHandle(h0, 'min')
92
+ this.bindHandle(h1, 'max')
93
+
94
+ //entrance: draw the overview line left→right on the first paint
95
+ if (chart.animateThisRender)
96
+ animateLineDraw(lineEl)
97
+ }
98
+
99
+ //faint calendar gridlines spanning the full overview, with dimmed labels below the strip — the same
100
+ //datetime tick logic as the main x-axis, so the navigator reads as a miniature timeline
101
+ renderTimeAxis(group, top, height, dMin, dMax, xToPx) {
102
+ const renderer = this.chart.renderer
103
+ const targetCount = Math.max(2, Math.round(this.width / 90))
104
+ const {positions, unitName} = getDateTimeTickPositions(dMin, dMax, targetCount)
105
+ const fmt = dateTimeLabelFormat(unitName)
106
+ const gridColor = 'var(--color-border-shadow)'
107
+ const labelStyle = {
108
+ fontSize: '11px',
109
+ fontFamily: 'Roboto Condensed,sans-serif',
110
+ fill: 'var(--color-dimmed)',
111
+ color: 'var(--color-dimmed)'
112
+ }
113
+ for (const pos of positions) {
114
+ if (pos < dMin - 1e-6 || pos > dMax + 1e-6)
115
+ continue
116
+ const x = xToPx(pos)
117
+ renderer.line(x, top, x, top + height, {'stroke-width': 1}).css({stroke: gridColor, 'pointer-events': 'none'}).add(group)
118
+ renderer.label(dateFormat(fmt, pos), x, top + height + 13, {'text-anchor': 'middle'}).css(labelStyle).add(group)
119
+ }
120
+ }
121
+
122
+ //a grip handle: rounded rect with two vertical grip lines
123
+ drawHandle(group, cx, hy) {
124
+ const renderer = this.chart.renderer
125
+ const rect = renderer.rect(cx - HANDLE_W / 2, hy, HANDLE_W, HANDLE_H, {rx: 2})
126
+ .css({fill: HANDLE_FILL, stroke: HANDLE_STROKE, 'stroke-width': 1, cursor: 'ew-resize'}).add(group)
127
+ renderer.line(cx - 1.5, hy + 4, cx - 1.5, hy + HANDLE_H - 4, {'stroke-width': 1})
128
+ .css({stroke: HANDLE_STROKE, 'pointer-events': 'none'}).add(group)
129
+ renderer.line(cx + 1.5, hy + 4, cx + 1.5, hy + HANDLE_H - 4, {'stroke-width': 1})
130
+ .css({stroke: HANDLE_STROKE, 'pointer-events': 'none'}).add(group)
131
+ return rect
132
+ }
133
+
134
+ bindBand(band) {
135
+ const chart = this.chart
136
+ const xAxis = chart.xAxis[0]
137
+ band.on('mousedown', e => {
138
+ e.preventDefault()
139
+ const svg = chart.renderer.root.element
140
+ const rect = svg.getBoundingClientRect()
141
+ const scaleX = chart.chartWidth / rect.width
142
+ const startPx = (e.clientX - rect.left) * scaleX
143
+ const startMin = viewMin(xAxis)
144
+ const startMax = viewMax(xAxis)
145
+ const span = startMax - startMin
146
+ const dataSpan = this.dataMax - this.dataMin
147
+ const move = ev => {
148
+ const px = (ev.clientX - rect.left) * scaleX
149
+ const delta = ((px - startPx) / this.width) * dataSpan
150
+ let min = startMin + delta
151
+ let max = startMax + delta
152
+ if (min < this.dataMin) {
153
+ min = this.dataMin
154
+ max = min + span
155
+ }
156
+ if (max > this.dataMax) {
157
+ max = this.dataMax
158
+ min = max - span
159
+ }
160
+ xAxis.setExtremes(min, max)
161
+ }
162
+ const up = () => {
163
+ document.removeEventListener('mousemove', move)
164
+ document.removeEventListener('mouseup', up)
165
+ }
166
+ document.addEventListener('mousemove', move)
167
+ document.addEventListener('mouseup', up)
168
+ })
169
+ }
170
+
171
+ bindHandle(handle, which) {
172
+ const chart = this.chart
173
+ const xAxis = chart.xAxis[0]
174
+ handle.on('mousedown', e => {
175
+ e.preventDefault()
176
+ const svg = chart.renderer.root.element
177
+ const rect = svg.getBoundingClientRect()
178
+ const scaleX = chart.chartWidth / rect.width
179
+ const move = ev => {
180
+ const px = (ev.clientX - rect.left) * scaleX
181
+ const v = this.dataMin + ((px - this.left) / this.width) * (this.dataMax - this.dataMin)
182
+ let min = viewMin(xAxis)
183
+ let max = viewMax(xAxis)
184
+ const minSpan = (this.dataMax - this.dataMin) / 500
185
+ if (which === 'min')
186
+ min = Math.max(this.dataMin, Math.min(v, max - minSpan))
187
+ else
188
+ max = Math.min(this.dataMax, Math.max(v, min + minSpan))
189
+ xAxis.setExtremes(min, max)
190
+ }
191
+ const up = () => {
192
+ document.removeEventListener('mousemove', move)
193
+ document.removeEventListener('mouseup', up)
194
+ }
195
+ document.addEventListener('mousemove', move)
196
+ document.addEventListener('mouseup', up)
197
+ })
198
+ }
199
+ }
200
+
201
+ function toPoints(data) {
202
+ return data.map(p => Array.isArray(p)
203
+ ? {x: p[0], y: p[p.length >= 5 ? 4 : 1], close: p.length >= 5 ? p[4] : undefined}
204
+ : p)
205
+ }