mirror of
https://github.com/apache/superset.git
synced 2024-09-19 12:09:42 -04:00
feat: line chart with revised encodeable utilities (#26)
* feat: line chart * feat: implement scale extraction * refactor: no error * fix: handle null * fix: nicing and demo * fix: legend and demo * fix: remove commented code * fix: clean * fix: reviewer comments * fix: legend and series * docs: make demos tsx * fix: reactnode * fix: label angle * fix: resolve labelxxx field names * docs: try knobs * feat: improve axis * refactor: combine computelayout into axisagent * refactor: cleaner use of scale * fix: proptypes
This commit is contained in:
parent
773de699d8
commit
049b40bc80
@ -39,7 +39,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@superset-ui/build-config": "^0.0.4",
|
||||
"@superset-ui/chart": "^0.10.2",
|
||||
"@superset-ui/chart": "^0.10.8",
|
||||
"@superset-ui/color": "^0.10.0",
|
||||
"@superset-ui/connection": "^0.10.0",
|
||||
"@superset-ui/core": "^0.10.0",
|
||||
@ -47,6 +47,7 @@
|
||||
"@superset-ui/number-format": "^0.10.0",
|
||||
"@superset-ui/time-format": "^0.10.0",
|
||||
"@superset-ui/translation": "^0.10.0",
|
||||
"@types/react": "^16.8.8",
|
||||
"csstype": "^2.6.3",
|
||||
"fast-glob": "^2.2.6",
|
||||
"fs-extra": "^7.0.1",
|
||||
|
@ -35,10 +35,11 @@
|
||||
"@storybook/addon-knobs": "^4.0.2",
|
||||
"@storybook/addon-options": "^4.0.3",
|
||||
"@storybook/react": "^4.1.11",
|
||||
"@superset-ui/chart": "^0.10.2",
|
||||
"@superset-ui/chart": "^0.10.8",
|
||||
"@superset-ui/color": "^0.10.1",
|
||||
"@superset-ui/time-format": "^0.10.1",
|
||||
"@superset-ui/translation": "^0.10.0",
|
||||
"@types/react": "^16.8.8",
|
||||
"babel-loader": "^8.0.4",
|
||||
"bootstrap": "^3.3.6",
|
||||
"cache-loader": "^1.2.2",
|
||||
|
@ -1,42 +0,0 @@
|
||||
/* eslint-disable no-magic-numbers */
|
||||
import React from 'react';
|
||||
import { SuperChart } from '@superset-ui/chart';
|
||||
import data from './data';
|
||||
|
||||
export default [
|
||||
{
|
||||
renderStory: () => (
|
||||
<SuperChart
|
||||
chartType="line2"
|
||||
chartProps={{
|
||||
datasource: { verboseMap: {} },
|
||||
formData: {
|
||||
bottomMargin: 'auto',
|
||||
colorScheme: 'd3Category10',
|
||||
leftMargin: 'auto',
|
||||
lineInterpolation: 'linear',
|
||||
richTooltip: true,
|
||||
showBrush: 'auto',
|
||||
showLegend: true,
|
||||
showMarkers: false,
|
||||
vizType: 'line',
|
||||
xAxisFormat: '%Y',
|
||||
xAxisLabel: '',
|
||||
xAxisShowminmax: false,
|
||||
xTicksLayout: 'auto',
|
||||
yAxisBounds: [null, null],
|
||||
yAxisFormat: '.3s',
|
||||
yAxisLabel: '',
|
||||
yAxisShowminmax: false,
|
||||
yLogScale: false,
|
||||
},
|
||||
height: 400,
|
||||
payload: { data },
|
||||
width: 400,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
storyName: 'Basic',
|
||||
storyPath: 'preset-chart-xy|LineChartPlugin',
|
||||
},
|
||||
];
|
@ -0,0 +1,2 @@
|
||||
export const LINE_PLUGIN_TYPE = 'v2-line';
|
||||
export const LINE_PLUGIN_LEGACY_TYPE = 'v2-line/legacy';
|
@ -0,0 +1,226 @@
|
||||
/* eslint-disable sort-keys, no-magic-numbers */
|
||||
export default {
|
||||
keys: ['name', 'x', 'y'],
|
||||
values: [
|
||||
{ x: -157766400000, y: 24703, name: 'Christopher' },
|
||||
{ x: -126230400000, y: 27861, name: 'Christopher' },
|
||||
{ x: -94694400000, y: 29436, name: 'Christopher' },
|
||||
{ x: -63158400000, y: 31463, name: 'Christopher' },
|
||||
{ x: -31536000000, y: 35718, name: 'Christopher' },
|
||||
{ x: 0, y: 41758, name: 'Christopher' },
|
||||
{ x: 31536000000, y: 48172, name: 'Christopher' },
|
||||
{ x: 63072000000, y: 52092, name: 'Christopher' },
|
||||
{ x: 94694400000, y: 48217, name: 'Christopher' },
|
||||
{ x: 126230400000, y: 48476, name: 'Christopher' },
|
||||
{ x: 157766400000, y: 46438, name: 'Christopher' },
|
||||
{ x: 189302400000, y: 45086, name: 'Christopher' },
|
||||
{ x: 220924800000, y: 46610, name: 'Christopher' },
|
||||
{ x: 252460800000, y: 47107, name: 'Christopher' },
|
||||
{ x: 283996800000, y: 50514, name: 'Christopher' },
|
||||
{ x: 315532800000, y: 48969, name: 'Christopher' },
|
||||
{ x: 347155200000, y: 50108, name: 'Christopher' },
|
||||
{ x: 378691200000, y: 59055, name: 'Christopher' },
|
||||
{ x: 410227200000, y: 59188, name: 'Christopher' },
|
||||
{ x: 441763200000, y: 59859, name: 'Christopher' },
|
||||
{ x: 473385600000, y: 59516, name: 'Christopher' },
|
||||
{ x: 504921600000, y: 56633, name: 'Christopher' },
|
||||
{ x: 536457600000, y: 54466, name: 'Christopher' },
|
||||
{ x: 567993600000, y: 52996, name: 'Christopher' },
|
||||
{ x: 599616000000, y: 53205, name: 'Christopher' },
|
||||
{ x: 631152000000, y: 52322, name: 'Christopher' },
|
||||
{ x: 662688000000, y: 47109, name: 'Christopher' },
|
||||
{ x: 694224000000, y: 42470, name: 'Christopher' },
|
||||
{ x: 725846400000, y: 38257, name: 'Christopher' },
|
||||
{ x: 757382400000, y: 34823, name: 'Christopher' },
|
||||
{ x: 788918400000, y: 32728, name: 'Christopher' },
|
||||
{ x: 820454400000, y: 30988, name: 'Christopher' },
|
||||
{ x: 852076800000, y: 29179, name: 'Christopher' },
|
||||
{ x: 883612800000, y: 27083, name: 'Christopher' },
|
||||
{ x: 915148800000, y: 25700, name: 'Christopher' },
|
||||
{ x: 946684800000, y: 24959, name: 'Christopher' },
|
||||
{ x: 978307200000, y: 23180, name: 'Christopher' },
|
||||
{ x: 1009843200000, y: 21731, name: 'Christopher' },
|
||||
{ x: 1041379200000, y: 20793, name: 'Christopher' },
|
||||
{ x: 1072915200000, y: 19739, name: 'Christopher' },
|
||||
{ x: 1104537600000, y: 19190, name: 'Christopher' },
|
||||
{ x: 1136073600000, y: 19674, name: 'Christopher' },
|
||||
{ x: 1167609600000, y: 19986, name: 'Christopher' },
|
||||
{ x: 1199145600000, y: 17771, name: 'Christopher' },
|
||||
{ x: -157766400000, y: 67646, name: 'David' },
|
||||
{ x: -126230400000, y: 66207, name: 'David' },
|
||||
{ x: -94694400000, y: 66581, name: 'David' },
|
||||
{ x: -63158400000, y: 63531, name: 'David' },
|
||||
{ x: -31536000000, y: 63502, name: 'David' },
|
||||
{ x: 0, y: 61570, name: 'David' },
|
||||
{ x: 31536000000, y: 52948, name: 'David' },
|
||||
{ x: 63072000000, y: 46218, name: 'David' },
|
||||
{ x: 94694400000, y: 40968, name: 'David' },
|
||||
{ x: 126230400000, y: 41654, name: 'David' },
|
||||
{ x: 157766400000, y: 39019, name: 'David' },
|
||||
{ x: 189302400000, y: 39165, name: 'David' },
|
||||
{ x: 220924800000, y: 40407, name: 'David' },
|
||||
{ x: 252460800000, y: 40533, name: 'David' },
|
||||
{ x: 283996800000, y: 41898, name: 'David' },
|
||||
{ x: 315532800000, y: 41743, name: 'David' },
|
||||
{ x: 347155200000, y: 40486, name: 'David' },
|
||||
{ x: 378691200000, y: 40283, name: 'David' },
|
||||
{ x: 410227200000, y: 39048, name: 'David' },
|
||||
{ x: 441763200000, y: 38346, name: 'David' },
|
||||
{ x: 473385600000, y: 38395, name: 'David' },
|
||||
{ x: 504921600000, y: 37021, name: 'David' },
|
||||
{ x: 536457600000, y: 36672, name: 'David' },
|
||||
{ x: 567993600000, y: 35214, name: 'David' },
|
||||
{ x: 599616000000, y: 35139, name: 'David' },
|
||||
{ x: 631152000000, y: 33661, name: 'David' },
|
||||
{ x: 662688000000, y: 30347, name: 'David' },
|
||||
{ x: 694224000000, y: 28344, name: 'David' },
|
||||
{ x: 725846400000, y: 26947, name: 'David' },
|
||||
{ x: 757382400000, y: 24784, name: 'David' },
|
||||
{ x: 788918400000, y: 22967, name: 'David' },
|
||||
{ x: 820454400000, y: 22941, name: 'David' },
|
||||
{ x: 852076800000, y: 21824, name: 'David' },
|
||||
{ x: 883612800000, y: 20816, name: 'David' },
|
||||
{ x: 915148800000, y: 20267, name: 'David' },
|
||||
{ x: 946684800000, y: 19695, name: 'David' },
|
||||
{ x: 978307200000, y: 19281, name: 'David' },
|
||||
{ x: 1009843200000, y: 18600, name: 'David' },
|
||||
{ x: 1041379200000, y: 18557, name: 'David' },
|
||||
{ x: 1072915200000, y: 18315, name: 'David' },
|
||||
{ x: 1104537600000, y: 18017, name: 'David' },
|
||||
{ x: 1136073600000, y: 17510, name: 'David' },
|
||||
{ x: 1167609600000, y: 17400, name: 'David' },
|
||||
{ x: 1199145600000, y: 16049, name: 'David' },
|
||||
{ x: -157766400000, y: 67506, name: 'James' },
|
||||
{ x: -126230400000, y: 65036, name: 'James' },
|
||||
{ x: -94694400000, y: 61554, name: 'James' },
|
||||
{ x: -63158400000, y: 60584, name: 'James' },
|
||||
{ x: -31536000000, y: 59824, name: 'James' },
|
||||
{ x: 0, y: 61597, name: 'James' },
|
||||
{ x: 31536000000, y: 54463, name: 'James' },
|
||||
{ x: 63072000000, y: 46960, name: 'James' },
|
||||
{ x: 94694400000, y: 42782, name: 'James' },
|
||||
{ x: 126230400000, y: 41258, name: 'James' },
|
||||
{ x: 157766400000, y: 39471, name: 'James' },
|
||||
{ x: 189302400000, y: 38203, name: 'James' },
|
||||
{ x: 220924800000, y: 39916, name: 'James' },
|
||||
{ x: 252460800000, y: 39783, name: 'James' },
|
||||
{ x: 283996800000, y: 39237, name: 'James' },
|
||||
{ x: 315532800000, y: 39185, name: 'James' },
|
||||
{ x: 347155200000, y: 38176, name: 'James' },
|
||||
{ x: 378691200000, y: 38750, name: 'James' },
|
||||
{ x: 410227200000, y: 36228, name: 'James' },
|
||||
{ x: 441763200000, y: 35728, name: 'James' },
|
||||
{ x: 473385600000, y: 35750, name: 'James' },
|
||||
{ x: 504921600000, y: 33955, name: 'James' },
|
||||
{ x: 536457600000, y: 32552, name: 'James' },
|
||||
{ x: 567993600000, y: 32418, name: 'James' },
|
||||
{ x: 599616000000, y: 32658, name: 'James' },
|
||||
{ x: 631152000000, y: 32288, name: 'James' },
|
||||
{ x: 662688000000, y: 30460, name: 'James' },
|
||||
{ x: 694224000000, y: 28450, name: 'James' },
|
||||
{ x: 725846400000, y: 26193, name: 'James' },
|
||||
{ x: 757382400000, y: 24706, name: 'James' },
|
||||
{ x: 788918400000, y: 22691, name: 'James' },
|
||||
{ x: 820454400000, y: 21122, name: 'James' },
|
||||
{ x: 852076800000, y: 20368, name: 'James' },
|
||||
{ x: 883612800000, y: 19651, name: 'James' },
|
||||
{ x: 915148800000, y: 18508, name: 'James' },
|
||||
{ x: 946684800000, y: 17939, name: 'James' },
|
||||
{ x: 978307200000, y: 17023, name: 'James' },
|
||||
{ x: 1009843200000, y: 16905, name: 'James' },
|
||||
{ x: 1041379200000, y: 16832, name: 'James' },
|
||||
{ x: 1072915200000, y: 16459, name: 'James' },
|
||||
{ x: 1104537600000, y: 16046, name: 'James' },
|
||||
{ x: 1136073600000, y: 16139, name: 'James' },
|
||||
{ x: 1167609600000, y: 15821, name: 'James' },
|
||||
{ x: 1199145600000, y: 14920, name: 'James' },
|
||||
{ x: -157766400000, y: 71390, name: 'John' },
|
||||
{ x: -126230400000, y: 64858, name: 'John' },
|
||||
{ x: -94694400000, y: 61480, name: 'John' },
|
||||
{ x: -63158400000, y: 60754, name: 'John' },
|
||||
{ x: -31536000000, y: 58644, name: 'John' },
|
||||
{ x: 0, y: 58348, name: 'John' },
|
||||
{ x: 31536000000, y: 51382, name: 'John' },
|
||||
{ x: 63072000000, y: 43028, name: 'John' },
|
||||
{ x: 94694400000, y: 39061, name: 'John' },
|
||||
{ x: 126230400000, y: 37553, name: 'John' },
|
||||
{ x: 157766400000, y: 34970, name: 'John' },
|
||||
{ x: 189302400000, y: 33876, name: 'John' },
|
||||
{ x: 220924800000, y: 34103, name: 'John' },
|
||||
{ x: 252460800000, y: 33895, name: 'John' },
|
||||
{ x: 283996800000, y: 35305, name: 'John' },
|
||||
{ x: 315532800000, y: 35131, name: 'John' },
|
||||
{ x: 347155200000, y: 34761, name: 'John' },
|
||||
{ x: 378691200000, y: 34560, name: 'John' },
|
||||
{ x: 410227200000, y: 33047, name: 'John' },
|
||||
{ x: 441763200000, y: 32484, name: 'John' },
|
||||
{ x: 473385600000, y: 31397, name: 'John' },
|
||||
{ x: 504921600000, y: 30103, name: 'John' },
|
||||
{ x: 536457600000, y: 29462, name: 'John' },
|
||||
{ x: 567993600000, y: 29301, name: 'John' },
|
||||
{ x: 599616000000, y: 29751, name: 'John' },
|
||||
{ x: 631152000000, y: 29011, name: 'John' },
|
||||
{ x: 662688000000, y: 27727, name: 'John' },
|
||||
{ x: 694224000000, y: 26156, name: 'John' },
|
||||
{ x: 725846400000, y: 24918, name: 'John' },
|
||||
{ x: 757382400000, y: 24119, name: 'John' },
|
||||
{ x: 788918400000, y: 23174, name: 'John' },
|
||||
{ x: 820454400000, y: 22104, name: 'John' },
|
||||
{ x: 852076800000, y: 21330, name: 'John' },
|
||||
{ x: 883612800000, y: 20556, name: 'John' },
|
||||
{ x: 915148800000, y: 20280, name: 'John' },
|
||||
{ x: 946684800000, y: 20032, name: 'John' },
|
||||
{ x: 978307200000, y: 18839, name: 'John' },
|
||||
{ x: 1009843200000, y: 17400, name: 'John' },
|
||||
{ x: 1041379200000, y: 17170, name: 'John' },
|
||||
{ x: 1072915200000, y: 16381, name: 'John' },
|
||||
{ x: 1104537600000, y: 15692, name: 'John' },
|
||||
{ x: 1136073600000, y: 15083, name: 'John' },
|
||||
{ x: 1167609600000, y: 14348, name: 'John' },
|
||||
{ x: 1199145600000, y: 13110, name: 'John' },
|
||||
{ x: -157766400000, y: 80812, name: 'Michael' },
|
||||
{ x: -126230400000, y: 79709, name: 'Michael' },
|
||||
{ x: -94694400000, y: 82204, name: 'Michael' },
|
||||
{ x: -63158400000, y: 81785, name: 'Michael' },
|
||||
{ x: -31536000000, y: 84893, name: 'Michael' },
|
||||
{ x: 0, y: 85015, name: 'Michael' },
|
||||
{ x: 31536000000, y: 77321, name: 'Michael' },
|
||||
{ x: 63072000000, y: 71197, name: 'Michael' },
|
||||
{ x: 94694400000, y: 67598, name: 'Michael' },
|
||||
{ x: 126230400000, y: 67304, name: 'Michael' },
|
||||
{ x: 157766400000, y: 68149, name: 'Michael' },
|
||||
{ x: 189302400000, y: 66686, name: 'Michael' },
|
||||
{ x: 220924800000, y: 67344, name: 'Michael' },
|
||||
{ x: 252460800000, y: 66875, name: 'Michael' },
|
||||
{ x: 283996800000, y: 67473, name: 'Michael' },
|
||||
{ x: 315532800000, y: 68375, name: 'Michael' },
|
||||
{ x: 347155200000, y: 68467, name: 'Michael' },
|
||||
{ x: 378691200000, y: 67904, name: 'Michael' },
|
||||
{ x: 410227200000, y: 67708, name: 'Michael' },
|
||||
{ x: 441763200000, y: 67457, name: 'Michael' },
|
||||
{ x: 473385600000, y: 64667, name: 'Michael' },
|
||||
{ x: 504921600000, y: 63959, name: 'Michael' },
|
||||
{ x: 536457600000, y: 63442, name: 'Michael' },
|
||||
{ x: 567993600000, y: 63924, name: 'Michael' },
|
||||
{ x: 599616000000, y: 65233, name: 'Michael' },
|
||||
{ x: 631152000000, y: 65138, name: 'Michael' },
|
||||
{ x: 662688000000, y: 60646, name: 'Michael' },
|
||||
{ x: 694224000000, y: 54216, name: 'Michael' },
|
||||
{ x: 725846400000, y: 49443, name: 'Michael' },
|
||||
{ x: 757382400000, y: 44361, name: 'Michael' },
|
||||
{ x: 788918400000, y: 41311, name: 'Michael' },
|
||||
{ x: 820454400000, y: 38284, name: 'Michael' },
|
||||
{ x: 852076800000, y: 37459, name: 'Michael' },
|
||||
{ x: 883612800000, y: 36525, name: 'Michael' },
|
||||
{ x: 915148800000, y: 33820, name: 'Michael' },
|
||||
{ x: 946684800000, y: 31956, name: 'Michael' },
|
||||
{ x: 978307200000, y: 29612, name: 'Michael' },
|
||||
{ x: 1009843200000, y: 28156, name: 'Michael' },
|
||||
{ x: 1041379200000, y: 27031, name: 'Michael' },
|
||||
{ x: 1072915200000, y: 25418, name: 'Michael' },
|
||||
{ x: 1104537600000, y: 23678, name: 'Michael' },
|
||||
{ x: 1136073600000, y: 22498, name: 'Michael' },
|
||||
{ x: 1167609600000, y: 21805, name: 'Michael' },
|
||||
{ x: 1199145600000, y: 20271, name: 'Michael' },
|
||||
],
|
||||
};
|
@ -0,0 +1,94 @@
|
||||
/* eslint-disable sort-keys, no-magic-numbers */
|
||||
export default {
|
||||
keys: ['snapshot', 'x', 'y'],
|
||||
values: [
|
||||
{ x: -157766400000, y: 24703, snapshot: 'Last year' },
|
||||
{ x: -126230400000, y: 27861, snapshot: 'Last year' },
|
||||
{ x: -94694400000, y: 29436, snapshot: 'Last year' },
|
||||
{ x: -63158400000, y: 31463, snapshot: 'Last year' },
|
||||
{ x: -31536000000, y: 35718, snapshot: 'Last year' },
|
||||
{ x: 0, y: 41758, snapshot: 'Last year' },
|
||||
{ x: 31536000000, y: 48172, snapshot: 'Last year' },
|
||||
{ x: 63072000000, y: 52092, snapshot: 'Last year' },
|
||||
{ x: 94694400000, y: 48217, snapshot: 'Last year' },
|
||||
{ x: 126230400000, y: 48476, snapshot: 'Last year' },
|
||||
{ x: 157766400000, y: 46438, snapshot: 'Last year' },
|
||||
{ x: 189302400000, y: 45086, snapshot: 'Last year' },
|
||||
{ x: 220924800000, y: 46610, snapshot: 'Last year' },
|
||||
{ x: 252460800000, y: 47107, snapshot: 'Last year' },
|
||||
{ x: 283996800000, y: 50514, snapshot: 'Last year' },
|
||||
{ x: 315532800000, y: 48969, snapshot: 'Last year' },
|
||||
{ x: 347155200000, y: 50108, snapshot: 'Last year' },
|
||||
{ x: 378691200000, y: 59055, snapshot: 'Last year' },
|
||||
{ x: 410227200000, y: 59188, snapshot: 'Last year' },
|
||||
{ x: 441763200000, y: 59859, snapshot: 'Last year' },
|
||||
{ x: 473385600000, y: 59516, snapshot: 'Last year' },
|
||||
{ x: 504921600000, y: 56633, snapshot: 'Last year' },
|
||||
{ x: 536457600000, y: 54466, snapshot: 'Last year' },
|
||||
{ x: 567993600000, y: 52996, snapshot: 'Last year' },
|
||||
{ x: 599616000000, y: 53205, snapshot: 'Last year' },
|
||||
{ x: 631152000000, y: 52322, snapshot: 'Last year' },
|
||||
{ x: 662688000000, y: 47109, snapshot: 'Last year' },
|
||||
{ x: 694224000000, y: 42470, snapshot: 'Last year' },
|
||||
{ x: 725846400000, y: 38257, snapshot: 'Last year' },
|
||||
{ x: 757382400000, y: 34823, snapshot: 'Last year' },
|
||||
{ x: 788918400000, y: 32728, snapshot: 'Last year' },
|
||||
{ x: 820454400000, y: 30988, snapshot: 'Last year' },
|
||||
{ x: 852076800000, y: 29179, snapshot: 'Last year' },
|
||||
{ x: 883612800000, y: 27083, snapshot: 'Last year' },
|
||||
{ x: 915148800000, y: 25700, snapshot: 'Last year' },
|
||||
{ x: 946684800000, y: 24959, snapshot: 'Last year' },
|
||||
{ x: 978307200000, y: 23180, snapshot: 'Last year' },
|
||||
{ x: 1009843200000, y: 21731, snapshot: 'Last year' },
|
||||
{ x: 1041379200000, y: 20793, snapshot: 'Last year' },
|
||||
{ x: 1072915200000, y: 19739, snapshot: 'Last year' },
|
||||
{ x: 1104537600000, y: 19190, snapshot: 'Last year' },
|
||||
{ x: 1136073600000, y: 19674, snapshot: 'Last year' },
|
||||
{ x: 1167609600000, y: 19986, snapshot: 'Last year' },
|
||||
{ x: 1199145600000, y: 17771, snapshot: 'Last year' },
|
||||
{ x: -157766400000, y: 80812, snapshot: 'Current' },
|
||||
{ x: -126230400000, y: 79709, snapshot: 'Current' },
|
||||
{ x: -94694400000, y: 82204, snapshot: 'Current' },
|
||||
{ x: -63158400000, y: 81785, snapshot: 'Current' },
|
||||
{ x: -31536000000, y: 84893, snapshot: 'Current' },
|
||||
{ x: 0, y: 85015, snapshot: 'Current' },
|
||||
{ x: 31536000000, y: 77321, snapshot: 'Current' },
|
||||
{ x: 63072000000, y: 71197, snapshot: 'Current' },
|
||||
{ x: 94694400000, y: 67598, snapshot: 'Current' },
|
||||
{ x: 126230400000, y: 67304, snapshot: 'Current' },
|
||||
{ x: 157766400000, y: 68149, snapshot: 'Current' },
|
||||
{ x: 189302400000, y: 66686, snapshot: 'Current' },
|
||||
{ x: 220924800000, y: 67344, snapshot: 'Current' },
|
||||
{ x: 252460800000, y: 66875, snapshot: 'Current' },
|
||||
{ x: 283996800000, y: 67473, snapshot: 'Current' },
|
||||
{ x: 315532800000, y: 68375, snapshot: 'Current' },
|
||||
{ x: 347155200000, y: 68467, snapshot: 'Current' },
|
||||
{ x: 378691200000, y: 67904, snapshot: 'Current' },
|
||||
{ x: 410227200000, y: 67708, snapshot: 'Current' },
|
||||
{ x: 441763200000, y: 67457, snapshot: 'Current' },
|
||||
{ x: 473385600000, y: 64667, snapshot: 'Current' },
|
||||
{ x: 504921600000, y: 63959, snapshot: 'Current' },
|
||||
{ x: 536457600000, y: 63442, snapshot: 'Current' },
|
||||
{ x: 567993600000, y: 63924, snapshot: 'Current' },
|
||||
{ x: 599616000000, y: 65233, snapshot: 'Current' },
|
||||
{ x: 631152000000, y: 65138, snapshot: 'Current' },
|
||||
{ x: 662688000000, y: 60646, snapshot: 'Current' },
|
||||
{ x: 694224000000, y: 54216, snapshot: 'Current' },
|
||||
{ x: 725846400000, y: 49443, snapshot: 'Current' },
|
||||
{ x: 757382400000, y: 44361, snapshot: 'Current' },
|
||||
{ x: 788918400000, y: 41311, snapshot: 'Current' },
|
||||
{ x: 820454400000, y: 38284, snapshot: 'Current' },
|
||||
{ x: 852076800000, y: 37459, snapshot: 'Current' },
|
||||
{ x: 883612800000, y: 36525, snapshot: 'Current' },
|
||||
{ x: 915148800000, y: 33820, snapshot: 'Current' },
|
||||
{ x: 946684800000, y: 31956, snapshot: 'Current' },
|
||||
{ x: 978307200000, y: 29612, snapshot: 'Current' },
|
||||
{ x: 1009843200000, y: 28156, snapshot: 'Current' },
|
||||
{ x: 1041379200000, y: 27031, snapshot: 'Current' },
|
||||
{ x: 1072915200000, y: 25418, snapshot: 'Current' },
|
||||
{ x: 1104537600000, y: 23678, snapshot: 'Current' },
|
||||
{ x: 1136073600000, y: 22498, snapshot: 'Current' },
|
||||
{ x: 1167609600000, y: 21805, snapshot: 'Current' },
|
||||
{ x: 1199145600000, y: 20271, snapshot: 'Current' },
|
||||
],
|
||||
};
|
@ -1,8 +1,14 @@
|
||||
import { LineChartPlugin as LegacyLineChartPlugin } from '../../../../../superset-ui-preset-chart-xy/src/legacy';
|
||||
import { LineChartPlugin } from '../../../../../superset-ui-preset-chart-xy/src';
|
||||
import Stories from './Stories';
|
||||
import BasicStories from './stories/basic';
|
||||
import LegacyStories from './stories/legacy';
|
||||
import MissingStories from './stories/missing';
|
||||
import TimeShiftStories from './stories/timeShift';
|
||||
import { LINE_PLUGIN_TYPE, LINE_PLUGIN_LEGACY_TYPE } from './constants';
|
||||
|
||||
new LineChartPlugin().configure({ key: 'line2' }).register();
|
||||
new LegacyLineChartPlugin().configure({ key: LINE_PLUGIN_LEGACY_TYPE }).register();
|
||||
new LineChartPlugin().configure({ key: LINE_PLUGIN_TYPE }).register();
|
||||
|
||||
export default {
|
||||
examples: [...Stories],
|
||||
examples: [...BasicStories, ...MissingStories, ...TimeShiftStories, ...LegacyStories],
|
||||
};
|
||||
|
@ -0,0 +1,59 @@
|
||||
/* eslint-disable no-magic-numbers, sort-keys */
|
||||
import * as React from 'react';
|
||||
import { SuperChart, ChartProps } from '@superset-ui/chart';
|
||||
import { radios } from '@storybook/addon-knobs';
|
||||
import data from '../data/data';
|
||||
import { LINE_PLUGIN_TYPE } from '../constants';
|
||||
|
||||
export default [
|
||||
{
|
||||
renderStory: () => [
|
||||
<SuperChart
|
||||
key="line1"
|
||||
chartType={LINE_PLUGIN_TYPE}
|
||||
chartProps={
|
||||
new ChartProps({
|
||||
datasource: { verboseMap: {} },
|
||||
formData: {
|
||||
encoding: {
|
||||
x: {
|
||||
field: 'x',
|
||||
type: 'temporal',
|
||||
format: '%Y',
|
||||
scale: {
|
||||
type: 'time',
|
||||
},
|
||||
axis: {
|
||||
orient: radios('x.axis.orient', ['top', 'bottom'], 'bottom'),
|
||||
title: 'Time',
|
||||
},
|
||||
},
|
||||
y: {
|
||||
field: 'y',
|
||||
type: 'quantitative',
|
||||
scale: {
|
||||
type: 'linear',
|
||||
},
|
||||
axis: {
|
||||
orient: radios('y.axis.orient', ['left', 'right'], 'left'),
|
||||
title: 'Score',
|
||||
},
|
||||
},
|
||||
color: {
|
||||
field: 'name',
|
||||
type: 'nominal',
|
||||
legend: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
height: 400,
|
||||
payload: { data },
|
||||
width: 400,
|
||||
})
|
||||
}
|
||||
/>,
|
||||
],
|
||||
storyName: 'Basic',
|
||||
storyPath: 'preset-chart-xy|LineChartPlugin',
|
||||
},
|
||||
];
|
@ -0,0 +1,78 @@
|
||||
/* eslint-disable no-magic-numbers */
|
||||
import * as React from 'react';
|
||||
import { SuperChart, ChartProps } from '@superset-ui/chart';
|
||||
import data from '../data/legacyData';
|
||||
import { LINE_PLUGIN_LEGACY_TYPE } from '../constants';
|
||||
|
||||
export default [
|
||||
{
|
||||
renderStory: () => [
|
||||
<SuperChart
|
||||
key="line1"
|
||||
chartType={LINE_PLUGIN_LEGACY_TYPE}
|
||||
chartProps={
|
||||
new ChartProps({
|
||||
datasource: { verboseMap: {} },
|
||||
formData: {
|
||||
bottomMargin: 'auto',
|
||||
colorScheme: 'd3Category10',
|
||||
leftMargin: 'auto',
|
||||
lineInterpolation: 'linear',
|
||||
richTooltip: true,
|
||||
showBrush: 'auto',
|
||||
showLegend: true,
|
||||
showMarkers: false,
|
||||
vizType: 'line',
|
||||
xAxisFormat: '%Y',
|
||||
xAxisLabel: '',
|
||||
xAxisShowminmax: false,
|
||||
xTicksLayout: 'auto',
|
||||
yAxisBounds: [null, null],
|
||||
yAxisFormat: '',
|
||||
yAxisLabel: '',
|
||||
yAxisShowminmax: false,
|
||||
yLogScale: false,
|
||||
},
|
||||
height: 400,
|
||||
payload: { data },
|
||||
width: 400,
|
||||
})
|
||||
}
|
||||
/>,
|
||||
<SuperChart
|
||||
key="line2"
|
||||
chartType={LINE_PLUGIN_LEGACY_TYPE}
|
||||
chartProps={
|
||||
new ChartProps({
|
||||
datasource: { verboseMap: {} },
|
||||
formData: {
|
||||
bottomMargin: 'auto',
|
||||
colorScheme: 'd3Category10',
|
||||
leftMargin: 'auto',
|
||||
lineInterpolation: 'linear',
|
||||
richTooltip: true,
|
||||
showBrush: 'auto',
|
||||
showLegend: true,
|
||||
showMarkers: false,
|
||||
vizType: 'line',
|
||||
xAxisFormat: '%Y-%m',
|
||||
xAxisLabel: '',
|
||||
xAxisShowminmax: false,
|
||||
xTicksLayout: 'auto',
|
||||
yAxisBounds: [null, null],
|
||||
yAxisFormat: '',
|
||||
yAxisLabel: '',
|
||||
yAxisShowminmax: false,
|
||||
yLogScale: false,
|
||||
},
|
||||
height: 400,
|
||||
payload: { data },
|
||||
width: 800,
|
||||
})
|
||||
}
|
||||
/>,
|
||||
],
|
||||
storyName: 'Use Legacy API shim',
|
||||
storyPath: 'preset-chart-xy|LineChartPlugin',
|
||||
},
|
||||
];
|
@ -0,0 +1,67 @@
|
||||
/* eslint-disable no-magic-numbers, sort-keys */
|
||||
import * as React from 'react';
|
||||
import { SuperChart, ChartProps } from '@superset-ui/chart';
|
||||
import data from '../data/data';
|
||||
import { LINE_PLUGIN_TYPE } from '../constants';
|
||||
|
||||
const missingData = {
|
||||
keys: data.keys,
|
||||
values: data.values.map(({ y, ...rest }) => ({
|
||||
...rest,
|
||||
y: Math.random() < 0.05 ? null : y,
|
||||
})),
|
||||
};
|
||||
|
||||
export default [
|
||||
{
|
||||
renderStory: () => [
|
||||
<SuperChart
|
||||
key="line1"
|
||||
chartType={LINE_PLUGIN_TYPE}
|
||||
chartProps={
|
||||
new ChartProps({
|
||||
datasource: { verboseMap: {} },
|
||||
formData: {
|
||||
encoding: {
|
||||
x: {
|
||||
field: 'x',
|
||||
type: 'temporal',
|
||||
format: '%Y',
|
||||
scale: {
|
||||
type: 'time',
|
||||
},
|
||||
axis: {
|
||||
orient: 'bottom',
|
||||
title: 'Time',
|
||||
},
|
||||
},
|
||||
y: {
|
||||
field: 'y',
|
||||
type: 'quantitative',
|
||||
scale: {
|
||||
type: 'linear',
|
||||
},
|
||||
axis: {
|
||||
orient: 'left',
|
||||
title: 'Score',
|
||||
},
|
||||
},
|
||||
color: {
|
||||
field: 'name',
|
||||
type: 'nominal',
|
||||
scale: {},
|
||||
legend: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
height: 400,
|
||||
payload: { data: missingData },
|
||||
width: 400,
|
||||
})
|
||||
}
|
||||
/>,
|
||||
],
|
||||
storyName: 'with missing data',
|
||||
storyPath: 'preset-chart-xy|LineChartPlugin',
|
||||
},
|
||||
];
|
@ -0,0 +1,78 @@
|
||||
/* eslint-disable no-magic-numbers, sort-keys */
|
||||
import * as React from 'react';
|
||||
import { SuperChart, ChartProps } from '@superset-ui/chart';
|
||||
import data from '../data/data2';
|
||||
import { LINE_PLUGIN_TYPE } from '../constants';
|
||||
|
||||
export default [
|
||||
{
|
||||
renderStory: () => [
|
||||
<SuperChart
|
||||
key="line1"
|
||||
chartType={LINE_PLUGIN_TYPE}
|
||||
chartProps={
|
||||
new ChartProps({
|
||||
datasource: { verboseMap: {} },
|
||||
formData: {
|
||||
encoding: {
|
||||
x: {
|
||||
field: 'x',
|
||||
type: 'temporal',
|
||||
format: '%Y',
|
||||
scale: {
|
||||
type: 'time',
|
||||
},
|
||||
axis: {
|
||||
orient: 'bottom',
|
||||
title: 'Time',
|
||||
},
|
||||
},
|
||||
y: {
|
||||
field: 'y',
|
||||
type: 'quantitative',
|
||||
scale: {
|
||||
type: 'linear',
|
||||
},
|
||||
axis: {
|
||||
orient: 'left',
|
||||
title: 'Score',
|
||||
},
|
||||
},
|
||||
color: {
|
||||
value: '#1abc9c',
|
||||
type: 'nominal',
|
||||
scale: false,
|
||||
},
|
||||
fill: {
|
||||
field: 'snapshot',
|
||||
type: 'nominal',
|
||||
scale: {
|
||||
type: 'ordinal',
|
||||
domain: ['Current', 'Last year'],
|
||||
range: [true, false],
|
||||
},
|
||||
legend: false,
|
||||
},
|
||||
strokeDasharray: {
|
||||
field: 'snapshot',
|
||||
type: 'nominal',
|
||||
scale: {
|
||||
type: 'ordinal',
|
||||
domain: ['Current', 'Last year'],
|
||||
range: [null, '4 4'],
|
||||
},
|
||||
legend: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
height: 400,
|
||||
payload: { data },
|
||||
width: 400,
|
||||
})
|
||||
}
|
||||
/>,
|
||||
],
|
||||
storyName: 'with time shift',
|
||||
storyPath: 'preset-chart-xy|LineChartPlugin',
|
||||
},
|
||||
];
|
@ -3,7 +3,9 @@
|
||||
[![Version](https://img.shields.io/npm/v/@superset-ui/preset-chart-xy.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/preset-chart-xy.svg?style=flat-square)
|
||||
[![David (path)](https://img.shields.io/david/apache-superset/superset-ui-plugins.svg?path=packages%2Fsuperset-ui-preset-chart-xy&style=flat-square)](https://david-dm.org/apache-superset/superset-ui-plugins?path=packages/superset-ui-preset-chart-xy)
|
||||
|
||||
This plugin provides Box Plot for Superset.
|
||||
This plugin provides basic charts on cartesian coordinates (Line, Box Plot) for Superset.
|
||||
|
||||
> DISCLAIMER: It is still under heavy development and the APIs are subject to changes.
|
||||
|
||||
### Usage
|
||||
|
||||
|
@ -1,6 +1,27 @@
|
||||
import { MarkPropChannelDef, XFieldDef, YFieldDef } from '../encodeable/types/FieldDef';
|
||||
import AbstractEncoder from '../encodeable/AbstractEncoder';
|
||||
import { PartialSpec } from '../encodeable/types/Specification';
|
||||
import { EncodingFromChannelsAndOutputs } from '../encodeable/types/Channel';
|
||||
|
||||
/**
|
||||
* Define channel types
|
||||
*/
|
||||
// This is a workaround until TypeScript 3.4 which has const context
|
||||
// which will allow use to derive type from object literal
|
||||
// without type widening (e.g. 'X' instead of string).
|
||||
// Now we have to define class with readonly fields
|
||||
// to be able to use "typeof" to infer strict types
|
||||
// See more details from
|
||||
// https://github.com/Microsoft/TypeScript/issues/20195
|
||||
// https://github.com/Microsoft/TypeScript/pull/29510
|
||||
const channelTypes = new class Channels {
|
||||
readonly x = 'X';
|
||||
readonly y = 'Y';
|
||||
readonly color = 'Color';
|
||||
readonly fill = 'Category';
|
||||
readonly strokeDasharray = 'Category';
|
||||
}();
|
||||
|
||||
export type ChannelTypes = typeof channelTypes;
|
||||
|
||||
/**
|
||||
* Define output type for each channel
|
||||
@ -14,18 +35,12 @@ export interface Outputs {
|
||||
}
|
||||
|
||||
/**
|
||||
* Define encoding config for each channel
|
||||
* Derive encoding config
|
||||
*/
|
||||
export interface Encoding {
|
||||
x: XFieldDef<Outputs['x']>;
|
||||
y: YFieldDef<Outputs['y']>;
|
||||
color: MarkPropChannelDef<Outputs['color']>;
|
||||
fill: MarkPropChannelDef<Outputs['fill']>;
|
||||
strokeDasharray: MarkPropChannelDef<Outputs['strokeDasharray']>;
|
||||
}
|
||||
export type Encoding = EncodingFromChannelsAndOutputs<ChannelTypes, Outputs>;
|
||||
|
||||
export default class Encoder extends AbstractEncoder<Outputs, Encoding> {
|
||||
static DEFAULT_ENCODINGS: Encoding = {
|
||||
export default class Encoder extends AbstractEncoder<ChannelTypes, Outputs> {
|
||||
static readonly DEFAULT_ENCODINGS: Encoding = {
|
||||
color: { value: '#222' },
|
||||
fill: { value: false },
|
||||
strokeDasharray: { value: '' },
|
||||
@ -33,17 +48,11 @@ export default class Encoder extends AbstractEncoder<Outputs, Encoding> {
|
||||
y: { field: 'y', type: 'quantitative' },
|
||||
};
|
||||
|
||||
constructor(spec: PartialSpec<Encoding>) {
|
||||
super(spec, Encoder.DEFAULT_ENCODINGS);
|
||||
}
|
||||
static readonly CHANNEL_OPTIONS = {
|
||||
fill: { legend: false },
|
||||
};
|
||||
|
||||
createChannels() {
|
||||
return {
|
||||
color: this.createChannel('color'),
|
||||
fill: this.createChannel('fill', { legend: false }),
|
||||
strokeDasharray: this.createChannel('strokeDasharray'),
|
||||
x: this.createChannel('x'),
|
||||
y: this.createChannel('y'),
|
||||
};
|
||||
constructor(spec: PartialSpec<Encoding>) {
|
||||
super(channelTypes, spec, Encoder.DEFAULT_ENCODINGS, Encoder.CHANNEL_OPTIONS);
|
||||
}
|
||||
}
|
||||
|
@ -1,136 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
/* eslint-disable sort-keys, no-magic-numbers, complexity */
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { LineSeries, XYChart } from '@data-ui/xy-chart';
|
||||
import { themeShape } from '@data-ui/xy-chart/esm/utils/propShapes';
|
||||
import { chartTheme } from '@data-ui/theme';
|
||||
import { CategoricalColorNamespace } from '@superset-ui/color';
|
||||
import createTooltip from './createTooltip';
|
||||
import renderLegend from '../utils/renderLegend';
|
||||
import XYChartLayout from '../utils/XYChartLayout';
|
||||
import WithLegend from '../components/WithLegend';
|
||||
|
||||
chartTheme.gridStyles.stroke = '#f1f3f5';
|
||||
|
||||
const propTypes = {
|
||||
className: PropTypes.string,
|
||||
data: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
key: PropTypes.string,
|
||||
values: PropTypes.arrayOf(PropTypes.number),
|
||||
}),
|
||||
).isRequired,
|
||||
width: PropTypes.number.isRequired,
|
||||
height: PropTypes.number.isRequired,
|
||||
margin: PropTypes.shape({
|
||||
top: PropTypes.number,
|
||||
left: PropTypes.number,
|
||||
bottom: PropTypes.number,
|
||||
right: PropTypes.number,
|
||||
}),
|
||||
encoding: PropTypes.shape({
|
||||
x: PropTypes.object,
|
||||
y: PropTypes.object,
|
||||
color: PropTypes.object,
|
||||
}).isRequired,
|
||||
theme: themeShape,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
className: '',
|
||||
margin: { top: 10, right: 10, left: 10, bottom: 10 },
|
||||
theme: chartTheme,
|
||||
};
|
||||
|
||||
class LineChart extends React.PureComponent {
|
||||
renderChart({ width, height }) {
|
||||
const { data, encoding, margin, theme } = this.props;
|
||||
|
||||
const config = {
|
||||
width,
|
||||
height,
|
||||
minContentWidth: 0,
|
||||
minContentHeight: 0,
|
||||
margin,
|
||||
theme,
|
||||
encoding,
|
||||
};
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(
|
||||
encoding.color.scale.scheme,
|
||||
encoding.color.scale.namespace,
|
||||
);
|
||||
|
||||
const colorAccessor = encoding.color.accessor;
|
||||
|
||||
const children = data.map(series => (
|
||||
<LineSeries
|
||||
key={series.key.join('/')}
|
||||
animated
|
||||
data={series.values}
|
||||
stroke={colorFn(colorAccessor(series))}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
));
|
||||
|
||||
const layout = new XYChartLayout({ ...config, children });
|
||||
|
||||
return layout.createChartWithFrame(dim => (
|
||||
<XYChart
|
||||
width={dim.width}
|
||||
height={dim.height}
|
||||
ariaLabel="BoxPlot"
|
||||
margin={layout.margin}
|
||||
renderTooltip={createTooltip(encoding.y.axis.tickFormat)}
|
||||
showYGrid
|
||||
// snapTooltipToDataX
|
||||
theme={config.theme}
|
||||
xScale={config.encoding.x.scale}
|
||||
yScale={config.encoding.y.scale}
|
||||
>
|
||||
{children}
|
||||
{layout.createXAxis()}
|
||||
{layout.createYAxis()}
|
||||
</XYChart>
|
||||
));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, data, width, height, encoding } = this.props;
|
||||
|
||||
return (
|
||||
<WithLegend
|
||||
className={`superset-chart-line ${className}`}
|
||||
width={width}
|
||||
height={height}
|
||||
position="top"
|
||||
renderLegend={() => renderLegend(data, encoding.color)}
|
||||
renderChart={parent => this.renderChart(parent)}
|
||||
hideLegend={!encoding.color.legend}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LineChart.propTypes = propTypes;
|
||||
LineChart.defaultProps = defaultProps;
|
||||
|
||||
export default LineChart;
|
@ -0,0 +1,226 @@
|
||||
/* eslint-disable sort-keys, no-magic-numbers, complexity */
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
import {
|
||||
AreaSeries,
|
||||
LinearGradient,
|
||||
LineSeries,
|
||||
XYChart,
|
||||
CrossHair,
|
||||
WithTooltip,
|
||||
} from '@data-ui/xy-chart';
|
||||
import { chartTheme, ChartTheme } from '@data-ui/theme';
|
||||
import { Margin, Dimension } from '@superset-ui/dimension';
|
||||
import { groupBy, flatMap, uniqueId, values } from 'lodash';
|
||||
import createTooltip from './createTooltip';
|
||||
import XYChartLayout from '../utils/XYChartLayout';
|
||||
import WithLegend from '../components/WithLegend';
|
||||
import Encoder, { ChannelTypes, Encoding, Outputs } from './Encoder';
|
||||
import { Dataset, PlainObject } from '../encodeable/types/Data';
|
||||
import ChartLegend from '../components/ChartLegend';
|
||||
|
||||
chartTheme.gridStyles.stroke = '#f1f3f5';
|
||||
|
||||
const defaultProps = {
|
||||
className: '',
|
||||
margin: { top: 20, right: 20, left: 20, bottom: 20 },
|
||||
theme: chartTheme,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
width: string | number;
|
||||
height: string | number;
|
||||
margin?: Margin;
|
||||
encoding: Encoding;
|
||||
data: Dataset;
|
||||
theme?: ChartTheme;
|
||||
} & Readonly<typeof defaultProps>;
|
||||
|
||||
export interface Series {
|
||||
key: string;
|
||||
color: Outputs['color'];
|
||||
fill: Outputs['fill'];
|
||||
strokeDasharray: Outputs['strokeDasharray'];
|
||||
values: SeriesValue[];
|
||||
}
|
||||
|
||||
export interface SeriesValue {
|
||||
x: Outputs['x'];
|
||||
y: Outputs['y'];
|
||||
data: PlainObject;
|
||||
parent: Series;
|
||||
}
|
||||
|
||||
const CIRCLE_STYLE = { strokeWidth: 1.5 };
|
||||
|
||||
class LineChart extends PureComponent<Props> {
|
||||
static defaultProps = defaultProps;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const { encoding } = this.props;
|
||||
|
||||
this.encoder = new Encoder({ encoding });
|
||||
this.renderChart = this.renderChart.bind(this);
|
||||
}
|
||||
|
||||
encoder: Encoder;
|
||||
|
||||
renderChart(dim: Dimension) {
|
||||
const { width, height } = dim;
|
||||
const { data, encoding, margin, theme } = this.props;
|
||||
|
||||
const fieldNames = data.keys
|
||||
.filter(k => k !== encoding.x.field && k !== encoding.y.field)
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
const groups = groupBy(data.values, row => fieldNames.map(f => `${f}=${row[f]}`).join(','));
|
||||
|
||||
const allSeries = values(groups).map(seriesData => {
|
||||
const firstDatum = seriesData[0];
|
||||
|
||||
const series: Series = {
|
||||
key: fieldNames.map(f => firstDatum[f]).join(','),
|
||||
color: this.encoder.channels.color.encode(firstDatum),
|
||||
fill: this.encoder.channels.fill.encode(firstDatum, false),
|
||||
strokeDasharray: this.encoder.channels.strokeDasharray.encode(firstDatum),
|
||||
values: [],
|
||||
};
|
||||
|
||||
series.values = seriesData.map(v => ({
|
||||
x: this.encoder.channels.x.encode(v),
|
||||
y: this.encoder.channels.y.encode(v),
|
||||
data: v,
|
||||
parent: series,
|
||||
}));
|
||||
|
||||
return series;
|
||||
});
|
||||
|
||||
const filledSeries = flatMap(
|
||||
allSeries
|
||||
.filter(({ fill }) => fill)
|
||||
.map(series => {
|
||||
const gradientId = uniqueId(`gradient-${series.key}`);
|
||||
|
||||
return [
|
||||
<LinearGradient
|
||||
key={`${series.key}-gradient`}
|
||||
id={gradientId}
|
||||
from={series.color}
|
||||
to="#fff"
|
||||
/>,
|
||||
<AreaSeries
|
||||
key={`${series.key}-fill`}
|
||||
data={series.values}
|
||||
interpolation="linear"
|
||||
fill={`url(#${gradientId})`}
|
||||
stroke={series.color}
|
||||
strokeWidth={1.5}
|
||||
/>,
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
const unfilledSeries = allSeries
|
||||
.filter(({ fill }) => !fill)
|
||||
.map(series => (
|
||||
<LineSeries
|
||||
key={series.key}
|
||||
seriesKey={series.key}
|
||||
interpolation="linear"
|
||||
data={series.values}
|
||||
stroke={series.color}
|
||||
strokeDasharray={series.strokeDasharray}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
));
|
||||
|
||||
const children = filledSeries.concat(unfilledSeries);
|
||||
|
||||
const layout = new XYChartLayout({
|
||||
width,
|
||||
height,
|
||||
margin,
|
||||
theme,
|
||||
xEncoder: this.encoder.channels.x,
|
||||
yEncoder: this.encoder.channels.y,
|
||||
children,
|
||||
});
|
||||
|
||||
return layout.renderChartWithFrame((chartDim: Dimension) => (
|
||||
<WithTooltip renderTooltip={createTooltip(this.encoder, allSeries)}>
|
||||
{({
|
||||
onMouseLeave,
|
||||
onMouseMove,
|
||||
tooltipData,
|
||||
}: {
|
||||
onMouseLeave: (...args: any[]) => void;
|
||||
onMouseMove: (...args: any[]) => void;
|
||||
tooltipData: any;
|
||||
}) => (
|
||||
<XYChart
|
||||
width={chartDim.width}
|
||||
height={chartDim.height}
|
||||
ariaLabel="LineChart"
|
||||
eventTrigger="container"
|
||||
margin={layout.margin}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseLeave={onMouseLeave}
|
||||
renderTooltip={null}
|
||||
showYGrid
|
||||
snapTooltipToDataX
|
||||
theme={theme}
|
||||
tooltipData={tooltipData}
|
||||
xScale={this.encoder.channels.x.definition.scale}
|
||||
yScale={this.encoder.channels.y.definition.scale}
|
||||
>
|
||||
{children}
|
||||
{layout.renderXAxis()}
|
||||
{layout.renderYAxis()}
|
||||
<CrossHair
|
||||
fullHeight
|
||||
strokeDasharray=""
|
||||
showHorizontalLine={false}
|
||||
circleFill={(d: SeriesValue) =>
|
||||
d.y === tooltipData.datum.y ? d.parent.color : '#fff'
|
||||
}
|
||||
circleSize={(d: SeriesValue) => (d.y === tooltipData.datum.y ? 6 : 4)}
|
||||
circleStroke={(d: SeriesValue) =>
|
||||
d.y === tooltipData.datum.y ? '#fff' : d.parent.color
|
||||
}
|
||||
circleStyles={CIRCLE_STYLE}
|
||||
stroke="#ccc"
|
||||
showCircle
|
||||
showMultipleCircles
|
||||
/>
|
||||
</XYChart>
|
||||
)}
|
||||
</WithTooltip>
|
||||
));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, data, width, height, encoding } = this.props;
|
||||
|
||||
this.encoder = new Encoder({ encoding });
|
||||
const renderLegend = this.encoder.hasLegend()
|
||||
? // eslint-disable-next-line react/jsx-props-no-multi-spaces
|
||||
() => <ChartLegend<ChannelTypes, Outputs, Encoding> data={data} encoder={this.encoder} />
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<WithLegend
|
||||
className={`superset-chart-line ${className}`}
|
||||
width={width}
|
||||
height={height}
|
||||
position="top"
|
||||
renderLegend={renderLegend}
|
||||
renderChart={this.renderChart}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LineChart;
|
@ -1,5 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function createTooltip() {
|
||||
return () => <div>tooltip!</div>;
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/* eslint-disable no-magic-numbers */
|
||||
|
||||
import React from 'react';
|
||||
import TooltipFrame from '../components/tooltip/TooltipFrame';
|
||||
import TooltipTable from '../components/tooltip/TooltipTable';
|
||||
import { Series, SeriesValue } from './Line';
|
||||
import Encoder from './Encoder';
|
||||
|
||||
export default function createTooltip(encoder: Encoder, allSeries: Series[]) {
|
||||
function LineTooltip({
|
||||
datum,
|
||||
series = {},
|
||||
}: {
|
||||
datum: SeriesValue;
|
||||
series: {
|
||||
[key: string]: {
|
||||
y: number;
|
||||
};
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
<TooltipFrame>
|
||||
<React.Fragment>
|
||||
<div>
|
||||
<strong>{encoder.channels.x.formatValue(datum.x)}</strong>
|
||||
</div>
|
||||
<br />
|
||||
{series && (
|
||||
<TooltipTable
|
||||
data={allSeries
|
||||
.filter(({ key }) => series[key])
|
||||
.concat()
|
||||
.sort((a, b) => series[b.key].y - series[a.key].y)
|
||||
.map(({ key, color }) => ({
|
||||
key,
|
||||
keyStyle: {
|
||||
color,
|
||||
fontWeight: series[key] === datum ? 600 : 200,
|
||||
},
|
||||
value: encoder.channels.y.formatValue(series[key].y),
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
</TooltipFrame>
|
||||
);
|
||||
}
|
||||
|
||||
return LineTooltip;
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
|
||||
import transformProps from './transformProps';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
description: '',
|
||||
name: t('Box Plot'),
|
||||
thumbnail,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class LineChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./Line'),
|
||||
metadata,
|
||||
transformProps,
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import { ChartPlugin } from '@superset-ui/chart';
|
||||
import metadata from './metadata';
|
||||
import transformProps from './transformProps';
|
||||
|
||||
export default class LineChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./Line'),
|
||||
metadata,
|
||||
transformProps,
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import { ChartPlugin } from '@superset-ui/chart';
|
||||
import metadata from './metadata';
|
||||
import transformProps from './transformProps';
|
||||
|
||||
export default class LineChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('../Line'),
|
||||
metadata,
|
||||
transformProps,
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import metadata from '../metadata';
|
||||
|
||||
const legacyMetadata = metadata.clone();
|
||||
legacyMetadata.useLegacyApi = true;
|
||||
|
||||
export default legacyMetadata;
|
@ -0,0 +1,67 @@
|
||||
/* eslint-disable sort-keys */
|
||||
import { ChartProps } from '@superset-ui/chart';
|
||||
import { flatMap } from 'lodash';
|
||||
|
||||
interface DataRow {
|
||||
key: string[];
|
||||
values: {
|
||||
x: number;
|
||||
y: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { width, height, formData, payload } = chartProps;
|
||||
const { colorScheme, xAxisLabel, xAxisFormat, yAxisLabel, yAxisFormat } = formData;
|
||||
const data = payload.data as DataRow[];
|
||||
|
||||
return {
|
||||
data: {
|
||||
keys: ['name', 'x', 'y'],
|
||||
values: flatMap(
|
||||
data.map((row: DataRow) =>
|
||||
row.values.map(v => ({
|
||||
...v,
|
||||
name: row.key[0],
|
||||
})),
|
||||
),
|
||||
),
|
||||
},
|
||||
width,
|
||||
height,
|
||||
encoding: {
|
||||
x: {
|
||||
field: 'x',
|
||||
type: 'temporal',
|
||||
format: xAxisFormat,
|
||||
scale: {
|
||||
type: 'time',
|
||||
},
|
||||
axis: {
|
||||
orient: 'bottom',
|
||||
title: xAxisLabel,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
field: 'y',
|
||||
type: 'quantitative',
|
||||
format: yAxisFormat,
|
||||
scale: {
|
||||
type: 'linear',
|
||||
},
|
||||
axis: {
|
||||
orient: 'left',
|
||||
title: yAxisLabel,
|
||||
},
|
||||
},
|
||||
color: {
|
||||
field: 'name',
|
||||
type: 'nominal',
|
||||
scale: {
|
||||
scheme: colorScheme,
|
||||
},
|
||||
legend: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { ChartMetadata } from '@superset-ui/chart';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
description: '',
|
||||
name: t('Line Chart'),
|
||||
thumbnail,
|
||||
});
|
||||
|
||||
export default metadata;
|
@ -1,47 +0,0 @@
|
||||
import { getNumberFormatter } from '@superset-ui/number-format';
|
||||
import { getTimeFormatter } from '@superset-ui/time-format';
|
||||
|
||||
/* eslint-disable sort-keys */
|
||||
|
||||
export default function transformProps(chartProps) {
|
||||
const { width, height, formData, payload } = chartProps;
|
||||
const { colorScheme, xAxisLabel, xAxisFormat, yAxisLabel, yAxisFormat } = formData;
|
||||
|
||||
return {
|
||||
data: payload.data,
|
||||
width,
|
||||
height,
|
||||
encoding: {
|
||||
x: {
|
||||
accessor: d => d.x,
|
||||
scale: {
|
||||
type: 'time',
|
||||
},
|
||||
axis: {
|
||||
orientation: 'bottom',
|
||||
label: xAxisLabel,
|
||||
numTicks: 5,
|
||||
tickFormat: getTimeFormatter(xAxisFormat),
|
||||
},
|
||||
},
|
||||
y: {
|
||||
accessor: d => d.y,
|
||||
scale: {
|
||||
type: 'linear',
|
||||
},
|
||||
axis: {
|
||||
orientation: 'left',
|
||||
label: yAxisLabel,
|
||||
tickFormat: getNumberFormatter(yAxisFormat),
|
||||
},
|
||||
},
|
||||
color: {
|
||||
accessor: d => d.key.join('/'),
|
||||
scale: {
|
||||
scheme: colorScheme,
|
||||
},
|
||||
legend: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { ChartProps } from '@superset-ui/chart';
|
||||
|
||||
/* eslint-disable sort-keys */
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { width, height, formData, payload } = chartProps;
|
||||
const { encoding } = formData;
|
||||
const { data } = payload;
|
||||
|
||||
return {
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
encoding,
|
||||
};
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { isDefined } from '@superset-ui/core';
|
||||
|
||||
function checkNumber(input: any): input is number {
|
||||
@ -9,11 +9,11 @@ type Props = {
|
||||
contentWidth?: number;
|
||||
contentHeight?: number;
|
||||
height: number;
|
||||
renderContent: ({ height, width }: { height: number; width: number }) => React.ReactElement;
|
||||
renderContent: ({ height, width }: { height: number; width: number }) => React.ReactNode;
|
||||
width: number;
|
||||
};
|
||||
|
||||
export default class ChartFrame extends React.PureComponent<Props, {}> {
|
||||
export default class ChartFrame extends PureComponent<Props, {}> {
|
||||
static defaultProps = {
|
||||
renderContent() {},
|
||||
};
|
||||
|
@ -0,0 +1,91 @@
|
||||
import React, { CSSProperties, PureComponent } from 'react';
|
||||
import { scaleOrdinal } from '@vx/scale';
|
||||
import { LegendOrdinal, LegendItem, LegendLabel } from '@vx/legend';
|
||||
import { Value } from 'vega-lite/build/src/fielddef';
|
||||
import AbstractEncoder from '../encodeable/AbstractEncoder';
|
||||
import { Dataset } from '../encodeable/types/Data';
|
||||
import { ObjectWithKeysFromAndValueType } from '../encodeable/types/Base';
|
||||
import { ChannelType, EncodingFromChannelsAndOutputs } from '../encodeable/types/Channel';
|
||||
import { BaseOptions } from '../encodeable/types/Specification';
|
||||
|
||||
type Props<Encoder> = {
|
||||
data: Dataset;
|
||||
encoder: Encoder;
|
||||
};
|
||||
|
||||
interface Label {
|
||||
text: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const MARK_SIZE = 8;
|
||||
|
||||
const MARK_STYLE: CSSProperties = { display: 'inline-block' };
|
||||
|
||||
const LABEL_STYLE: CSSProperties = { display: 'flex', flexDirection: 'row', flexWrap: 'wrap' };
|
||||
|
||||
const LEGEND_CONTAINER_STYLE: CSSProperties = {
|
||||
maxHeight: 100,
|
||||
overflowY: 'hidden',
|
||||
paddingLeft: 14,
|
||||
paddingTop: 6,
|
||||
position: 'relative',
|
||||
};
|
||||
|
||||
export default class ChartLegend<
|
||||
ChannelTypes extends ObjectWithKeysFromAndValueType<Outputs, ChannelType>,
|
||||
Outputs extends ObjectWithKeysFromAndValueType<Encoding, Value>,
|
||||
Encoding extends EncodingFromChannelsAndOutputs<
|
||||
ChannelTypes,
|
||||
Outputs
|
||||
> = EncodingFromChannelsAndOutputs<ChannelTypes, Outputs>,
|
||||
Options extends BaseOptions = BaseOptions
|
||||
> extends PureComponent<Props<AbstractEncoder<ChannelTypes, Outputs, Encoding, Options>>, {}> {
|
||||
render() {
|
||||
const { data, encoder } = this.props;
|
||||
|
||||
const legends = Object.keys(encoder.legends).map((field: string) => {
|
||||
const channelNames = encoder.legends[field];
|
||||
const channelEncoder = encoder.channels[channelNames[0]];
|
||||
const domain = Array.from(new Set(data.values.map(channelEncoder.get)));
|
||||
const scale = scaleOrdinal({
|
||||
domain,
|
||||
range: domain.map((key: string) => channelEncoder.encodeValue(key)),
|
||||
});
|
||||
|
||||
return (
|
||||
<div key={field} style={LEGEND_CONTAINER_STYLE}>
|
||||
<LegendOrdinal scale={scale} labelFormat={channelEncoder.formatValue}>
|
||||
{(labels: Label[]) => (
|
||||
<div style={LABEL_STYLE}>
|
||||
{labels.map((label: Label) => (
|
||||
<LegendItem
|
||||
key={`legend-quantile-${label.text}`}
|
||||
margin="0 5px"
|
||||
onClick={() => {
|
||||
alert(`clicked: ${JSON.stringify(label)}`);
|
||||
}}
|
||||
>
|
||||
<svg width={MARK_SIZE} height={MARK_SIZE} style={MARK_STYLE}>
|
||||
<circle
|
||||
fill={label.value}
|
||||
r={MARK_SIZE / 2}
|
||||
cx={MARK_SIZE / 2}
|
||||
cy={MARK_SIZE / 2}
|
||||
/>
|
||||
</svg>
|
||||
<LegendLabel align="left" margin="0 0 0 4px">
|
||||
{label.text}
|
||||
</LegendLabel>
|
||||
</LegendItem>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</LegendOrdinal>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return <React.Fragment>{legends}</React.Fragment>;
|
||||
}
|
||||
}
|
@ -1,27 +1,17 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
/* eslint-disable sort-keys */
|
||||
import React, { CSSProperties, ReactNode } from 'react';
|
||||
import React, { CSSProperties, ReactNode, PureComponent } from 'react';
|
||||
import { ParentSize } from '@vx/responsive';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import * as CSS from 'csstype';
|
||||
|
||||
const defaultProps = {
|
||||
className: '',
|
||||
height: 'auto' as number | string,
|
||||
width: 'auto' as number | string,
|
||||
legendJustifyContent: undefined,
|
||||
position: 'top',
|
||||
};
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
width: number | string;
|
||||
@ -29,35 +19,27 @@ type Props = {
|
||||
legendJustifyContent: 'center' | 'flex-start' | 'flex-end';
|
||||
position: 'top' | 'left' | 'bottom' | 'right';
|
||||
renderChart: (dim: { width: number; height: number }) => ReactNode;
|
||||
renderLegend: (params: { direction: string }) => ReactNode;
|
||||
hideLegend: boolean;
|
||||
};
|
||||
renderLegend?: (params: { direction: string }) => ReactNode;
|
||||
} & Readonly<typeof defaultProps>;
|
||||
|
||||
const LEGEND_STYLE_BASE: CSSProperties = {
|
||||
display: 'flex',
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
fontSize: '0.9em',
|
||||
order: -1,
|
||||
paddingTop: '5px',
|
||||
fontSize: '0.9em',
|
||||
};
|
||||
|
||||
const CHART_STYLE_BASE: CSSProperties = {
|
||||
flexBasis: 'auto',
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
flexBasis: 'auto',
|
||||
position: 'relative',
|
||||
};
|
||||
|
||||
class WithLegend extends React.PureComponent<Props, {}> {
|
||||
static defaultProps = {
|
||||
className: '',
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
legendJustifyContent: undefined,
|
||||
position: 'top',
|
||||
hideLegend: false,
|
||||
};
|
||||
class WithLegend extends PureComponent<Props, {}> {
|
||||
static defaultProps = defaultProps;
|
||||
|
||||
getContainerDirection(): CSS.FlexDirectionProperty {
|
||||
const { position } = this.props;
|
||||
@ -93,15 +75,7 @@ class WithLegend extends React.PureComponent<Props, {}> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
width,
|
||||
height,
|
||||
position,
|
||||
renderChart,
|
||||
renderLegend,
|
||||
hideLegend,
|
||||
} = this.props;
|
||||
const { className, width, height, position, renderChart, renderLegend } = this.props;
|
||||
|
||||
const isHorizontal = position === 'left' || position === 'right';
|
||||
|
||||
@ -132,7 +106,7 @@ class WithLegend extends React.PureComponent<Props, {}> {
|
||||
|
||||
return (
|
||||
<div className={`with-legend ${className}`} style={style}>
|
||||
{!hideLegend && (
|
||||
{renderLegend && (
|
||||
<div className="legend-container" style={legendStyle}>
|
||||
{renderLegend({
|
||||
// Pass flexDirection for @vx/legend to arrange legend items
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
@ -7,7 +7,7 @@ type Props = {
|
||||
|
||||
const CONTAINER_STYLE = { padding: 8 };
|
||||
|
||||
class TooltipFrame extends React.PureComponent<Props, {}> {
|
||||
class TooltipFrame extends PureComponent<Props, {}> {
|
||||
static defaultProps = {
|
||||
className: '',
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import React, { CSSProperties, PureComponent } from 'react';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
@ -12,7 +12,7 @@ type Props = {
|
||||
|
||||
const VALUE_CELL_STYLE: CSSProperties = { paddingLeft: 8, textAlign: 'right' };
|
||||
|
||||
export default class TooltipTable extends React.PureComponent<Props, {}> {
|
||||
export default class TooltipTable extends PureComponent<Props, {}> {
|
||||
static defaultProps = {
|
||||
className: '',
|
||||
data: [],
|
||||
|
@ -1,52 +1,80 @@
|
||||
import { Value } from 'vega-lite/build/src/fielddef';
|
||||
import ChannelEncoder from './ChannelEncoder';
|
||||
import { ChannelOptions } from './types/Channel';
|
||||
import { ChannelDef, isFieldDef } from './types/FieldDef';
|
||||
import { ObjectWithKeysFromAndValueType } from './types/Base';
|
||||
import { ChannelOptions, EncodingFromChannelsAndOutputs, ChannelType } from './types/Channel';
|
||||
import { FullSpec, BaseOptions, PartialSpec } from './types/Specification';
|
||||
|
||||
export type ObjectWithKeysFromAndValueType<T extends {}, V> = { [key in keyof T]: V };
|
||||
|
||||
export type ChannelOutputs<T> = ObjectWithKeysFromAndValueType<T, Value>;
|
||||
|
||||
export type BaseEncoding<Output extends ObjectWithKeysFromAndValueType<Output, Value>> = {
|
||||
[key in keyof Output]: ChannelDef<Output[key]>
|
||||
};
|
||||
|
||||
export type Channels<
|
||||
Outputs extends ChannelOutputs<Encoding>,
|
||||
Encoding extends BaseEncoding<Outputs>
|
||||
> = { readonly [k in keyof Outputs]: ChannelEncoder<Encoding[k], Outputs[k]> };
|
||||
import { isFieldDef } from './types/FieldDef';
|
||||
import ChannelEncoder from './ChannelEncoder';
|
||||
|
||||
export default abstract class AbstractEncoder<
|
||||
Outputs extends ChannelOutputs<Encoding>,
|
||||
Encoding extends BaseEncoding<Outputs>,
|
||||
// The first 3 generics depends on each other
|
||||
// to ensure all of them will have the exact same keys
|
||||
ChannelTypes extends ObjectWithKeysFromAndValueType<Outputs, ChannelType>,
|
||||
Outputs extends ObjectWithKeysFromAndValueType<Encoding, Value>,
|
||||
Encoding extends EncodingFromChannelsAndOutputs<
|
||||
ChannelTypes,
|
||||
Outputs
|
||||
> = EncodingFromChannelsAndOutputs<ChannelTypes, Outputs>,
|
||||
Options extends BaseOptions = BaseOptions
|
||||
> {
|
||||
readonly channelTypes: ChannelTypes;
|
||||
readonly spec: FullSpec<Encoding, Options>;
|
||||
readonly channels: Channels<Outputs, Encoding>;
|
||||
|
||||
readonly legends: {
|
||||
[key: string]: (keyof Encoding)[];
|
||||
readonly channels: {
|
||||
readonly [k in keyof ChannelTypes]: ChannelEncoder<Encoding[k], Outputs[k]>
|
||||
};
|
||||
|
||||
constructor(spec: PartialSpec<Encoding, Options>, defaultEncoding?: Encoding) {
|
||||
readonly legends: {
|
||||
[key: string]: (keyof ChannelTypes)[];
|
||||
};
|
||||
|
||||
constructor(
|
||||
channelTypes: ChannelTypes,
|
||||
spec: PartialSpec<Encoding, Options>,
|
||||
defaultEncoding?: Encoding,
|
||||
channelOptions: Partial<{ [k in keyof ChannelTypes]: ChannelOptions }> = {},
|
||||
) {
|
||||
this.channelTypes = channelTypes;
|
||||
this.spec = this.createFullSpec(spec, defaultEncoding);
|
||||
this.channels = this.createChannels();
|
||||
this.legends = {};
|
||||
|
||||
type ChannelName = keyof ChannelTypes;
|
||||
type Channels = { readonly [k in ChannelName]: ChannelEncoder<Encoding[k], Outputs[k]> };
|
||||
|
||||
const channelNames = Object.keys(this.channelTypes) as ChannelName[];
|
||||
|
||||
const { encoding } = this.spec;
|
||||
this.channels = channelNames
|
||||
.map(
|
||||
(name: ChannelName) =>
|
||||
new ChannelEncoder<Encoding[typeof name], Outputs[typeof name]>({
|
||||
definition: encoding[name],
|
||||
name,
|
||||
options: {
|
||||
...this.spec.options,
|
||||
...channelOptions[name],
|
||||
},
|
||||
type: channelTypes[name],
|
||||
}),
|
||||
)
|
||||
.reduce((prev: Partial<Channels>, curr) => {
|
||||
const all = prev;
|
||||
all[curr.name as ChannelName] = curr;
|
||||
|
||||
return all;
|
||||
}, {}) as Channels;
|
||||
|
||||
// Group the channels that use the same field together
|
||||
// so they can share the same legend.
|
||||
(Object.keys(this.channels) as (keyof Encoding)[])
|
||||
.map((key: keyof Encoding) => this.channels[key])
|
||||
this.legends = {};
|
||||
channelNames
|
||||
.map((name: ChannelName) => this.channels[name])
|
||||
.filter(c => c.hasLegend())
|
||||
.forEach(c => {
|
||||
if (isFieldDef(c.definition)) {
|
||||
const key = c.name as keyof Encoding;
|
||||
const name = c.name as ChannelName;
|
||||
const { field } = c.definition;
|
||||
if (this.legends[field]) {
|
||||
this.legends[field].push(key);
|
||||
this.legends[field].push(name);
|
||||
} else {
|
||||
this.legends[field] = [key];
|
||||
this.legends[field] = [name];
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -71,27 +99,6 @@ export default abstract class AbstractEncoder<
|
||||
};
|
||||
}
|
||||
|
||||
protected createChannel<ChannelName extends keyof Outputs>(
|
||||
name: ChannelName,
|
||||
options?: ChannelOptions,
|
||||
) {
|
||||
const { encoding } = this.spec;
|
||||
|
||||
return new ChannelEncoder<Encoding[ChannelName], Outputs[ChannelName]>(
|
||||
`${name}`,
|
||||
encoding[name],
|
||||
{
|
||||
...this.spec.options,
|
||||
...options,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* subclass should override this
|
||||
*/
|
||||
protected abstract createChannels(): Channels<Outputs, Encoding>;
|
||||
|
||||
hasLegend() {
|
||||
return Object.keys(this.legends).length > 0;
|
||||
}
|
||||
|
@ -0,0 +1,177 @@
|
||||
/* eslint-disable no-magic-numbers */
|
||||
import { CSSProperties } from 'react';
|
||||
import { Value } from 'vega-lite/build/src/fielddef';
|
||||
import { getTextDimension } from '@superset-ui/dimension';
|
||||
import { CategoricalColorScale } from '@superset-ui/color';
|
||||
import { extractFormatFromTypeAndFormat } from './parsers/extractFormat';
|
||||
import { CoreAxis, LabelOverlapStrategy } from './types/Axis';
|
||||
import { PositionFieldDef, ChannelDef } from './types/FieldDef';
|
||||
import ChannelEncoder from './ChannelEncoder';
|
||||
import { DEFAULT_LABEL_ANGLE } from '../utils/constants';
|
||||
|
||||
const DEFAULT_BASE_CONFIG: {
|
||||
labelOverlap: LabelOverlapStrategy;
|
||||
labelPadding: number;
|
||||
tickCount: number;
|
||||
} = {
|
||||
labelOverlap: 'auto',
|
||||
labelPadding: 4,
|
||||
tickCount: 5,
|
||||
};
|
||||
|
||||
const DEFAULT_X_CONFIG: CoreAxis = {
|
||||
...DEFAULT_BASE_CONFIG,
|
||||
labelAngle: DEFAULT_LABEL_ANGLE,
|
||||
orient: 'bottom',
|
||||
};
|
||||
|
||||
const DEFAULT_Y_CONFIG: CoreAxis = {
|
||||
...DEFAULT_BASE_CONFIG,
|
||||
labelAngle: 0,
|
||||
orient: 'left',
|
||||
};
|
||||
|
||||
export default class AxisAgent<Def extends ChannelDef<Output>, Output extends Value = Value> {
|
||||
private readonly channelEncoder: ChannelEncoder<Def, Output>;
|
||||
private readonly format?: (value: any) => string;
|
||||
readonly config: CoreAxis;
|
||||
|
||||
constructor(channelEncoder: ChannelEncoder<Def, Output>) {
|
||||
this.channelEncoder = channelEncoder;
|
||||
const definition = channelEncoder.definition as PositionFieldDef;
|
||||
const { type, axis = {} } = definition;
|
||||
|
||||
this.config = this.channelEncoder.isX()
|
||||
? { ...DEFAULT_X_CONFIG, ...axis }
|
||||
: { ...DEFAULT_Y_CONFIG, ...axis };
|
||||
|
||||
if (typeof axis.format !== 'undefined') {
|
||||
this.format = extractFormatFromTypeAndFormat(type, axis.format);
|
||||
}
|
||||
}
|
||||
|
||||
getFormat() {
|
||||
return this.format || this.channelEncoder.formatValue;
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return this.config.title || this.channelEncoder.getTitle();
|
||||
}
|
||||
|
||||
getTickLabels() {
|
||||
const { tickCount, values } = this.config;
|
||||
|
||||
const format = this.getFormat();
|
||||
if (typeof values !== 'undefined') {
|
||||
return (values as any[]).map(format);
|
||||
}
|
||||
|
||||
if (typeof this.channelEncoder.scale !== 'undefined') {
|
||||
const { scale } = this.channelEncoder.scale;
|
||||
if (typeof scale !== 'undefined' && !(scale instanceof CategoricalColorScale)) {
|
||||
return ('ticks' in scale && typeof scale.ticks !== 'undefined'
|
||||
? scale.ticks(tickCount)
|
||||
: scale.domain()
|
||||
).map(format);
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
computeLayout({
|
||||
axisLabelHeight = 20,
|
||||
axisWidth,
|
||||
gapBetweenAxisLabelAndBorder = 8,
|
||||
gapBetweenTickAndTickLabel = 4,
|
||||
labelAngle = this.config.labelAngle,
|
||||
tickLength,
|
||||
tickTextStyle,
|
||||
}: {
|
||||
axisLabelHeight?: number;
|
||||
axisWidth: number;
|
||||
gapBetweenAxisLabelAndBorder?: number;
|
||||
gapBetweenTickAndTickLabel?: number;
|
||||
labelAngle?: number;
|
||||
tickLength: number;
|
||||
tickTextStyle: CSSProperties;
|
||||
}) {
|
||||
const tickLabels = this.getTickLabels();
|
||||
|
||||
const labelDimensions = tickLabels.map((text: string) =>
|
||||
getTextDimension({
|
||||
style: tickTextStyle,
|
||||
text,
|
||||
}),
|
||||
);
|
||||
|
||||
const { labelOverlap, labelPadding, orient } = this.config;
|
||||
|
||||
const maxWidth = Math.max(...labelDimensions.map(d => d.width));
|
||||
|
||||
// TODO: Add other strategies: stagger, chop, wrap.
|
||||
let strategy = labelOverlap;
|
||||
if (strategy === 'auto') {
|
||||
// cheap heuristic, can improve
|
||||
const widthPerTick = axisWidth / tickLabels.length;
|
||||
if (this.channelEncoder.isY() || maxWidth <= widthPerTick) {
|
||||
strategy = 'flat';
|
||||
} else {
|
||||
strategy = 'rotate';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.channelEncoder.isX()) {
|
||||
let labelOffset = 0;
|
||||
let layout: {
|
||||
labelAngle: number;
|
||||
tickTextAnchor?: string;
|
||||
} = { labelAngle };
|
||||
|
||||
if (strategy === 'flat') {
|
||||
labelOffset = labelDimensions[0].height + labelPadding;
|
||||
layout = { labelAngle: 0 };
|
||||
} else if (strategy === 'rotate') {
|
||||
const labelHeight = Math.ceil(Math.abs(maxWidth * Math.sin((labelAngle * Math.PI) / 180)));
|
||||
labelOffset = labelHeight + labelPadding;
|
||||
layout = {
|
||||
labelAngle,
|
||||
tickTextAnchor:
|
||||
(orient === 'top' && labelAngle > 0) || (orient === 'bottom' && labelAngle < 0)
|
||||
? 'end'
|
||||
: 'start',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...layout,
|
||||
labelOffset,
|
||||
labelOverlap: strategy,
|
||||
minMargin: {
|
||||
[orient]: Math.ceil(
|
||||
tickLength +
|
||||
gapBetweenTickAndTickLabel +
|
||||
labelOffset +
|
||||
axisLabelHeight +
|
||||
gapBetweenAxisLabelAndBorder +
|
||||
8,
|
||||
),
|
||||
},
|
||||
orient,
|
||||
};
|
||||
}
|
||||
|
||||
const labelOffset = Math.ceil(maxWidth + labelPadding + axisLabelHeight);
|
||||
|
||||
return {
|
||||
labelAngle,
|
||||
labelOffset,
|
||||
labelOverlap,
|
||||
minMargin: {
|
||||
[orient]:
|
||||
tickLength + gapBetweenTickAndTickLabel + labelOffset + gapBetweenAxisLabelAndBorder,
|
||||
},
|
||||
orient,
|
||||
};
|
||||
}
|
||||
}
|
@ -1,76 +1,77 @@
|
||||
import { Value } from 'vega-lite/build/src/fielddef';
|
||||
import { CategoricalColorScale } from '@superset-ui/color';
|
||||
import { ScaleOrdinal } from 'd3-scale';
|
||||
import { TimeFormatter } from '@superset-ui/time-format';
|
||||
import { NumberFormatter } from '@superset-ui/number-format';
|
||||
import { extractFormatFromChannelDef } from './parsers/extractFormat';
|
||||
import extractScale, { ScaleAgent } from './parsers/extractScale';
|
||||
import extractGetter from './parsers/extractGetter';
|
||||
import { ChannelOptions, ChannelType } from './types/Channel';
|
||||
import { PlainObject } from './types/Data';
|
||||
import {
|
||||
ChannelDef,
|
||||
Formatter,
|
||||
isScaleFieldDef,
|
||||
isMarkPropFieldDef,
|
||||
isValueDef,
|
||||
isFieldDef,
|
||||
isNonValueDef,
|
||||
} from './types/FieldDef';
|
||||
import { PlainObject } from './types/Data';
|
||||
import extractScale from './parsers/extractScale';
|
||||
import extractGetter from './parsers/extractGetter';
|
||||
import extractFormat from './parsers/extractFormat';
|
||||
import extractAxis, { isXYChannel } from './parsers/extractAxis';
|
||||
import isEnabled from './utils/isEnabled';
|
||||
import isDisabled from './utils/isDisabled';
|
||||
import { ChannelOptions } from './types/Channel';
|
||||
import identity from './utils/identity';
|
||||
import AxisAgent from './AxisAgent';
|
||||
|
||||
export default class ChannelEncoder<Def extends ChannelDef<Output>, Output extends Value = Value> {
|
||||
readonly name: string;
|
||||
readonly name: string | Symbol | number;
|
||||
readonly type: ChannelType;
|
||||
readonly definition: Def;
|
||||
readonly options: ChannelOptions;
|
||||
|
||||
readonly axis?: PlainObject;
|
||||
protected readonly getValue: (datum: PlainObject) => Value;
|
||||
readonly scale?: ScaleOrdinal<string, Output> | CategoricalColorScale | ((x: any) => Output);
|
||||
readonly formatter: Formatter;
|
||||
|
||||
readonly encodeValue: (value: any) => Output;
|
||||
protected readonly getValue: (datum: PlainObject) => Value | undefined;
|
||||
readonly encodeValue: (value: any) => Output | null | undefined;
|
||||
readonly formatValue: (value: any) => string;
|
||||
readonly scale?: ScaleAgent<Output>;
|
||||
readonly axis?: AxisAgent<Def, Output>;
|
||||
|
||||
constructor(name: string, definition: Def, options: ChannelOptions = {}) {
|
||||
constructor({
|
||||
name,
|
||||
type,
|
||||
definition,
|
||||
options = {},
|
||||
}: {
|
||||
name: string | Symbol | number;
|
||||
type: ChannelType;
|
||||
definition: Def;
|
||||
options?: ChannelOptions;
|
||||
}) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.definition = definition;
|
||||
this.options = options;
|
||||
|
||||
this.getValue = extractGetter(definition);
|
||||
this.formatValue = extractFormatFromChannelDef(definition);
|
||||
|
||||
const formatter = extractFormat(definition);
|
||||
this.formatter = formatter;
|
||||
if (formatter instanceof NumberFormatter) {
|
||||
this.formatValue = (value: any) => formatter(value);
|
||||
} else if (formatter instanceof TimeFormatter) {
|
||||
this.formatValue = (value: any) => formatter(value);
|
||||
} else {
|
||||
this.formatValue = formatter;
|
||||
this.scale = extractScale(this.type, definition, options.namespace);
|
||||
// Has to extract axis after format and scale
|
||||
if (
|
||||
this.isXY() &&
|
||||
isNonValueDef(this.definition) &&
|
||||
(('axis' in this.definition && isEnabled(this.definition.axis)) ||
|
||||
!('axis' in this.definition))
|
||||
) {
|
||||
this.axis = new AxisAgent<Def, Output>(this);
|
||||
}
|
||||
|
||||
const scale = extractScale<Output>(definition, options.namespace);
|
||||
this.scale = scale;
|
||||
if (typeof scale === 'undefined') {
|
||||
this.encodeValue = identity;
|
||||
} else if (scale instanceof CategoricalColorScale) {
|
||||
this.encodeValue = (value: any) => scale(`${value}`);
|
||||
} else {
|
||||
this.encodeValue = (value: any) => scale(value);
|
||||
}
|
||||
|
||||
this.axis = extractAxis(name, definition, this.formatter);
|
||||
}
|
||||
|
||||
get(datum: PlainObject, otherwise?: any) {
|
||||
const value = this.getValue(datum);
|
||||
|
||||
return otherwise !== undefined && (value === null || value === undefined) ? otherwise : value;
|
||||
this.encodeValue = this.scale ? this.scale.encodeValue : identity;
|
||||
this.encode = this.encode.bind(this);
|
||||
this.format = this.format.bind(this);
|
||||
this.get = this.get.bind(this);
|
||||
}
|
||||
|
||||
encode(datum: PlainObject, otherwise?: Output) {
|
||||
const output = this.encodeValue(this.get(datum));
|
||||
const value = this.get(datum);
|
||||
if (value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const output = this.encodeValue(value);
|
||||
|
||||
return otherwise !== undefined && (output === null || output === undefined)
|
||||
? otherwise
|
||||
@ -81,14 +82,22 @@ export default class ChannelEncoder<Def extends ChannelDef<Output>, Output exten
|
||||
return this.formatValue(this.get(datum));
|
||||
}
|
||||
|
||||
get(datum: PlainObject, otherwise?: any) {
|
||||
const value = this.getValue(datum);
|
||||
|
||||
return otherwise !== undefined && (value === null || value === undefined) ? otherwise : value;
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
if (isFieldDef(this.definition)) {
|
||||
return this.definition.title || this.definition.field;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
hasLegend() {
|
||||
if (isDisabled(this.options.legend)) {
|
||||
return false;
|
||||
}
|
||||
if (isXYChannel(this.name)) {
|
||||
return false;
|
||||
}
|
||||
if (isValueDef(this.definition)) {
|
||||
if (isDisabled(this.options.legend) || this.isXY() || isValueDef(this.definition)) {
|
||||
return false;
|
||||
}
|
||||
if (isMarkPropFieldDef(this.definition)) {
|
||||
@ -97,4 +106,16 @@ export default class ChannelEncoder<Def extends ChannelDef<Output>, Output exten
|
||||
|
||||
return isScaleFieldDef(this.definition);
|
||||
}
|
||||
|
||||
isX() {
|
||||
return this.type === 'X' || this.type === 'XBand';
|
||||
}
|
||||
|
||||
isXY() {
|
||||
return this.type === 'X' || this.type === 'Y' || this.type === 'XBand' || this.type === 'YBand';
|
||||
}
|
||||
|
||||
isY() {
|
||||
return this.type === 'Y' || this.type === 'YBand';
|
||||
}
|
||||
}
|
||||
|
@ -1,35 +0,0 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { Axis } from 'vega-lite/build/src/axis';
|
||||
import { ChannelDef, isPositionFieldDef, Formatter } from '../types/FieldDef';
|
||||
import extractFormat from './extractFormat';
|
||||
import { PlainObject } from '../types/Data';
|
||||
|
||||
export function isXYChannel(channelName: string) {
|
||||
return channelName === 'x' || channelName === 'y';
|
||||
}
|
||||
|
||||
function isAxis(axis: Axis | null | undefined | false): axis is Axis {
|
||||
return axis !== false && axis !== null && axis !== undefined;
|
||||
}
|
||||
|
||||
export default function extractAxis(
|
||||
channelName: string,
|
||||
definition: ChannelDef,
|
||||
defaultFormatter: Formatter,
|
||||
) {
|
||||
if (isXYChannel(channelName) && isPositionFieldDef(definition)) {
|
||||
const { type, axis } = definition;
|
||||
if (isAxis(axis)) {
|
||||
const parsedAxis: PlainObject = cloneDeep(axis);
|
||||
const { labels } = parsedAxis;
|
||||
const { format } = labels;
|
||||
parsedAxis.format = format
|
||||
? extractFormat({ field: definition.field, format: axis.format, type })
|
||||
: defaultFormatter;
|
||||
|
||||
return parsedAxis;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
@ -1,20 +1,32 @@
|
||||
import { getNumberFormatter } from '@superset-ui/number-format';
|
||||
import { getTimeFormatter } from '@superset-ui/time-format';
|
||||
import { Type } from 'vega-lite/build/src/type';
|
||||
import { isTypedFieldDef, ChannelDef } from '../types/FieldDef';
|
||||
|
||||
export default function extractFormat(definition: ChannelDef) {
|
||||
const fallbackFormatter = (v: any) => `${v}`;
|
||||
|
||||
export function extractFormatFromTypeAndFormat(type: Type, format: string) {
|
||||
if (type === 'quantitative') {
|
||||
const formatter = getNumberFormatter(format);
|
||||
|
||||
return (value: any) => formatter(value);
|
||||
} else if (type === 'temporal') {
|
||||
const formatter = getTimeFormatter(format);
|
||||
|
||||
return (value: any) => formatter(value);
|
||||
}
|
||||
|
||||
return fallbackFormatter;
|
||||
}
|
||||
|
||||
export function extractFormatFromChannelDef(definition: ChannelDef) {
|
||||
if (isTypedFieldDef(definition)) {
|
||||
const { type } = definition;
|
||||
const format =
|
||||
'format' in definition && definition.format !== undefined ? definition.format : '';
|
||||
switch (type) {
|
||||
case 'quantitative':
|
||||
return getNumberFormatter(format);
|
||||
case 'temporal':
|
||||
return getTimeFormatter(format);
|
||||
default:
|
||||
}
|
||||
|
||||
return extractFormatFromTypeAndFormat(type, format);
|
||||
}
|
||||
|
||||
return (v: any) => `${v}`;
|
||||
return fallbackFormatter;
|
||||
}
|
||||
|
@ -1,38 +1,246 @@
|
||||
import { CategoricalColorNamespace } from '@superset-ui/color';
|
||||
import { scaleOrdinal } from 'd3-scale';
|
||||
import { CategoricalColorNamespace, CategoricalColorScale } from '@superset-ui/color';
|
||||
import {
|
||||
ScaleOrdinal,
|
||||
ScaleLinear,
|
||||
ScaleLogarithmic,
|
||||
ScalePower,
|
||||
ScaleTime,
|
||||
ScaleQuantile,
|
||||
ScaleQuantize,
|
||||
ScaleThreshold,
|
||||
ScalePoint,
|
||||
ScaleBand,
|
||||
scaleLinear,
|
||||
scaleLog,
|
||||
scalePow,
|
||||
scaleSqrt,
|
||||
scaleTime,
|
||||
scaleUtc,
|
||||
scaleQuantile,
|
||||
scaleQuantize,
|
||||
scaleThreshold,
|
||||
scaleOrdinal,
|
||||
scalePoint,
|
||||
scaleBand,
|
||||
} from 'd3-scale';
|
||||
import { Value } from 'vega-lite/build/src/fielddef';
|
||||
import isEnabled from '../utils/isEnabled';
|
||||
import { isScaleFieldDef, ChannelDef, isPositionFieldDef } from '../types/FieldDef';
|
||||
import { Type } from 'vega-lite/build/src/type';
|
||||
import { ScaleType } from 'vega-lite/build/src/scale';
|
||||
import { isNonValueDef, ChannelDef } from '../types/FieldDef';
|
||||
import isDisabled from '../utils/isDisabled';
|
||||
import { ChannelType } from '../types/Channel';
|
||||
import { Scale } from '../types/Scale';
|
||||
|
||||
export default function extractScale<Output extends Value = Value>(
|
||||
definition: ChannelDef<Output>,
|
||||
namespace?: string,
|
||||
) {
|
||||
if (isScaleFieldDef<Output>(definition)) {
|
||||
const { scale, type } = definition;
|
||||
if (isEnabled(scale) && !isPositionFieldDef(definition)) {
|
||||
if (scale) {
|
||||
const { domain, range, scheme } = scale;
|
||||
if (type === 'nominal') {
|
||||
if (scheme) {
|
||||
return CategoricalColorNamespace.getScale(scheme, namespace);
|
||||
}
|
||||
export interface ScaleAgent<Output extends Value> {
|
||||
config: Scale<Output>;
|
||||
setDomain: (newDomain: number[] | string[] | boolean[] | Date[]) => void;
|
||||
encodeValue: (value: number | string | boolean | null | undefined | Date) => Output;
|
||||
scale:
|
||||
| CategoricalColorScale
|
||||
| ScaleLinear<Output, Output>
|
||||
| ScaleLogarithmic<Output, Output>
|
||||
| ScalePower<Output, Output>
|
||||
| ScaleLogarithmic<Output, Output>
|
||||
| ScaleTime<Output, Output>
|
||||
| ScaleQuantile<Output>
|
||||
| ScaleQuantize<Output>
|
||||
| ScaleThreshold<number | string | Date, Output>
|
||||
| ScaleOrdinal<{ toString(): string }, Output>
|
||||
| ScalePoint<{ toString(): string }>
|
||||
| ScaleBand<{ toString(): string }>;
|
||||
}
|
||||
|
||||
const scaleFn = scaleOrdinal<any, Output>();
|
||||
if (domain) {
|
||||
scaleFn.domain(domain);
|
||||
}
|
||||
if (range) {
|
||||
scaleFn.range(range);
|
||||
}
|
||||
export interface ScaleTypeToD3ScaleType<Output> {
|
||||
[ScaleType.LINEAR]: ScaleLinear<Output, Output>;
|
||||
[ScaleType.LOG]: ScaleLogarithmic<Output, Output>;
|
||||
[ScaleType.POW]: ScalePower<Output, Output>;
|
||||
[ScaleType.SQRT]: ScalePower<Output, Output>;
|
||||
[ScaleType.SYMLOG]: ScaleLogarithmic<Output, Output>;
|
||||
[ScaleType.TIME]: ScaleTime<Output, Output>;
|
||||
[ScaleType.UTC]: ScaleTime<Output, Output>;
|
||||
[ScaleType.QUANTILE]: ScaleQuantile<Output>;
|
||||
[ScaleType.QUANTIZE]: ScaleQuantize<Output>;
|
||||
[ScaleType.THRESHOLD]: ScaleThreshold<number | string | Date, Output>;
|
||||
[ScaleType.BIN_ORDINAL]: ScaleOrdinal<{ toString(): string }, Output>;
|
||||
[ScaleType.ORDINAL]: ScaleOrdinal<{ toString(): string }, Output>;
|
||||
[ScaleType.POINT]: ScalePoint<{ toString(): string }>;
|
||||
[ScaleType.BAND]: ScaleBand<{ toString(): string }>;
|
||||
}
|
||||
|
||||
return scaleFn;
|
||||
}
|
||||
} else if (type === 'nominal') {
|
||||
return CategoricalColorNamespace.getScale(undefined, namespace);
|
||||
}
|
||||
// eslint-disable-next-line complexity
|
||||
export function deriveScaleTypeFromDataTypeAndChannelType(
|
||||
dataType: Type | undefined,
|
||||
channelType: ChannelType,
|
||||
isBinned: boolean = false,
|
||||
): ScaleType | undefined {
|
||||
if (typeof dataType === 'undefined') {
|
||||
return undefined;
|
||||
} else if (dataType === 'nominal' || dataType === 'ordinal') {
|
||||
switch (channelType) {
|
||||
case 'XBand':
|
||||
case 'YBand':
|
||||
return ScaleType.POINT;
|
||||
case 'X':
|
||||
case 'Y':
|
||||
case 'Numeric':
|
||||
return ScaleType.POINT;
|
||||
case 'Color':
|
||||
case 'Category':
|
||||
return ScaleType.ORDINAL;
|
||||
default:
|
||||
}
|
||||
} else if (dataType === 'quantitative') {
|
||||
switch (channelType) {
|
||||
case 'XBand':
|
||||
case 'YBand':
|
||||
case 'X':
|
||||
case 'Y':
|
||||
case 'Numeric':
|
||||
return ScaleType.LINEAR;
|
||||
case 'Color':
|
||||
return isBinned ? ScaleType.LINEAR : ScaleType.BIN_ORDINAL;
|
||||
default:
|
||||
}
|
||||
} else if (dataType === 'temporal') {
|
||||
switch (channelType) {
|
||||
case 'XBand':
|
||||
case 'YBand':
|
||||
case 'X':
|
||||
case 'Y':
|
||||
case 'Numeric':
|
||||
return ScaleType.TIME;
|
||||
case 'Color':
|
||||
return ScaleType.LINEAR;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
function createScaleFromType<Output>(type: ScaleType) {
|
||||
switch (type) {
|
||||
case ScaleType.LINEAR:
|
||||
return scaleLinear<Output>();
|
||||
case ScaleType.LOG:
|
||||
return scaleLog<Output>();
|
||||
case ScaleType.POW:
|
||||
return scalePow<Output>();
|
||||
case ScaleType.SQRT:
|
||||
return scaleSqrt<Output>();
|
||||
case ScaleType.SYMLOG:
|
||||
return undefined;
|
||||
case ScaleType.TIME:
|
||||
return scaleTime<Output>();
|
||||
case ScaleType.UTC:
|
||||
return scaleUtc<Output>();
|
||||
case ScaleType.QUANTILE:
|
||||
return scaleQuantile<Output>();
|
||||
case ScaleType.QUANTIZE:
|
||||
return scaleQuantize<Output>();
|
||||
case ScaleType.THRESHOLD:
|
||||
return scaleThreshold<number | string | Date, Output>();
|
||||
case ScaleType.BIN_ORDINAL:
|
||||
return scaleOrdinal<{ toString(): string }, Output>();
|
||||
case ScaleType.ORDINAL:
|
||||
return scaleOrdinal<{ toString(): string }, Output>();
|
||||
case ScaleType.POINT:
|
||||
return scalePoint<{ toString(): string }>();
|
||||
case ScaleType.BAND:
|
||||
return scaleBand<{ toString(): string }>();
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
function createScale<Output extends Value>(
|
||||
channelType: ChannelType,
|
||||
scaleType: ScaleType,
|
||||
config: Scale<Output>,
|
||||
) {
|
||||
const { namespace } = config;
|
||||
|
||||
if (channelType === 'Color') {
|
||||
const { scheme } = config;
|
||||
|
||||
return typeof scheme === 'string' || typeof scheme === 'undefined'
|
||||
? CategoricalColorNamespace.getScale(scheme, namespace)
|
||||
: // TODO: fully use SchemeParams
|
||||
CategoricalColorNamespace.getScale(scheme.name, namespace);
|
||||
}
|
||||
|
||||
const scale = createScaleFromType<Output>(scaleType);
|
||||
|
||||
if (typeof scale !== 'undefined') {
|
||||
if (scale.domain && typeof config.domain !== 'undefined') {
|
||||
scale.domain(config.domain as any[]);
|
||||
}
|
||||
if (scale.range && typeof config.range !== 'undefined') {
|
||||
scale.range(config.range as any[]);
|
||||
}
|
||||
if ('nice' in scale && scale.nice && config.nice !== false) {
|
||||
scale.nice();
|
||||
}
|
||||
if (
|
||||
'clamp' in scale &&
|
||||
typeof scale.clamp !== 'undefined' &&
|
||||
typeof config.clamp !== 'undefined'
|
||||
) {
|
||||
scale.clamp(config.clamp);
|
||||
}
|
||||
}
|
||||
|
||||
return scale;
|
||||
}
|
||||
|
||||
export default function extractScale<Output extends Value>(
|
||||
channelType: ChannelType,
|
||||
definition: ChannelDef<Output>,
|
||||
namespace?: string,
|
||||
) {
|
||||
if (isNonValueDef(definition)) {
|
||||
const scaleConfig =
|
||||
'scale' in definition && typeof definition.scale !== 'undefined' ? definition.scale : {};
|
||||
|
||||
// return if scale is disabled
|
||||
if (isDisabled(scaleConfig)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let scaleType = scaleConfig.type;
|
||||
|
||||
if (typeof scaleType === 'undefined') {
|
||||
// If scale type is not defined, try to derive scale type from field type
|
||||
const dataType = 'type' in definition ? definition.type : undefined;
|
||||
scaleType = deriveScaleTypeFromDataTypeAndChannelType(dataType, channelType);
|
||||
|
||||
// If still do not have scale type, cannot create scale
|
||||
if (typeof scaleType === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const scale = createScale(channelType, scaleType, { namespace, ...scaleConfig });
|
||||
|
||||
if (scale) {
|
||||
const setDomain =
|
||||
scale instanceof CategoricalColorScale || typeof scale.domain === 'undefined'
|
||||
? () => {}
|
||||
: scale.domain;
|
||||
|
||||
return {
|
||||
config: { ...scaleConfig, type: scaleType },
|
||||
encodeValue: (scale as unknown) as (
|
||||
value: number | string | boolean | null | undefined | Date,
|
||||
) => Output,
|
||||
scale,
|
||||
setDomain,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ValueDef does not have scale
|
||||
return undefined;
|
||||
}
|
||||
|
@ -1,22 +1,38 @@
|
||||
interface Axis {
|
||||
title: string;
|
||||
import { DateTime } from 'vega-lite/build/src/datetime';
|
||||
import { AxisOrient } from 'vega';
|
||||
|
||||
export type LabelOverlapStrategy = 'auto' | 'flat' | 'rotate';
|
||||
|
||||
export interface CoreAxis {
|
||||
format?: string;
|
||||
labelAngle: number;
|
||||
labelOverlap: LabelOverlapStrategy;
|
||||
/** The padding, in pixels, between axis and text labels. */
|
||||
labelPadding: number;
|
||||
orient: AxisOrient;
|
||||
tickCount: number;
|
||||
format: string;
|
||||
title?: string;
|
||||
/** Explicitly set the visible axis tick values. */
|
||||
values?: string[] | number[] | boolean[] | DateTime[];
|
||||
}
|
||||
|
||||
export type XAxis = Axis & {
|
||||
orient: 'top' | 'bottom';
|
||||
labelAngle: number;
|
||||
labelOverlap: string;
|
||||
};
|
||||
export type Axis = Partial<CoreAxis>;
|
||||
|
||||
export interface XAxis extends Axis {
|
||||
orient?: 'top' | 'bottom';
|
||||
labelAngle?: number;
|
||||
labelOverlap?: LabelOverlapStrategy;
|
||||
}
|
||||
|
||||
export interface WithXAxis {
|
||||
axis?: XAxis;
|
||||
}
|
||||
|
||||
export type YAxis = Axis & {
|
||||
orient: 'left' | 'right';
|
||||
};
|
||||
export interface YAxis extends Axis {
|
||||
orient?: 'left' | 'right';
|
||||
labelAngle?: 0;
|
||||
labelOverlap?: 'auto' | 'flat';
|
||||
}
|
||||
|
||||
export interface WithYAxis {
|
||||
axis?: YAxis;
|
||||
@ -25,3 +41,7 @@ export interface WithYAxis {
|
||||
export interface WithAxis {
|
||||
axis?: XAxis | YAxis;
|
||||
}
|
||||
|
||||
export function isAxis(axis: Axis | null | undefined | false): axis is Axis {
|
||||
return axis !== false && axis !== null && axis !== undefined;
|
||||
}
|
||||
|
@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export type ObjectWithKeysFromAndValueType<T extends {}, V> = { [key in keyof T]: V };
|
@ -1,5 +1,44 @@
|
||||
import { Value } from 'vega-lite/build/src/fielddef';
|
||||
import { XFieldDef, YFieldDef, ChannelDef, MarkPropChannelDef, TextChannelDef } from './FieldDef';
|
||||
import { ObjectWithKeysFromAndValueType } from './Base';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export interface ChannelOptions {
|
||||
namespace?: string;
|
||||
legend?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define all channel types and mapping to available definition grammar
|
||||
*/
|
||||
export interface ChannelTypeToDefMap<Output extends Value = Value>
|
||||
extends ObjectWithKeysFromAndValueType<ChannelTypeToDefMap<Output>, ChannelDef> {
|
||||
// position on x-axis
|
||||
X: XFieldDef<Output>;
|
||||
// position on y-axis
|
||||
Y: YFieldDef<Output>;
|
||||
// position on x-axis but as a range, e.g., bar chart or heat map
|
||||
XBand: XFieldDef<Output>;
|
||||
// position on y-axis but as a range, e.g., bar chart or heat map
|
||||
YBand: YFieldDef<Output>;
|
||||
// numeric attributes of the mark, e.g., size, opacity
|
||||
Numeric: MarkPropChannelDef<Output>;
|
||||
// categorical attributes of the mark, e.g., color, visibility, shape
|
||||
Category: MarkPropChannelDef<Output>;
|
||||
// color of the mark
|
||||
Color: MarkPropChannelDef<Output>;
|
||||
// plain text, e.g., tooltip, key
|
||||
Text: TextChannelDef<Output>;
|
||||
}
|
||||
|
||||
export type ChannelType = keyof ChannelTypeToDefMap;
|
||||
|
||||
export type ChannelDefFromType<
|
||||
T extends keyof ChannelTypeToDefMap,
|
||||
Output extends Value
|
||||
> = ChannelTypeToDefMap<Output>[T];
|
||||
|
||||
export type EncodingFromChannelsAndOutputs<
|
||||
Channels extends ObjectWithKeysFromAndValueType<Outputs, ChannelType>,
|
||||
Outputs extends ObjectWithKeysFromAndValueType<Channels, Value>
|
||||
> = { [key in keyof Channels]: ChannelDefFromType<Channels[key], Outputs[key]> };
|
||||
|
@ -14,6 +14,7 @@ export type Formatter = NumberFormatter | TimeFormatter | ((d: any) => string);
|
||||
export interface FieldDef {
|
||||
field: string;
|
||||
format?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface TypedFieldDef extends FieldDef {
|
||||
@ -22,8 +23,6 @@ export interface TypedFieldDef extends FieldDef {
|
||||
|
||||
export type TextFieldDef = FieldDef;
|
||||
|
||||
// PropFieldDef is { field: 'fieldName', scale: xxx }
|
||||
|
||||
type ScaleFieldDef<Output extends Value = Value> = TypedFieldDef & WithScale<Output>;
|
||||
|
||||
export type MarkPropFieldDef<Output extends Value = Value> = ScaleFieldDef<Output> & WithLegend;
|
||||
@ -42,19 +41,26 @@ export type MarkPropChannelDef<Output extends Value = Value> =
|
||||
| MarkPropFieldDef<Output>
|
||||
| ValueDef<Output>;
|
||||
|
||||
export type TextChannelDef = TextFieldDef | ValueDef<string>;
|
||||
export type TextChannelDef<Output extends Value = Value> = TextFieldDef | ValueDef<Output>;
|
||||
|
||||
export type ChannelDef<Output extends Value = Value> =
|
||||
export type NonValueDef<Output extends Value = Value> =
|
||||
| XFieldDef<Output>
|
||||
| YFieldDef<Output>
|
||||
| MarkPropFieldDef<Output>
|
||||
| TextFieldDef
|
||||
| ValueDef<Output>;
|
||||
| TextFieldDef;
|
||||
|
||||
export type ChannelDef<Output extends Value = Value> = NonValueDef<Output> | ValueDef<Output>;
|
||||
|
||||
export function isValueDef<Output extends Value>(
|
||||
channelDef: ChannelDef<Output>,
|
||||
): channelDef is ValueDef<Output> {
|
||||
return channelDef && 'value' in channelDef && !!channelDef.value;
|
||||
return channelDef && 'value' in channelDef;
|
||||
}
|
||||
|
||||
export function isNonValueDef<Output extends Value>(
|
||||
channelDef: ChannelDef<Output>,
|
||||
): channelDef is NonValueDef<Output> {
|
||||
return channelDef && !('value' in channelDef);
|
||||
}
|
||||
|
||||
export function isFieldDef<Output extends Value>(
|
||||
|
@ -1,11 +1,16 @@
|
||||
import { ScaleType } from 'vega-lite/build/src/scale';
|
||||
import { Value } from 'vega-lite/build/src/fielddef';
|
||||
import { DateTime } from 'vega-lite/build/src/datetime';
|
||||
import { SchemeParams, ScaleType } from 'vega-lite/build/src/scale';
|
||||
|
||||
export interface Scale<Output extends Value = Value> {
|
||||
type?: ScaleType;
|
||||
domain?: any[];
|
||||
domain?: number[] | string[] | boolean[] | DateTime[];
|
||||
range?: Output[];
|
||||
scheme?: string;
|
||||
clamp?: boolean;
|
||||
nice?: boolean;
|
||||
scheme?: string | SchemeParams;
|
||||
// vega-lite does not have this
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
export interface WithScale<Output extends Value = Value> {
|
||||
|
@ -0,0 +1,2 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { default as LineChartPlugin } from './Line/legacy';
|
@ -1,140 +0,0 @@
|
||||
/* eslint-disable sort-keys, no-magic-numbers */
|
||||
|
||||
import React from 'react';
|
||||
import collectScalesFromProps from '@data-ui/xy-chart/esm/utils/collectScalesFromProps';
|
||||
import { XAxis, YAxis } from '@data-ui/xy-chart';
|
||||
import adjustMargin from './adjustMargin';
|
||||
import computeXAxisLayout from './computeXAxisLayout';
|
||||
import computeYAxisLayout from './computeYAxisLayout';
|
||||
import createTickComponent from './createTickComponent';
|
||||
import getTickLabels from './getTickLabels';
|
||||
import ChartFrame from '../components/ChartFrame';
|
||||
|
||||
// Additional margin to avoid content hidden behind scroll bar
|
||||
const OVERFLOW_MARGIN = 8;
|
||||
|
||||
export default class XYChartLayout {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
minContentWidth = 0,
|
||||
minContentHeight = 0,
|
||||
margin,
|
||||
encoding,
|
||||
children,
|
||||
theme,
|
||||
} = config;
|
||||
const { x, y } = encoding;
|
||||
|
||||
const { xScale, yScale } = collectScalesFromProps({
|
||||
width,
|
||||
height,
|
||||
margin,
|
||||
xScale: x.scale,
|
||||
yScale: y.scale,
|
||||
theme,
|
||||
children,
|
||||
});
|
||||
|
||||
const { axis: yAxis = {} } = y;
|
||||
|
||||
const yLayout = computeYAxisLayout({
|
||||
orientation: yAxis.orientation,
|
||||
tickLabels: getTickLabels(yScale, yAxis),
|
||||
tickLength: theme.yTickStyles.length,
|
||||
tickTextStyle: theme.yTickStyles.label.right,
|
||||
});
|
||||
|
||||
const secondMargin = adjustMargin(margin, yLayout.minMargin);
|
||||
const { left, right } = secondMargin;
|
||||
|
||||
const { axis: xAxis = {} } = x;
|
||||
|
||||
const { orientation: xOrientation = 'bottom' } = xAxis;
|
||||
const AUTO_ROTATION =
|
||||
(yLayout.orientation === 'right' && xOrientation === 'bottom') ||
|
||||
(yLayout.orientation === 'left' && xOrientation === 'top')
|
||||
? 40
|
||||
: -40;
|
||||
const { rotation = AUTO_ROTATION } = xAxis;
|
||||
|
||||
const innerWidth = Math.max(width - left - right, minContentWidth);
|
||||
|
||||
const xLayout = computeXAxisLayout({
|
||||
axisWidth: innerWidth,
|
||||
orientation: xOrientation,
|
||||
rotation,
|
||||
tickLabels: getTickLabels(xScale, xAxis),
|
||||
tickLength: theme.xTickStyles.length,
|
||||
tickTextStyle: theme.xTickStyles.label.bottom,
|
||||
});
|
||||
|
||||
const finalMargin = adjustMargin(secondMargin, xLayout.minMargin);
|
||||
const innerHeight = Math.max(height - finalMargin.top - finalMargin.bottom, minContentHeight);
|
||||
|
||||
const chartWidth = Math.round(innerWidth + finalMargin.left + finalMargin.right);
|
||||
const chartHeight = Math.round(innerHeight + finalMargin.top + finalMargin.bottom);
|
||||
|
||||
const isOverFlowX = chartWidth > width;
|
||||
const isOverFlowY = chartHeight > height;
|
||||
if (isOverFlowX) {
|
||||
finalMargin.bottom += OVERFLOW_MARGIN;
|
||||
}
|
||||
if (isOverFlowY) {
|
||||
finalMargin.right += OVERFLOW_MARGIN;
|
||||
}
|
||||
|
||||
this.chartWidth = isOverFlowX ? chartWidth + OVERFLOW_MARGIN : chartWidth;
|
||||
this.chartHeight = isOverFlowY ? chartHeight + OVERFLOW_MARGIN : chartHeight;
|
||||
this.containerWidth = width;
|
||||
this.containerHeight = height;
|
||||
this.margin = finalMargin;
|
||||
this.xLayout = xLayout;
|
||||
this.yLayout = yLayout;
|
||||
}
|
||||
|
||||
createChartWithFrame(renderChart) {
|
||||
return (
|
||||
<ChartFrame
|
||||
width={this.containerWidth}
|
||||
height={this.containerHeight}
|
||||
containerWidth={this.containerWidth}
|
||||
contentHeight={this.containerHeight}
|
||||
renderContent={renderChart}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
createXAxis(props) {
|
||||
const { axis } = this.config.encoding.x;
|
||||
|
||||
return (
|
||||
<XAxis
|
||||
label={axis.label}
|
||||
labelOffset={this.xLayout.labelOffset}
|
||||
orientation={this.xLayout.orientation}
|
||||
tickComponent={createTickComponent(this.xLayout)}
|
||||
tickFormat={axis.tickFormat}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
createYAxis(props) {
|
||||
const { axis } = this.config.encoding.y;
|
||||
|
||||
return (
|
||||
<YAxis
|
||||
label={axis.label}
|
||||
labelOffset={this.yLayout.labelOffset}
|
||||
numTicks={axis.numTicks}
|
||||
orientation={this.yLayout.orientation}
|
||||
tickFormat={axis.tickFormat}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,182 @@
|
||||
/* eslint-disable sort-keys, no-magic-numbers */
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import collectScalesFromProps from '@data-ui/xy-chart/esm/utils/collectScalesFromProps';
|
||||
import { XAxis, YAxis } from '@data-ui/xy-chart';
|
||||
import { ChartTheme } from '@data-ui/theme';
|
||||
import { Margin, mergeMargin } from '@superset-ui/dimension';
|
||||
import { AxisOrient } from 'vega';
|
||||
import createTickComponent from './createTickComponent';
|
||||
import ChartFrame from '../components/ChartFrame';
|
||||
import ChannelEncoder from '../encodeable/ChannelEncoder';
|
||||
import { XFieldDef, YFieldDef } from '../encodeable/types/FieldDef';
|
||||
import { PlainObject } from '../encodeable/types/Data';
|
||||
import { DEFAULT_LABEL_ANGLE } from './constants';
|
||||
|
||||
// Additional margin to avoid content hidden behind scroll bar
|
||||
const OVERFLOW_MARGIN = 8;
|
||||
|
||||
interface Input {
|
||||
width: number;
|
||||
height: number;
|
||||
minContentWidth?: number;
|
||||
minContentHeight?: number;
|
||||
margin: Margin;
|
||||
xEncoder: ChannelEncoder<XFieldDef>;
|
||||
yEncoder: ChannelEncoder<YFieldDef>;
|
||||
children: ReactNode[];
|
||||
theme: ChartTheme;
|
||||
}
|
||||
|
||||
export default class XYChartLayout {
|
||||
chartWidth: number;
|
||||
chartHeight: number;
|
||||
containerWidth: number;
|
||||
containerHeight: number;
|
||||
margin: Margin;
|
||||
spec: Input;
|
||||
|
||||
xLayout?: {
|
||||
labelOffset: number;
|
||||
labelOverlap: string;
|
||||
labelAngle: number;
|
||||
tickTextAnchor?: string;
|
||||
minMargin: Partial<Margin>;
|
||||
orient: AxisOrient;
|
||||
};
|
||||
|
||||
yLayout?: {
|
||||
labelOffset: number;
|
||||
minMargin: Partial<Margin>;
|
||||
orient: AxisOrient;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
constructor(spec: Input) {
|
||||
this.spec = spec;
|
||||
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
minContentWidth = 0,
|
||||
minContentHeight = 0,
|
||||
margin,
|
||||
xEncoder,
|
||||
yEncoder,
|
||||
children,
|
||||
theme,
|
||||
} = spec;
|
||||
|
||||
const { xScale, yScale } = collectScalesFromProps({
|
||||
width,
|
||||
height,
|
||||
margin,
|
||||
xScale: xEncoder.definition.scale || {},
|
||||
yScale: yEncoder.definition.scale || {},
|
||||
theme,
|
||||
children,
|
||||
});
|
||||
|
||||
if (typeof yEncoder.scale !== 'undefined') {
|
||||
yEncoder.scale.setDomain(yScale.domain());
|
||||
}
|
||||
if (typeof yEncoder.axis !== 'undefined') {
|
||||
this.yLayout = yEncoder.axis.computeLayout({
|
||||
axisWidth: Math.max(height - margin.top - margin.bottom),
|
||||
tickLength: theme.yTickStyles.length,
|
||||
tickTextStyle: theme.yTickStyles.label.right,
|
||||
});
|
||||
}
|
||||
|
||||
const secondMargin = this.yLayout ? mergeMargin(margin, this.yLayout.minMargin) : margin;
|
||||
const innerWidth = Math.max(width - secondMargin.left - secondMargin.right, minContentWidth);
|
||||
|
||||
if (typeof xEncoder.scale !== 'undefined') {
|
||||
xEncoder.scale.setDomain(xScale.domain());
|
||||
}
|
||||
if (typeof xEncoder.axis !== 'undefined') {
|
||||
this.xLayout = xEncoder.axis.computeLayout({
|
||||
axisWidth: innerWidth,
|
||||
labelAngle: this.recommendXLabelAngle(xEncoder.axis.config.orient as 'top' | 'bottom'),
|
||||
tickLength: theme.xTickStyles.length,
|
||||
tickTextStyle: theme.xTickStyles.label.bottom,
|
||||
});
|
||||
}
|
||||
|
||||
const finalMargin = this.xLayout
|
||||
? mergeMargin(secondMargin, this.xLayout.minMargin)
|
||||
: secondMargin;
|
||||
const innerHeight = Math.max(height - finalMargin.top - finalMargin.bottom, minContentHeight);
|
||||
|
||||
const chartWidth = Math.round(innerWidth + finalMargin.left + finalMargin.right);
|
||||
const chartHeight = Math.round(innerHeight + finalMargin.top + finalMargin.bottom);
|
||||
|
||||
const isOverFlowX = chartWidth > width;
|
||||
const isOverFlowY = chartHeight > height;
|
||||
if (isOverFlowX) {
|
||||
finalMargin.bottom += OVERFLOW_MARGIN;
|
||||
}
|
||||
if (isOverFlowY) {
|
||||
finalMargin.right += OVERFLOW_MARGIN;
|
||||
}
|
||||
this.chartWidth = isOverFlowX ? chartWidth + OVERFLOW_MARGIN : chartWidth;
|
||||
this.chartHeight = isOverFlowY ? chartHeight + OVERFLOW_MARGIN : chartHeight;
|
||||
this.containerWidth = width;
|
||||
this.containerHeight = height;
|
||||
this.margin = finalMargin;
|
||||
}
|
||||
|
||||
recommendXLabelAngle(xOrient: 'top' | 'bottom' = 'bottom') {
|
||||
const { axis } = this.spec.yEncoder;
|
||||
|
||||
return !this.yLayout ||
|
||||
(typeof axis !== 'undefined' &&
|
||||
((axis.config.orient === 'right' && xOrient === 'bottom') ||
|
||||
(axis.config.orient === 'left' && xOrient === 'top')))
|
||||
? DEFAULT_LABEL_ANGLE
|
||||
: -DEFAULT_LABEL_ANGLE;
|
||||
}
|
||||
|
||||
renderChartWithFrame(renderChart: (input: { width: number; height: number }) => ReactNode) {
|
||||
return (
|
||||
<ChartFrame
|
||||
width={this.containerWidth}
|
||||
height={this.containerHeight}
|
||||
contentWidth={this.chartWidth}
|
||||
contentHeight={this.chartHeight}
|
||||
renderContent={renderChart}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderXAxis(props?: PlainObject) {
|
||||
const { axis } = this.spec.xEncoder;
|
||||
|
||||
return axis && this.xLayout ? (
|
||||
<XAxis
|
||||
label={axis.getTitle()}
|
||||
labelOffset={this.xLayout.labelOffset}
|
||||
numTicks={axis.config.tickCount}
|
||||
orientation={axis.config.orient}
|
||||
tickComponent={createTickComponent(this.xLayout)}
|
||||
tickFormat={axis.getFormat()}
|
||||
{...props}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
renderYAxis(props?: PlainObject) {
|
||||
const { axis } = this.spec.yEncoder;
|
||||
|
||||
return axis && this.yLayout ? (
|
||||
<YAxis
|
||||
label={axis.getTitle()}
|
||||
labelOffset={this.yLayout.labelOffset}
|
||||
numTicks={axis.config.tickCount}
|
||||
orientation={axis.config.orient}
|
||||
tickFormat={axis.getFormat()}
|
||||
{...props}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
interface Margin {
|
||||
top: number;
|
||||
left: number;
|
||||
bottom: number;
|
||||
right: number;
|
||||
}
|
||||
|
||||
export default function adjustMargin(
|
||||
baseMargin: Partial<Margin> = {},
|
||||
minMargin: Partial<Margin> = {},
|
||||
) {
|
||||
const { top = 0, left = 0, bottom = 0, right = 0 } = baseMargin;
|
||||
const {
|
||||
top: minTop = 0,
|
||||
left: minLeft = 0,
|
||||
bottom: minBottom = 0,
|
||||
right: minRight = 0,
|
||||
} = minMargin;
|
||||
|
||||
return {
|
||||
bottom: Math.max(bottom, minBottom),
|
||||
left: Math.max(left, minLeft),
|
||||
right: Math.max(right, minRight),
|
||||
top: Math.max(top, minTop),
|
||||
};
|
||||
}
|
@ -1,147 +0,0 @@
|
||||
/* eslint-disable sort-keys, no-magic-numbers */
|
||||
|
||||
import React from 'react';
|
||||
import collectScalesFromProps from '@data-ui/xy-chart/esm/utils/collectScalesFromProps';
|
||||
import { XAxis, YAxis } from '@data-ui/xy-chart';
|
||||
import adjustMargin from './adjustMargin';
|
||||
import computeXAxisLayout from './computeXAxisLayout';
|
||||
import computeYAxisLayout from './computeYAxisLayout';
|
||||
import createTickComponent from './createTickComponent';
|
||||
import getTickLabels from './getTickLabels';
|
||||
|
||||
const OVERFLOW_MARGIN = 8;
|
||||
|
||||
// {
|
||||
// width,
|
||||
// height,
|
||||
// margin:
|
||||
// encoding: {
|
||||
// x: {
|
||||
// scale:
|
||||
// axis: {
|
||||
// labellingStrategy:
|
||||
// rotation:
|
||||
// orientation:
|
||||
// scaleConfig:
|
||||
// tickFormat:
|
||||
// tickValues:
|
||||
// numTicks:
|
||||
// }
|
||||
// },
|
||||
// y: {
|
||||
// scale:
|
||||
// axis: {
|
||||
// tickFormat:
|
||||
// tickValues:
|
||||
// numTicks:
|
||||
// orientation:
|
||||
// }
|
||||
// },
|
||||
// }
|
||||
// children:
|
||||
// theme:
|
||||
// }
|
||||
|
||||
export default function computeChartLayout(config) {
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
minContentWidth = 0,
|
||||
minContentHeight = 0,
|
||||
margin,
|
||||
encoding,
|
||||
children,
|
||||
theme,
|
||||
} = config;
|
||||
const { x, y } = encoding;
|
||||
|
||||
const { xScale, yScale } = collectScalesFromProps({
|
||||
width,
|
||||
height,
|
||||
margin,
|
||||
xScale: x.scale,
|
||||
yScale: y.scale,
|
||||
theme,
|
||||
children,
|
||||
});
|
||||
|
||||
const { axis: yAxis = {} } = y;
|
||||
|
||||
const yLayout = computeYAxisLayout({
|
||||
orientation: yAxis.orientation,
|
||||
tickLabels: getTickLabels(yScale, y.axis),
|
||||
tickLength: theme.xTickStyles.length,
|
||||
tickTextStyle: theme.yTickStyles.label.right,
|
||||
});
|
||||
|
||||
const secondMargin = adjustMargin(margin, yLayout.minMargin);
|
||||
const { left, right } = secondMargin;
|
||||
|
||||
const innerWidth = Math.max(width - left - right, minContentWidth);
|
||||
|
||||
const { axis: xAxis = {} } = x;
|
||||
const { orientation: xOrientation = 'bottom' } = xAxis;
|
||||
const AUTO_ROTATION =
|
||||
(yLayout.orientation === 'right' && xOrientation === 'bottom') ||
|
||||
(yLayout.orientation === 'left' && xOrientation === 'top')
|
||||
? 40
|
||||
: -40;
|
||||
const { rotation = AUTO_ROTATION } = xAxis;
|
||||
|
||||
const xLayout = computeXAxisLayout({
|
||||
axisWidth: innerWidth,
|
||||
orientation: xOrientation,
|
||||
rotation,
|
||||
tickLabels: getTickLabels(xScale, x.axis),
|
||||
tickLength: theme.xTickStyles.length,
|
||||
tickTextStyle: theme.xTickStyles.label.bottom,
|
||||
});
|
||||
|
||||
const finalMargin = adjustMargin(secondMargin, xLayout.minMargin);
|
||||
const innerHeight = Math.max(height - finalMargin.top - finalMargin.bottom, minContentHeight);
|
||||
|
||||
const createXAxis = props => (
|
||||
<XAxis
|
||||
label={config.encoding.x.axis.label}
|
||||
labelOffset={xLayout.labelOffset}
|
||||
orientation={xLayout.orientation}
|
||||
tickComponent={createTickComponent(xLayout)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const createYAxis = props => (
|
||||
<YAxis
|
||||
label={config.encoding.y.axis.label}
|
||||
labelOffset={yLayout.labelOffset}
|
||||
numTicks={config.encoding.y.axis.numTicks}
|
||||
orientation={yLayout.orientation}
|
||||
tickFormat={config.encoding.y.axis.tickFormat}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const chartWidth = Math.round(innerWidth + finalMargin.left + finalMargin.right);
|
||||
const chartHeight = Math.round(innerHeight + finalMargin.top + finalMargin.bottom);
|
||||
|
||||
const isOverFlowX = chartWidth > width;
|
||||
const isOverFlowY = chartHeight > height;
|
||||
if (isOverFlowX) {
|
||||
finalMargin.bottom += OVERFLOW_MARGIN;
|
||||
}
|
||||
if (isOverFlowY) {
|
||||
finalMargin.right += OVERFLOW_MARGIN;
|
||||
}
|
||||
|
||||
return {
|
||||
chartWidth: isOverFlowX ? chartWidth + OVERFLOW_MARGIN : chartWidth,
|
||||
chartHeight: isOverFlowY ? chartHeight + OVERFLOW_MARGIN : chartHeight,
|
||||
containerWidth: width,
|
||||
containerHeight: height,
|
||||
margin: finalMargin,
|
||||
x: xLayout,
|
||||
y: yLayout,
|
||||
createXAxis,
|
||||
createYAxis,
|
||||
};
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
/* eslint-disable no-magic-numbers */
|
||||
|
||||
import { getTextDimension } from '@superset-ui/dimension';
|
||||
|
||||
export default function computeXAxisLayout({
|
||||
axisLabelHeight = 20,
|
||||
axisWidth,
|
||||
gapBetweenAxisLabelAndBorder = 8,
|
||||
gapBetweenTickAndTickLabel = 4,
|
||||
gapBetweenTickLabelsAndAxisLabel = 4,
|
||||
labellingStrategy = 'auto',
|
||||
orientation = 'bottom',
|
||||
rotation = -40,
|
||||
tickLabels,
|
||||
tickLength,
|
||||
tickTextStyle,
|
||||
}) {
|
||||
const labelDimensions = tickLabels.map(text =>
|
||||
getTextDimension({
|
||||
style: tickTextStyle,
|
||||
text,
|
||||
}),
|
||||
);
|
||||
|
||||
const maxWidth = Math.max(...labelDimensions.map(d => d.width));
|
||||
// cheap heuristic, can improve
|
||||
const widthPerTick = axisWidth / tickLabels.length;
|
||||
|
||||
let finalStrategy;
|
||||
if (labellingStrategy !== 'auto') {
|
||||
finalStrategy = labellingStrategy;
|
||||
} else if (maxWidth <= widthPerTick) {
|
||||
finalStrategy = 'flat';
|
||||
} else {
|
||||
finalStrategy = 'rotate';
|
||||
}
|
||||
// TODO: Add other strategies: stagger, chop, wrap.
|
||||
|
||||
let layout = { labelOffset: 0 };
|
||||
if (finalStrategy === 'flat') {
|
||||
const labelHeight = labelDimensions[0].height;
|
||||
const labelOffset = labelHeight + gapBetweenTickLabelsAndAxisLabel;
|
||||
layout = { labelOffset };
|
||||
} else if (finalStrategy === 'rotate') {
|
||||
const labelHeight = Math.ceil(Math.abs(maxWidth * Math.sin((rotation * Math.PI) / 180)));
|
||||
const labelOffset = labelHeight + gapBetweenTickLabelsAndAxisLabel;
|
||||
const tickTextAnchor =
|
||||
(orientation === 'top' && rotation > 0) || (orientation === 'bottom' && rotation < 0)
|
||||
? 'end'
|
||||
: 'start';
|
||||
layout = {
|
||||
labelOffset,
|
||||
rotation,
|
||||
tickTextAnchor,
|
||||
};
|
||||
}
|
||||
|
||||
const { labelOffset } = layout;
|
||||
|
||||
return {
|
||||
...layout,
|
||||
labellingStrategy: finalStrategy,
|
||||
minMargin: {
|
||||
[orientation]: Math.ceil(
|
||||
tickLength +
|
||||
gapBetweenTickAndTickLabel +
|
||||
labelOffset +
|
||||
axisLabelHeight +
|
||||
gapBetweenAxisLabelAndBorder +
|
||||
8,
|
||||
),
|
||||
},
|
||||
orientation,
|
||||
};
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
/* eslint-disable no-magic-numbers */
|
||||
|
||||
import { getTextDimension } from '@superset-ui/dimension';
|
||||
|
||||
export default function computeYAxisLayout({
|
||||
axisLabelHeight = 20,
|
||||
gapBetweenAxisLabelAndBorder = 8,
|
||||
gapBetweenTickAndTickLabel = 4,
|
||||
gapBetweenTickLabelsAndAxisLabel = 4,
|
||||
orientation = 'left',
|
||||
tickLabels,
|
||||
tickLength,
|
||||
tickTextStyle,
|
||||
}) {
|
||||
const labelDimensions = tickLabels.map(text =>
|
||||
getTextDimension({
|
||||
style: tickTextStyle,
|
||||
text,
|
||||
}),
|
||||
);
|
||||
|
||||
const maxWidth = Math.ceil(Math.max(...labelDimensions.map(d => d.width)));
|
||||
const labelOffset = Math.ceil(maxWidth + gapBetweenTickLabelsAndAxisLabel + axisLabelHeight);
|
||||
|
||||
const margin =
|
||||
tickLength + gapBetweenTickAndTickLabel + labelOffset + gapBetweenAxisLabelAndBorder;
|
||||
|
||||
return {
|
||||
labelOffset,
|
||||
minMargin: {
|
||||
[orientation]: margin,
|
||||
},
|
||||
orientation,
|
||||
};
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const DEFAULT_LABEL_ANGLE = 40;
|
@ -1,46 +0,0 @@
|
||||
/* eslint-disable no-magic-numbers */
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function createTickComponent({
|
||||
labellingStrategy,
|
||||
orientation = 'bottom',
|
||||
rotation = 40,
|
||||
tickTextAnchor = 'start',
|
||||
}) {
|
||||
if (labellingStrategy === 'rotate' && rotation !== 0) {
|
||||
let xOffset = rotation > 0 ? -6 : 6;
|
||||
if (orientation === 'top') {
|
||||
xOffset = 0;
|
||||
}
|
||||
const yOffset = orientation === 'top' ? -3 : 0;
|
||||
|
||||
const propTypes = {
|
||||
dy: PropTypes.number,
|
||||
formattedValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
x: PropTypes.number.isRequired,
|
||||
y: PropTypes.number.isRequired,
|
||||
};
|
||||
const defaultProps = {
|
||||
dy: null,
|
||||
formattedValue: '',
|
||||
};
|
||||
|
||||
const TickComponent = ({ x, y, dy, formattedValue, ...textStyle }) => (
|
||||
<g transform={`translate(${x + xOffset}, ${y + yOffset})`}>
|
||||
<text transform={`rotate(${rotation})`} {...textStyle} textAnchor={tickTextAnchor}>
|
||||
{formattedValue}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
|
||||
TickComponent.propTypes = propTypes;
|
||||
TickComponent.defaultProps = defaultProps;
|
||||
|
||||
return TickComponent;
|
||||
}
|
||||
|
||||
// This will render the tick as horizontal string.
|
||||
return null;
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/* eslint-disable no-magic-numbers */
|
||||
|
||||
import React, { CSSProperties } from 'react';
|
||||
|
||||
export default function createTickComponent({
|
||||
labelAngle,
|
||||
labelOverlap,
|
||||
orient,
|
||||
tickTextAnchor = 'start',
|
||||
}: {
|
||||
labelAngle: number;
|
||||
labelOverlap: string;
|
||||
orient: string;
|
||||
tickTextAnchor?: string;
|
||||
}) {
|
||||
if (labelOverlap === 'rotate' && labelAngle !== 0) {
|
||||
let xOffset = labelAngle > 0 ? -6 : 6;
|
||||
if (orient === 'top') {
|
||||
xOffset = 0;
|
||||
}
|
||||
const yOffset = orient === 'top' ? -3 : 0;
|
||||
|
||||
const TickComponent = ({
|
||||
x,
|
||||
y,
|
||||
dy,
|
||||
formattedValue = '',
|
||||
...textStyle
|
||||
}: {
|
||||
x: number;
|
||||
y: number;
|
||||
dy?: number;
|
||||
formattedValue: string;
|
||||
textStyle: CSSProperties;
|
||||
}) => (
|
||||
<g transform={`translate(${x + xOffset}, ${y + yOffset})`}>
|
||||
<text transform={`rotate(${labelAngle})`} {...textStyle} textAnchor={tickTextAnchor}>
|
||||
{formattedValue}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
|
||||
return TickComponent;
|
||||
}
|
||||
|
||||
// This will render the tick as horizontal string.
|
||||
return null;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/* eslint-disable no-magic-numbers */
|
||||
|
||||
export default function createTickLabelProps({
|
||||
labelAngle,
|
||||
labelOverlap,
|
||||
orient,
|
||||
tickTextAnchor = 'start',
|
||||
}: {
|
||||
labelAngle: number;
|
||||
labelOverlap: string;
|
||||
orient: string;
|
||||
tickTextAnchor?: string;
|
||||
}) {
|
||||
let dx = 0;
|
||||
let dy = 0;
|
||||
if (labelOverlap === 'rotate' && labelAngle !== 0) {
|
||||
dx = labelAngle > 0 ? -6 : 6;
|
||||
if (orient === 'top') {
|
||||
dx = 0;
|
||||
}
|
||||
dy = orient === 'top' ? -3 : 0;
|
||||
}
|
||||
|
||||
return {
|
||||
angle: labelAngle,
|
||||
dx,
|
||||
dy,
|
||||
textAnchor: tickTextAnchor,
|
||||
};
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
/* eslint-disable no-magic-numbers */
|
||||
import identity from '@vx/axis/build/utils/identity';
|
||||
|
||||
export default function getTickLabels(scale, axisConfig) {
|
||||
const { numTicks = 10, tickValues, tickFormat } = axisConfig;
|
||||
let values = scale.ticks ? scale.ticks(numTicks) : scale.domain();
|
||||
if (tickValues) values = tickValues;
|
||||
|
||||
let format = scale.tickFormat ? scale.tickFormat() : identity;
|
||||
if (tickFormat) format = tickFormat;
|
||||
|
||||
return values.map(format);
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
import React from 'react';
|
||||
import { CategoricalColorNamespace } from '@superset-ui/color';
|
||||
import { LegendOrdinal, LegendItem, LegendLabel } from '@vx/legend';
|
||||
import { scaleOrdinal } from '@vx/scale';
|
||||
|
||||
export default function renderLegend(data, colorEncoding) {
|
||||
const { accessor, field, scale } = colorEncoding;
|
||||
const { scheme, namespace } = scale;
|
||||
const colorFn = CategoricalColorNamespace.getScale(scheme, namespace);
|
||||
const keySet = new Set();
|
||||
data.forEach(d => {
|
||||
keySet.add(accessor ? accessor(d) : d[field]);
|
||||
});
|
||||
const keys = [...keySet.values()];
|
||||
const colorScale = scaleOrdinal({
|
||||
domain: keys,
|
||||
range: keys.map(colorFn),
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
maxHeight: 100,
|
||||
overflowY: 'hidden',
|
||||
paddingLeft: 14,
|
||||
paddingTop: 6,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<LegendOrdinal scale={colorScale} labelFormat={label => label}>
|
||||
{labels => (
|
||||
<div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap' }}>
|
||||
{labels.map(label => {
|
||||
const size = 8;
|
||||
|
||||
return (
|
||||
<LegendItem
|
||||
key={`legend-quantile-${label.text}`}
|
||||
margin="0 5px"
|
||||
onClick={() => {
|
||||
alert(`clicked: ${JSON.stringify(label)}`);
|
||||
}}
|
||||
>
|
||||
<svg width={size} height={size} style={{ display: 'inline-block' }}>
|
||||
<rect fill={label.value} width={size} height={size} />
|
||||
</svg>
|
||||
<LegendLabel align="left" margin="0 0 0 4px">
|
||||
{label.text}
|
||||
</LegendLabel>
|
||||
</LegendItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</LegendOrdinal>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
|
||||
declare module '@data-ui/xy-chart/esm/utils/collectScalesFromProps' {
|
||||
import { ScaleLinear, ScaleBand, ScaleContinuousNumeric, ScaleDiverging, ScaleIdentity, ScaleLogarithmic, ScaleOrdinal, ScalePoint, ScalePower, ScaleQuantile, ScaleQuantize, ScaleSequential, ScaleThreshold, ScaleTime } from "d3-scale";
|
||||
import React from "react";
|
||||
import { ReactNode } from "react";
|
||||
import { ChartTheme } from "@data-ui/theme";
|
||||
|
||||
interface ScaleConfig {
|
||||
@ -26,7 +26,7 @@ declare module '@data-ui/xy-chart/esm/utils/collectScalesFromProps' {
|
||||
xScale: ScaleConfig,
|
||||
yScale: ScaleConfig,
|
||||
theme: ChartTheme,
|
||||
children: React.ReactElement[],
|
||||
children: ReactNode[],
|
||||
}): {
|
||||
xScale: Scale;
|
||||
yScale: Scale;
|
||||
|
@ -7,20 +7,25 @@ declare module '@data-ui/xy-chart' {
|
||||
};
|
||||
|
||||
interface XYChartProps {
|
||||
theme: any;
|
||||
width: number;
|
||||
height: number;
|
||||
ariaLabel: string;
|
||||
eventTrigger?: any;
|
||||
margin?: {
|
||||
top?: number;
|
||||
right?: number;
|
||||
bottom?: number;
|
||||
left?: number;
|
||||
};
|
||||
ariaLabel: string;
|
||||
onMouseMove?: (...args: any[]) => void;
|
||||
onMouseLeave?: (...args: any[]) => void;
|
||||
renderTooltip: any;
|
||||
showYGrid: boolean;
|
||||
snapTooltipToDataX: boolean;
|
||||
theme: any;
|
||||
tooltipData: any;
|
||||
xScale: any;
|
||||
yScale: any;
|
||||
renderTooltip: any;
|
||||
eventTrigger?: any;
|
||||
}
|
||||
|
||||
export class AreaSeries extends React.PureComponent<Props, {}> {}
|
||||
|
@ -1,16 +1,16 @@
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
declare module '@vx/legend' {
|
||||
import React from 'react';
|
||||
import { ReactNode, ReactElement } from 'react';
|
||||
|
||||
export function LegendOrdinal(props: { [key: string]: any }): React.ReactNode;
|
||||
export function LegendOrdinal(props: { [key: string]: any }): ReactElement;
|
||||
|
||||
export function LegendItem(props: { [key: string]: any }): React.ReactNode;
|
||||
export function LegendItem(props: { [key: string]: any }): ReactElement;
|
||||
|
||||
export function LegendLabel(props: {
|
||||
align: string;
|
||||
label?: React.ReactNode;
|
||||
label?: ReactNode;
|
||||
flex?: string | number;
|
||||
margin?: string | number;
|
||||
children?: React.ReactNode;
|
||||
}): React.ReactNode;
|
||||
children?: ReactNode;
|
||||
}): ReactElement;
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
declare module '@vx/responsive' {
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
interface ParentSizeProps {
|
||||
children: (renderProps: { width: number; height: number }) => React.ReactNode;
|
||||
}
|
||||
|
||||
const ParentSize: React.ComponentType<ParentSizeProps>;
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const ParentSize: React.ComponentType<ParentSizeProps>;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user