@stellar-expert/ui-framework 1.19.1 → 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.
- package/charts/axis/axis.js +470 -0
- package/charts/axis/tick-positioner.js +172 -0
- package/charts/chart-default-options.js +158 -0
- package/charts/chart-loader.js +26 -0
- package/charts/chart-options-builder.js +81 -0
- package/charts/chart.js +99 -0
- package/charts/chart.scss +80 -0
- package/charts/core/animate.js +99 -0
- package/charts/core/chart.js +555 -0
- package/charts/core/color.js +89 -0
- package/charts/core/options.js +40 -0
- package/charts/core/svg-renderer.js +155 -0
- package/charts/core/time.js +129 -0
- package/charts/core/utilities.js +92 -0
- package/charts/globals.js +32 -0
- package/charts/index.js +4 -0
- package/charts/interaction/legend.js +125 -0
- package/charts/interaction/tooltip.js +185 -0
- package/charts/pie/pie-chart.js +269 -0
- package/charts/polar/polar-chart.js +99 -0
- package/charts/series/area-series.js +57 -0
- package/charts/series/candlestick-series.js +60 -0
- package/charts/series/column-series.js +55 -0
- package/charts/series/index.js +21 -0
- package/charts/series/line-series.js +21 -0
- package/charts/series/series.js +161 -0
- package/charts/stock/data-grouping.js +141 -0
- package/charts/stock/navigator.js +205 -0
- package/charts/stock/range-selector.js +140 -0
- package/effect/effect-description.js +1 -1
- package/index.d.ts +36 -0
- package/index.js +2 -0
- package/interaction/dialog.js +10 -4
- package/interaction/dialog.scss +13 -1
- package/package.json +5 -2
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
//StellarExpert explorer chart theme — colors, fonts and per-type defaults applied on top of the
|
|
2
|
+
//engine baseline via setOptions, plus the custom logarithmic axis scaling used by balance/distribution charts.
|
|
3
|
+
import ChartEngine from './globals'
|
|
4
|
+
|
|
5
|
+
//allow negative values on the logarithmic axis
|
|
6
|
+
ChartEngine.Axis.prototype.allowNegativeLog = true
|
|
7
|
+
|
|
8
|
+
//override conversions (custom log scaling used by account-balance / distribution charts)
|
|
9
|
+
ChartEngine.Axis.prototype.log2lin = function (num) {
|
|
10
|
+
let adjustedNum = Math.abs(num)
|
|
11
|
+
if (adjustedNum < 10) {
|
|
12
|
+
adjustedNum += (10 - adjustedNum) / 10
|
|
13
|
+
}
|
|
14
|
+
const result = Math.log(adjustedNum) / Math.LN10
|
|
15
|
+
return num < 0 ? -result : result
|
|
16
|
+
}
|
|
17
|
+
ChartEngine.Axis.prototype.lin2log = function (num) {
|
|
18
|
+
let result = Math.pow(10, Math.abs(num))
|
|
19
|
+
if (result < 10) {
|
|
20
|
+
result = (10 * (result - 1)) / (10 - 1)
|
|
21
|
+
}
|
|
22
|
+
return num < 0 ? -result : result
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const defaultTextStyle = {
|
|
26
|
+
fontSize: '12px',
|
|
27
|
+
fontFamily: 'Roboto Condensed,sans-serif',
|
|
28
|
+
fontWeight: 400,
|
|
29
|
+
color: 'var(--color-text)'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const primaryColor = '#08B5E5'
|
|
33
|
+
|
|
34
|
+
const theme = ChartEngine.theme = {
|
|
35
|
+
colors: ['hsl(193,93%,46%)', 'hsl(27,93%,66%)', 'hsl(130,60%,46%)', 'hsl(290,40%,46%)',
|
|
36
|
+
'hsl(180,93%,46%)', 'hsl(13,40%,46%)', 'hsl(115,40%,46%)', 'hsl(270,40%,66%)',
|
|
37
|
+
'hsl(155,60%,46%)', 'hsl(0,40%,66%)', 'hsl(100,43%,66%)', 'hsl(250,50%,66%)',
|
|
38
|
+
'hsl(143,40%,66%)', 'hsl(340,40%,46%)', 'hsl(212,40%,46%)', 'hsl(235,40%,46%)',
|
|
39
|
+
'hsl(135,60%,40%)', 'hsl(320,50%,66%)'
|
|
40
|
+
],
|
|
41
|
+
title: {
|
|
42
|
+
text: '',
|
|
43
|
+
style: {...defaultTextStyle, fontSize: '17px'}
|
|
44
|
+
},
|
|
45
|
+
chart: {
|
|
46
|
+
backgroundColor: null,
|
|
47
|
+
zoomType: 'x',
|
|
48
|
+
spacing: [2, 2, 2, 2],
|
|
49
|
+
style: {
|
|
50
|
+
fontFamily: 'sans-serif'
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
scrollbar: {
|
|
54
|
+
enabled: false
|
|
55
|
+
},
|
|
56
|
+
tooltip: {
|
|
57
|
+
shared: true,
|
|
58
|
+
split: false,
|
|
59
|
+
borderWidth: 0,
|
|
60
|
+
headerFormat: '<span>{point.key}</span><br/>',
|
|
61
|
+
shadow: false
|
|
62
|
+
},
|
|
63
|
+
legend: {
|
|
64
|
+
itemStyle: defaultTextStyle,
|
|
65
|
+
itemHoverStyle: {color: 'var(--color-highlight)'},
|
|
66
|
+
itemDisabledStyle: {color: 'var(--color-dimmed)'}
|
|
67
|
+
},
|
|
68
|
+
navigator: {
|
|
69
|
+
enabled: false
|
|
70
|
+
},
|
|
71
|
+
credits: {
|
|
72
|
+
enabled: false
|
|
73
|
+
},
|
|
74
|
+
navigation: {
|
|
75
|
+
buttonOptions: {
|
|
76
|
+
symbolFill: 'var(--color-border-shadow)'
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
xAxis: {
|
|
80
|
+
type: 'datetime',
|
|
81
|
+
crosshair: true,
|
|
82
|
+
ordinal: false,
|
|
83
|
+
minPadding: 0,
|
|
84
|
+
maxPadding: 0,
|
|
85
|
+
gridLineWidth: 1,
|
|
86
|
+
lineColor: 'var(--color-border-shadow)',
|
|
87
|
+
gridLineColor: 'var(--color-border-shadow)',
|
|
88
|
+
minorGridLineColor: 'var(--color-border-shadow)',
|
|
89
|
+
minorTickColor: 'var(--color-border-shadow)',
|
|
90
|
+
tickColor: 'var(--color-border-shadow)',
|
|
91
|
+
title: {
|
|
92
|
+
style: defaultTextStyle,
|
|
93
|
+
margin: 4
|
|
94
|
+
},
|
|
95
|
+
labels: {
|
|
96
|
+
style: defaultTextStyle
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
yAxis: {
|
|
100
|
+
minorTickInterval: 'auto',
|
|
101
|
+
lineColor: 'var(--color-border-shadow)',
|
|
102
|
+
gridLineColor: 'var(--color-border-shadow)',
|
|
103
|
+
minorGridLineColor: 'var(--color-border-shadow)',
|
|
104
|
+
minorTickColor: 'var(--color-border-shadow)',
|
|
105
|
+
tickColor: 'var(--color-border-shadow)',
|
|
106
|
+
title: {
|
|
107
|
+
style: defaultTextStyle
|
|
108
|
+
},
|
|
109
|
+
labels: {
|
|
110
|
+
style: defaultTextStyle
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
plotOptions: {
|
|
114
|
+
candlestick: {
|
|
115
|
+
upColor: 'var(--color-price-up)',
|
|
116
|
+
color: 'var(--color-price-down)',
|
|
117
|
+
lineColor: 'var(--color-dimmed)',
|
|
118
|
+
lineWidth: 0.6
|
|
119
|
+
},
|
|
120
|
+
area: {
|
|
121
|
+
fillColor: {
|
|
122
|
+
linearGradient: {
|
|
123
|
+
x1: 0,
|
|
124
|
+
y1: 0,
|
|
125
|
+
x2: 0,
|
|
126
|
+
y2: 1
|
|
127
|
+
},
|
|
128
|
+
stops: [
|
|
129
|
+
[0, ChartEngine.Color(primaryColor).setOpacity(0.8).get('rgba')],
|
|
130
|
+
[1, ChartEngine.Color(primaryColor).setOpacity(0).get('rgba')]
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
lineWidth: 2,
|
|
134
|
+
states: {
|
|
135
|
+
hover: {
|
|
136
|
+
lineWidth: 2
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
series: {
|
|
141
|
+
pointPadding: 0.1,
|
|
142
|
+
groupPadding: 0.1
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
time: {
|
|
146
|
+
useUTC: true
|
|
147
|
+
},
|
|
148
|
+
background2: '#F0F0EA',
|
|
149
|
+
lang: {
|
|
150
|
+
decimalPoint: '.',
|
|
151
|
+
thousandsSep: ','
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
//apply the theme
|
|
156
|
+
ChartEngine.setOptions(theme)
|
|
157
|
+
|
|
158
|
+
export {ChartEngine}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import cn from 'classnames'
|
|
3
|
+
|
|
4
|
+
const placeholderBars = [30, 60, 42, 40, 70, 62, 80, 83]
|
|
5
|
+
|
|
6
|
+
export function ChartLoader({title, unavailable = false, container}) {
|
|
7
|
+
return <div className={cn('chart', container !== undefined ? container : 'segment blank')}>
|
|
8
|
+
{!!title && <>
|
|
9
|
+
<h3>{title}</h3>
|
|
10
|
+
<hr className="flare"/>
|
|
11
|
+
</>}
|
|
12
|
+
<div className="chart-placeholder">
|
|
13
|
+
{placeholderBars.map(v => <div key={v} style={{height: v + '%'}}/>)}
|
|
14
|
+
</div>
|
|
15
|
+
<div className="v-center-block">
|
|
16
|
+
{unavailable ?
|
|
17
|
+
<div className="text-center dimmed">
|
|
18
|
+
<span className="icon icon-minus-circle"/> Data unavailable
|
|
19
|
+
</div> :
|
|
20
|
+
<div className="text-center">
|
|
21
|
+
<div className="loader"/>
|
|
22
|
+
<div className="text-small dimmed text-center" style={{marginTop: '-2em'}}>Loading chart data...</div>
|
|
23
|
+
</div>}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//Normalizes per-chart options before they reach the engine: applies grouping/range/legend flags and
|
|
2
|
+
//registers optional engine modules. Uses the engine's own merge (no deepmerge dependency).
|
|
3
|
+
import {merge} from './core/utilities'
|
|
4
|
+
import {ChartEngine} from './chart-default-options'
|
|
5
|
+
|
|
6
|
+
const rangeSelectorOptions = {
|
|
7
|
+
//the in-house range selector renders span controls styled via chart.scss (.chart-zoom-btn);
|
|
8
|
+
//only the button definitions are consumed by the engine
|
|
9
|
+
buttons: [
|
|
10
|
+
{type: 'month', count: 1, text: '1m'},
|
|
11
|
+
{type: 'month', count: 3, text: '3m'},
|
|
12
|
+
{type: 'month', count: 6, text: '6m'},
|
|
13
|
+
{type: 'year', count: 1, text: '1y'},
|
|
14
|
+
{type: 'all', text: 'All'}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const groupingUnits = [
|
|
19
|
+
['day', [1, 3]],
|
|
20
|
+
['week', [1, 2]],
|
|
21
|
+
['month', [1, 2, 6]]
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const groupedPlotOptions = {
|
|
25
|
+
series: {
|
|
26
|
+
dataGrouping: {
|
|
27
|
+
units: groupingUnits,
|
|
28
|
+
//17 (not 16) so the bucket interval clears the weekly/bi-weekly rounding boundary at the 1y
|
|
29
|
+
//view — keeps the 1y window on monthly buckets instead of tipping to weekly
|
|
30
|
+
groupPixelWidth: 17
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const activeModules = new Set()
|
|
36
|
+
|
|
37
|
+
function extendWithModules(modules) {
|
|
38
|
+
if (!modules)
|
|
39
|
+
return
|
|
40
|
+
for (const module of modules)
|
|
41
|
+
if (!activeModules.has(module)) {
|
|
42
|
+
module(ChartEngine)
|
|
43
|
+
activeModules.add(module)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function prepareChartOptions(externalOptions, {modules, noLegend, grouped, range, inline}) {
|
|
48
|
+
extendWithModules(modules)
|
|
49
|
+
const extraOptions = {}
|
|
50
|
+
extraOptions.legend = {enabled: !noLegend}
|
|
51
|
+
//inline sparklines have no axis margins, so give both axes a little fractional padding: vertical keeps
|
|
52
|
+
//the line stroke off the top/bottom edges, horizontal insets the line so it isn't flush against the
|
|
53
|
+
//surrounding label/edges (deep-merged, so per-chart axis settings are preserved)
|
|
54
|
+
if (inline) {
|
|
55
|
+
extraOptions.xAxis = {minPadding: 0.1, maxPadding: 0.05}
|
|
56
|
+
extraOptions.yAxis = {minPadding: 0.12, maxPadding: 0.12}
|
|
57
|
+
}
|
|
58
|
+
if (grouped) {
|
|
59
|
+
extraOptions.plotOptions = groupedPlotOptions
|
|
60
|
+
}
|
|
61
|
+
if (range) {
|
|
62
|
+
extraOptions.rangeSelector = rangeSelectorOptions
|
|
63
|
+
if (typeof range === 'string') {
|
|
64
|
+
const now = new Date()
|
|
65
|
+
const period = new Date(now.valueOf())
|
|
66
|
+
switch (range) {
|
|
67
|
+
case 'year':
|
|
68
|
+
period.setUTCFullYear(period.getUTCFullYear() - 1)
|
|
69
|
+
extraOptions.xAxis = {range: now - period}
|
|
70
|
+
break
|
|
71
|
+
case 'month':
|
|
72
|
+
period.setUTCMonth(period.getUTCMonth() - 1)
|
|
73
|
+
extraOptions.xAxis = {range: now - period}
|
|
74
|
+
break
|
|
75
|
+
default:
|
|
76
|
+
throw new TypeError('Invalid date range for a chart: ' + range)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return merge(externalOptions, extraOptions)
|
|
81
|
+
}
|
package/charts/chart.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, {useRef} from 'react'
|
|
2
|
+
import cn from 'classnames'
|
|
3
|
+
import PropTypes from 'prop-types'
|
|
4
|
+
import {useDeepEffect} from '../state/state-hooks'
|
|
5
|
+
import {withErrorBoundary} from '../errors/error-boundary'
|
|
6
|
+
import {ChartEngine} from './chart-default-options'
|
|
7
|
+
import {prepareChartOptions} from './chart-options-builder'
|
|
8
|
+
import {ChartLoader} from './chart-loader'
|
|
9
|
+
import './chart.scss'
|
|
10
|
+
|
|
11
|
+
export {ChartEngine, ChartLoader}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Pure-JS SVG charting component (zero external dependencies)
|
|
15
|
+
* @param {Object} props
|
|
16
|
+
* @param {{}} props.options - Chart data and options
|
|
17
|
+
* @param {'Chart'|'StockChart'} [props.type] - Chart type (`StockChart` adds navigator/range-selector support)
|
|
18
|
+
* @param {String|JSX.Element} [props.title] - Chart title rendered above the plot
|
|
19
|
+
* @param {Boolean} [props.inline] - Render the chart as inline-block (for sparkline charts)
|
|
20
|
+
* @param {Boolean} [props.grouped] - Apply data grouping (downsample dense series into time buckets)
|
|
21
|
+
* @param {true|false|'year'|'month'} [props.range] - Enable the range selector / initial visible window
|
|
22
|
+
* @param {Boolean} [props.noLegend] - Hide the legend section
|
|
23
|
+
* @param {String} [props.container] - Container CSS class (defaults to `segment blank`)
|
|
24
|
+
* @param {String} [props.className] - Additional CSS classes
|
|
25
|
+
* @param {{}} [props.style] - Additional inline styles
|
|
26
|
+
* @param {Function[]} [props.modules] - Additional engine modules to register (`module(engine)`)
|
|
27
|
+
* @param {*} [props.children] - Optional content added to the chart header
|
|
28
|
+
* @constructor
|
|
29
|
+
*/
|
|
30
|
+
export default function Chart({
|
|
31
|
+
options,
|
|
32
|
+
type = 'Chart',
|
|
33
|
+
title,
|
|
34
|
+
inline,
|
|
35
|
+
grouped,
|
|
36
|
+
range,
|
|
37
|
+
noLegend,
|
|
38
|
+
container = 'segment blank',
|
|
39
|
+
className,
|
|
40
|
+
style,
|
|
41
|
+
modules,
|
|
42
|
+
children
|
|
43
|
+
}) {
|
|
44
|
+
const chart = useRef(null)
|
|
45
|
+
const chartIdRef = useRef(`ixchart${Math.floor(Math.random() * 0x10000000000000)}`)
|
|
46
|
+
|
|
47
|
+
//deep-compare deps so a re-render with a freshly-built (but equal) options object doesn't tear down
|
|
48
|
+
//and recreate the chart — only real option/type changes rebuild it
|
|
49
|
+
useDeepEffect(() => {
|
|
50
|
+
if (chart.current) {
|
|
51
|
+
chart.current.destroy()
|
|
52
|
+
chart.current = null
|
|
53
|
+
}
|
|
54
|
+
if (!options)
|
|
55
|
+
return
|
|
56
|
+
const mergedOptions = prepareChartOptions(options, {grouped, range, noLegend, modules, inline})
|
|
57
|
+
chart.current = new ChartEngine[type](chartIdRef.current, mergedOptions)
|
|
58
|
+
return () => {
|
|
59
|
+
if (chart.current) {
|
|
60
|
+
chart.current.destroy()
|
|
61
|
+
chart.current = null
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}, [options, modules, type, inline, title])
|
|
65
|
+
|
|
66
|
+
if (!options)
|
|
67
|
+
return <ChartLoader title={title}/>
|
|
68
|
+
|
|
69
|
+
const containerStyle = {...style}
|
|
70
|
+
if (inline) {
|
|
71
|
+
containerStyle.display = 'inline-block'
|
|
72
|
+
return <div id={chartIdRef.current} style={containerStyle}/>
|
|
73
|
+
}
|
|
74
|
+
return <div className={cn('chart', container, className)} style={containerStyle}>
|
|
75
|
+
{!!title && <h3>{title}</h3>}
|
|
76
|
+
{children}
|
|
77
|
+
<hr className="flare"/>
|
|
78
|
+
<div className="v-center-block">
|
|
79
|
+
<div id={chartIdRef.current}/>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
Chart.propTypes = {
|
|
85
|
+
options: PropTypes.object,
|
|
86
|
+
type: PropTypes.oneOf(['StockChart', 'Chart']),
|
|
87
|
+
title: PropTypes.any,
|
|
88
|
+
noLegend: PropTypes.bool,
|
|
89
|
+
grouped: PropTypes.bool,
|
|
90
|
+
range: PropTypes.oneOf([true, false, 'year', 'month']),
|
|
91
|
+
inline: PropTypes.bool,
|
|
92
|
+
container: PropTypes.string,
|
|
93
|
+
className: PropTypes.string,
|
|
94
|
+
style: PropTypes.object,
|
|
95
|
+
modules: PropTypes.arrayOf(PropTypes.func)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
Chart.Loader = ChartLoader
|
|
99
|
+
Chart.withErrorBoundary = withErrorBoundary
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
.chart {
|
|
2
|
+
position: relative;
|
|
3
|
+
z-index: 0;
|
|
4
|
+
min-height: 30em;
|
|
5
|
+
|
|
6
|
+
.v-center-block {
|
|
7
|
+
min-height: 26em;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
//floating zoom/range selector — self-contained styling owned by the charts lib. Controls are spans
|
|
12
|
+
//(not <button>/.button) so they never inherit the global project button look; only the overlay's
|
|
13
|
+
//horizontal offset (dynamic plotLeft) is set inline in range-selector.js
|
|
14
|
+
.chart-range-selector {
|
|
15
|
+
position: absolute;
|
|
16
|
+
top: 2px;
|
|
17
|
+
z-index: 5;
|
|
18
|
+
display: flex;
|
|
19
|
+
gap: 3px;
|
|
20
|
+
align-items: center;
|
|
21
|
+
font: 11px 'Roboto Condensed', sans-serif;
|
|
22
|
+
|
|
23
|
+
.zoom-label {
|
|
24
|
+
opacity: 0.6;
|
|
25
|
+
margin-right: 3px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.chart-zoom-btn {
|
|
29
|
+
display: inline-block;
|
|
30
|
+
border-radius: 3px;
|
|
31
|
+
padding: 2px 8px;
|
|
32
|
+
font: 12px 'Roboto Condensed', sans-serif;
|
|
33
|
+
line-height: 1.4;
|
|
34
|
+
background: rgba(176, 185, 189, 0.1);
|
|
35
|
+
color: var(--color-text);
|
|
36
|
+
cursor: pointer;
|
|
37
|
+
user-select: none;
|
|
38
|
+
|
|
39
|
+
&:hover {
|
|
40
|
+
background: rgba(176, 185, 189, 0.25);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
&.selected {
|
|
44
|
+
background: rgb(83, 87, 89);
|
|
45
|
+
font-weight: 700;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
&.disabled {
|
|
49
|
+
color: var(--color-dimmed);
|
|
50
|
+
opacity: 0.5;
|
|
51
|
+
cursor: default;
|
|
52
|
+
background: rgba(176, 185, 189, 0.1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@media (max-width: 420px) {
|
|
58
|
+
.chart-range-selector .chart-zoom-btn {
|
|
59
|
+
padding: 1px 6px;
|
|
60
|
+
font-size: 11px;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.chart-placeholder {
|
|
65
|
+
position: absolute;
|
|
66
|
+
width: 100%;
|
|
67
|
+
height: 100%;
|
|
68
|
+
top: 0;
|
|
69
|
+
left: 0;
|
|
70
|
+
opacity: 0.04;
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: flex-end;
|
|
73
|
+
padding: $space-standard;
|
|
74
|
+
|
|
75
|
+
& > * {
|
|
76
|
+
background: #000;
|
|
77
|
+
margin: $space-micro;
|
|
78
|
+
width: 100%;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
//Load/draw animations for the initial chart render (a per-series-type entrance).
|
|
2
|
+
//Only runs once, on first paint — redraws (zoom, legend toggle, resize) skip it.
|
|
3
|
+
import {uniqueId} from './utilities'
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_DURATION = 1500
|
|
6
|
+
|
|
7
|
+
//easeOutCubic — fast start, gentle settle
|
|
8
|
+
function easeOut(t) {
|
|
9
|
+
return 1 - Math.pow(1 - t, 3)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const raf = typeof requestAnimationFrame === 'function' ? requestAnimationFrame : (cb => setTimeout(() => cb(now()), 16))
|
|
13
|
+
const now = () => (typeof performance !== 'undefined' && performance.now ? performance.now() : new Date().getTime())
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Drive a 0→1 eased progress value over `duration` ms.
|
|
17
|
+
* @param {number} duration
|
|
18
|
+
* @param {(progress: number) => void} onStep called each frame with eased progress in [0,1]
|
|
19
|
+
* @param {() => void} [onDone]
|
|
20
|
+
*/
|
|
21
|
+
export function animate(duration, onStep, onDone) {
|
|
22
|
+
const start = now()
|
|
23
|
+
onStep(0)
|
|
24
|
+
function frame() {
|
|
25
|
+
let t = (now() - start) / duration
|
|
26
|
+
if (t > 1) t = 1
|
|
27
|
+
onStep(easeOut(t))
|
|
28
|
+
if (t < 1)
|
|
29
|
+
raf(frame)
|
|
30
|
+
else if (onDone)
|
|
31
|
+
onDone()
|
|
32
|
+
}
|
|
33
|
+
raf(frame)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Fill columns from the chart baseline as a single rising level. For a stacked column the whole stack
|
|
38
|
+
* grows from 0: the lower segment reaches its final size first and stops, the upper one keeps rising.
|
|
39
|
+
* @param {{el: import('./svg-renderer').SvgElement, topY: number, botY: number, baselineY: number, colTopY: number}[]} bars
|
|
40
|
+
* topY/botY = this segment's final top/bottom pixel; colTopY = the whole column's top pixel (stack total)
|
|
41
|
+
*/
|
|
42
|
+
export function animateColumns(bars, duration = DEFAULT_DURATION) {
|
|
43
|
+
if (!bars.length)
|
|
44
|
+
return
|
|
45
|
+
animate(duration, k => {
|
|
46
|
+
for (const b of bars) {
|
|
47
|
+
//the fill level rises from the baseline up to the full column top
|
|
48
|
+
const levelY = b.baselineY - (b.baselineY - b.colTopY) * k
|
|
49
|
+
//clamp it into this segment's own [topY, botY] band
|
|
50
|
+
const visibleTop = Math.min(b.botY, Math.max(b.topY, levelY))
|
|
51
|
+
b.el.attr({y: visibleTop, height: Math.max(0, b.botY - visibleTop)})
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Reveal a series group left→right by growing a clip rectangle (used for line/area "drawing").
|
|
58
|
+
* @param {import('./chart').Chart} chart
|
|
59
|
+
* @param {import('./svg-renderer').SvgElement} group
|
|
60
|
+
*/
|
|
61
|
+
export function revealLeftToRight(chart, group, duration = DEFAULT_DURATION) {
|
|
62
|
+
const r = chart.renderer
|
|
63
|
+
const id = uniqueId('anim-clip')
|
|
64
|
+
const clip = r.element('clipPath').attr('id', id)
|
|
65
|
+
const rect = r.rect(chart.plotLeft, chart.plotTop - 4, 0, chart.plotHeight + 8)
|
|
66
|
+
rect.add(clip)
|
|
67
|
+
clip.add(r.defs)
|
|
68
|
+
group.attr('clip-path', `url(#${id})`)
|
|
69
|
+
animate(duration, k => rect.attr('width', chart.plotWidth * k))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Draw a line path on left→right by animating its stroke dash offset (used by the navigator overview).
|
|
74
|
+
* @param {import('./svg-renderer').SvgElement} pathEl
|
|
75
|
+
*/
|
|
76
|
+
export function animateLineDraw(pathEl, duration = DEFAULT_DURATION) {
|
|
77
|
+
let len
|
|
78
|
+
try {
|
|
79
|
+
len = pathEl.element.getTotalLength()
|
|
80
|
+
} catch (e) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
if (!len)
|
|
84
|
+
return
|
|
85
|
+
pathEl.attr({'stroke-dasharray': len, 'stroke-dashoffset': len})
|
|
86
|
+
animate(duration, k => pathEl.attr('stroke-dashoffset', len * (1 - k)),
|
|
87
|
+
() => pathEl.attr({'stroke-dasharray': 'none', 'stroke-dashoffset': 0}))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Grow a group out from a centre point (radar/polar entrance).
|
|
92
|
+
*/
|
|
93
|
+
export function animateFromCenter(group, cx, cy, duration = DEFAULT_DURATION) {
|
|
94
|
+
animate(duration, k => {
|
|
95
|
+
const s = Math.max(0.0001, k)
|
|
96
|
+
group.attr('transform', `translate(${cx * (1 - s)} ${cy * (1 - s)}) scale(${s})`)
|
|
97
|
+
}, () => group.attr('transform', ''))
|
|
98
|
+
}
|
|
99
|
+
|