mirror of
https://github.com/apache/superset.git
synced 2024-09-17 11:09:47 -04:00
Implements Box Plot and XY Chart utilities (+10 squashed commits) Squashed commits: [878ed06] clean up some more [624bb7f] separate legend and chart [dfef0fa] working version [aaef60d] allow overflow [b696917] fix margins [2e12359] support top,left,bottom,right axes [278607b] create many utilities [88315c1] Many enhancements to BoxPlot [9d8eb80] box plot working [12d0d1e] new box plot working
This commit is contained in:
parent
b2f523fde1
commit
423edbe558
@ -37,11 +37,12 @@
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@data-ui/build-config": "^0.0.40",
|
||||
"@superset-ui/chart": "^0.9.x",
|
||||
"@superset-ui/color": "^0.9.x",
|
||||
"@superset-ui/number-format": "^0.9.x",
|
||||
"@superset-ui/time-format": "^0.9.x",
|
||||
"@superset-ui/translation": "^0.9.x",
|
||||
"@superset-ui/chart": "^0.9.6",
|
||||
"@superset-ui/color": "^0.9.5",
|
||||
"@superset-ui/connection": "^0.9.5",
|
||||
"@superset-ui/number-format": "^0.9.5",
|
||||
"@superset-ui/time-format": "^0.9.5",
|
||||
"@superset-ui/translation": "^0.9.5",
|
||||
"fast-glob": "^2.2.6",
|
||||
"fs-extra": "^7.0.1",
|
||||
"husky": "^1.1.2",
|
||||
|
@ -0,0 +1,29 @@
|
||||
/* eslint-disable no-magic-numbers */
|
||||
import React from 'react';
|
||||
import { SuperChart } from '@superset-ui/chart';
|
||||
import data from './data';
|
||||
|
||||
export default [
|
||||
{
|
||||
renderStory: () => (
|
||||
<SuperChart
|
||||
chartType="box-plot2"
|
||||
chartProps={{
|
||||
datasource: { verboseMap: {} },
|
||||
formData: {
|
||||
colorScheme: 'd3Category10',
|
||||
groupby: ['region'],
|
||||
metrics: ['sum__SP_POP_TOTL'],
|
||||
vizType: 'box_plot',
|
||||
whiskerOptions: 'Min/max (no outliers)',
|
||||
},
|
||||
height: 400,
|
||||
payload: { data },
|
||||
width: 400,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
storyName: 'Basic',
|
||||
storyPath: 'plugin-chart-box-plot|BoxPlotChartPlugin',
|
||||
},
|
||||
];
|
@ -0,0 +1,80 @@
|
||||
/* eslint-disable sort-keys, no-magic-numbers */
|
||||
export default [
|
||||
{
|
||||
label: 'East Asia & Pacific',
|
||||
values: {
|
||||
Q1: 1384725172.5,
|
||||
Q2: 1717904169.0,
|
||||
Q3: 2032724922.5,
|
||||
whisker_high: 2240687901.0,
|
||||
whisker_low: 1031863394.0,
|
||||
outliers: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Europe & Central Asia',
|
||||
values: {
|
||||
Q1: 751386460.5,
|
||||
Q2: 820716895.0,
|
||||
Q3: 862814192.5,
|
||||
whisker_high: 903095786.0,
|
||||
whisker_low: 660881033.0,
|
||||
outliers: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Latin America & Caribbean',
|
||||
values: {
|
||||
Q1: 313690832.5,
|
||||
Q2: 421490233.0,
|
||||
Q3: 529668114.5,
|
||||
whisker_high: 626270167.0,
|
||||
whisker_low: 220564224.0,
|
||||
outliers: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Middle East & North Africa',
|
||||
values: {
|
||||
Q1: 152382756.5,
|
||||
Q2: 232066828.0,
|
||||
Q3: 318191071.5,
|
||||
whisker_high: 417451428.0,
|
||||
whisker_low: 105512645.0,
|
||||
outliers: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'North America',
|
||||
values: {
|
||||
Q1: 235506847.5,
|
||||
Q2: 268896849.0,
|
||||
Q3: 314553651.5,
|
||||
whisker_high: 354462656.0,
|
||||
whisker_low: 198624409.0,
|
||||
outliers: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'South Asia',
|
||||
values: {
|
||||
Q1: 772373036.5,
|
||||
Q2: 1059570231.0,
|
||||
Q3: 1398841234.0,
|
||||
whisker_high: 1720976995.0,
|
||||
whisker_low: 572036107.0,
|
||||
outliers: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Sub-Saharan Africa',
|
||||
values: {
|
||||
Q1: 320037758.0,
|
||||
Q2: 467337821.0,
|
||||
Q3: 676768689.0,
|
||||
whisker_high: 974315323.0,
|
||||
whisker_low: 228268752.0,
|
||||
outliers: [],
|
||||
},
|
||||
},
|
||||
];
|
@ -0,0 +1,8 @@
|
||||
import BoxPlotChartPlugin from '../../../../superset-ui-legacy-plugin-chart-box-plot/src';
|
||||
import Stories from './Stories';
|
||||
|
||||
new BoxPlotChartPlugin().configure({ key: 'box-plot2' }).register();
|
||||
|
||||
export default {
|
||||
examples: [...Stories],
|
||||
};
|
@ -0,0 +1,34 @@
|
||||
## @superset-ui/legacy-plugin-chart-box-plot
|
||||
|
||||
[![Version](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-box-plot.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-box-plot.svg?style=flat-square)
|
||||
[![David (path)](https://img.shields.io/david/apache-superset/superset-ui.svg?path=packages%2Fsuperset-ui-legacy-plugin-chart-box-plot&style=flat-square)](https://david-dm.org/apache-superset/superset-ui?path=packages/superset-ui-legacy-plugin-chart-box-plot)
|
||||
|
||||
This plugin provides Box Plot for Superset.
|
||||
|
||||
### Usage
|
||||
|
||||
Configure `key`, which can be any `string`, and register the plugin. This `key` will be used to lookup this chart throughout the app.
|
||||
|
||||
```js
|
||||
import BoxPlotChartPlugin from '@superset-ui/legacy-plugin-chart-box-plot';
|
||||
|
||||
new BoxPlotChartPlugin()
|
||||
.configure({ key: 'box-plot' })
|
||||
.register();
|
||||
```
|
||||
|
||||
Then use it via `SuperChart`. See [storybook](https://apache-superset.github.io/superset-ui-legacy/?selectedKind=plugin-chart-box-plot) for more details.
|
||||
|
||||
```js
|
||||
<SuperChart
|
||||
chartType="box-plot"
|
||||
chartProps={{
|
||||
width: 600,
|
||||
height: 600,
|
||||
formData: {...},
|
||||
payload: {
|
||||
data: {...},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
```
|
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@superset-ui/legacy-plugin-chart-box-plot",
|
||||
"version": "0.0.0",
|
||||
"description": "Superset Legacy Chart - Box Plot",
|
||||
"sideEffects": false,
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
"files": [
|
||||
"esm",
|
||||
"lib"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/apache-superset/superset-ui-legacy.git"
|
||||
},
|
||||
"keywords": [
|
||||
"superset"
|
||||
],
|
||||
"author": "Superset",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/apache-superset/superset-ui-legacy/issues"
|
||||
},
|
||||
"homepage": "https://github.com/apache-superset/superset-ui-legacy#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@data-ui/xy-chart": "^0.0.75",
|
||||
"@data-ui/theme": "^0.0.75",
|
||||
"@vx/axis": "^0.0.184",
|
||||
"@vx/group": "^0.0.183",
|
||||
"@vx/legend": "^0.0.183",
|
||||
"@vx/responsive": "^0.0.184",
|
||||
"@vx/shape": "^0.0.184",
|
||||
"@vx/scale": "^0.0.182",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@superset-ui/core": "^0.9.x",
|
||||
"@superset-ui/chart": "^0.9.x",
|
||||
"@superset-ui/color": "^0.9.x",
|
||||
"@superset-ui/dimension": "^0.9.x",
|
||||
"@superset-ui/number-format": "^0.9.x",
|
||||
"@superset-ui/translation": "^0.9.x",
|
||||
"react": "^15 || ^16"
|
||||
}
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
/**
|
||||
* 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 { BoxPlotSeries, XYChart } from '@data-ui/xy-chart';
|
||||
import { chartTheme } from '@data-ui/theme';
|
||||
import { CategoricalColorNamespace } from '@superset-ui/color';
|
||||
import createTooltip from './utils/createBoxPlotTooltip';
|
||||
import renderLegend from './utils/renderLegend';
|
||||
import XYChartLayout from './utils/XYChartLayout';
|
||||
import WithLegend from './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,
|
||||
isHorizontal: PropTypes.bool,
|
||||
theme: PropTypes.object,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
className: '',
|
||||
margin: { top: 10, right: 10, left: 10, bottom: 10 },
|
||||
isHorizontal: false,
|
||||
theme: chartTheme,
|
||||
};
|
||||
|
||||
class BoxPlot extends React.PureComponent {
|
||||
renderChart({ width, height }) {
|
||||
const { data, encoding, margin, theme, isHorizontal } = this.props;
|
||||
|
||||
const config = {
|
||||
width,
|
||||
height,
|
||||
minContentWidth: 0,
|
||||
minContentHeight: 0,
|
||||
margin,
|
||||
theme,
|
||||
encoding: isHorizontal
|
||||
? {
|
||||
...encoding,
|
||||
x: { ...encoding.y, axis: { ...encoding.y.axis, orientation: 'bottom' } },
|
||||
y: { ...encoding.x, axis: { ...encoding.x.axis, orientation: 'left' } },
|
||||
}
|
||||
: encoding,
|
||||
};
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(
|
||||
encoding.color.scale.scheme,
|
||||
encoding.color.scale.namespace,
|
||||
);
|
||||
|
||||
const colorField = encoding.color.field;
|
||||
|
||||
const children = [
|
||||
<BoxPlotSeries
|
||||
key={datum => datum[encoding.x.field]}
|
||||
animated
|
||||
data={
|
||||
isHorizontal
|
||||
? data.map(row => ({ ...row, y: row[encoding.x.field] }))
|
||||
: data.map(row => ({ ...row, x: row[encoding.x.field] }))
|
||||
}
|
||||
fill={datum => colorFn(datum[colorField])}
|
||||
fillOpacity={0.4}
|
||||
stroke={datum => colorFn(datum[colorField])}
|
||||
strokeWidth={1}
|
||||
widthRatio={0.6}
|
||||
horizontal={isHorizontal}
|
||||
/>,
|
||||
];
|
||||
|
||||
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
|
||||
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-legacy-chart-box-plot ${className}`}
|
||||
width={width}
|
||||
height={height}
|
||||
position="top"
|
||||
renderLegend={() => renderLegend(data, encoding.color)}
|
||||
renderChart={parent => this.renderChart(parent)}
|
||||
hideLegend={!encoding.color.legend}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BoxPlot.propTypes = propTypes;
|
||||
BoxPlot.defaultProps = defaultProps;
|
||||
|
||||
export default BoxPlot;
|
@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isDefined } from '@superset-ui/core';
|
||||
|
||||
const propTypes = {
|
||||
contentHeight: PropTypes.number,
|
||||
contentWidth: PropTypes.number,
|
||||
height: PropTypes.number.isRequired,
|
||||
renderContent: PropTypes.func,
|
||||
width: PropTypes.number.isRequired,
|
||||
};
|
||||
const defaultProps = {
|
||||
contentHeight: null,
|
||||
contentWidth: null,
|
||||
renderContent() {},
|
||||
};
|
||||
|
||||
class ChartFrame extends React.PureComponent {
|
||||
render() {
|
||||
const { contentWidth, contentHeight, width, height, renderContent } = this.props;
|
||||
|
||||
const overflowX = isDefined(contentWidth) && contentWidth > width;
|
||||
const overflowY = isDefined(contentHeight) && contentHeight > height;
|
||||
|
||||
if (overflowX || overflowY) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height,
|
||||
overflowX: overflowX ? 'scroll' : 'hidden',
|
||||
overflowY: overflowY ? 'scroll' : 'hidden',
|
||||
width,
|
||||
}}
|
||||
>
|
||||
{renderContent({
|
||||
height: contentHeight,
|
||||
width: contentWidth,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return renderContent({ height, width });
|
||||
}
|
||||
}
|
||||
|
||||
ChartFrame.propTypes = propTypes;
|
||||
ChartFrame.defaultProps = defaultProps;
|
||||
|
||||
export default ChartFrame;
|
@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 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 from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ParentSize } from '@vx/responsive';
|
||||
|
||||
const propTypes = {
|
||||
className: PropTypes.string,
|
||||
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
legendJustifyContent: PropTypes.oneOf(['center', 'flex-start', 'flex-end']),
|
||||
position: PropTypes.oneOf(['top', 'left', 'bottom', 'right']),
|
||||
renderChart: PropTypes.func.isRequired,
|
||||
renderLegend: PropTypes.func.isRequired,
|
||||
hideLegend: PropTypes.bool,
|
||||
};
|
||||
const defaultProps = {
|
||||
className: '',
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
legendJustifyContent: undefined,
|
||||
position: 'top',
|
||||
hideLegend: false,
|
||||
};
|
||||
|
||||
const LEGEND_STYLE_BASE = {
|
||||
display: 'flex',
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
order: -1,
|
||||
paddingTop: '5px',
|
||||
fontSize: '0.9em',
|
||||
};
|
||||
|
||||
const CHART_STYLE_BASE = {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
flexBasis: 'auto',
|
||||
position: 'relative',
|
||||
};
|
||||
|
||||
class WithLegend extends React.Component {
|
||||
getContainerDirection() {
|
||||
const { position } = this.props;
|
||||
switch (position) {
|
||||
case 'left':
|
||||
return 'row';
|
||||
case 'right':
|
||||
return 'row-reverse';
|
||||
case 'bottom':
|
||||
return 'column-reverse';
|
||||
default:
|
||||
case 'top':
|
||||
return 'column';
|
||||
}
|
||||
}
|
||||
|
||||
getLegendJustifyContent() {
|
||||
const { legendJustifyContent, position } = this.props;
|
||||
if (legendJustifyContent) {
|
||||
return legendJustifyContent;
|
||||
}
|
||||
switch (position) {
|
||||
case 'left':
|
||||
return 'flex-start';
|
||||
case 'right':
|
||||
return 'flex-start';
|
||||
case 'bottom':
|
||||
return 'flex-end';
|
||||
default:
|
||||
case 'top':
|
||||
return 'flex-end';
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
width,
|
||||
height,
|
||||
position,
|
||||
renderChart,
|
||||
renderLegend,
|
||||
hideLegend,
|
||||
} = this.props;
|
||||
|
||||
if (hideLegend) {
|
||||
return <div className={className}>{renderChart({ width, height })}</div>;
|
||||
}
|
||||
|
||||
const isHorizontal = position === 'left' || position === 'right';
|
||||
|
||||
const style = {
|
||||
display: 'flex',
|
||||
flexDirection: this.getContainerDirection(),
|
||||
};
|
||||
if (width) {
|
||||
style.width = width;
|
||||
}
|
||||
if (height) {
|
||||
style.height = height;
|
||||
}
|
||||
|
||||
const chartStyle = { ...CHART_STYLE_BASE };
|
||||
if (isHorizontal) {
|
||||
chartStyle.width = 0;
|
||||
} else {
|
||||
chartStyle.height = 0;
|
||||
}
|
||||
|
||||
const legendDirection = isHorizontal ? 'column' : 'row';
|
||||
const legendStyle = {
|
||||
...LEGEND_STYLE_BASE,
|
||||
flexDirection: legendDirection,
|
||||
justifyContent: this.getLegendJustifyContent(),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`with-legend ${className}`} style={style}>
|
||||
<div className="legend-container" style={legendStyle}>
|
||||
{renderLegend({
|
||||
// Pass flexDirection for @vx/legend to arrange legend items
|
||||
direction: legendDirection,
|
||||
})}
|
||||
</div>
|
||||
<div className="main-container" style={chartStyle}>
|
||||
<ParentSize>
|
||||
{parent =>
|
||||
parent.width > 0 && parent.height > 0
|
||||
? // Only render when necessary
|
||||
renderChart(parent)
|
||||
: null
|
||||
}
|
||||
</ParentSize>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
WithLegend.propTypes = propTypes;
|
||||
WithLegend.defaultProps = defaultProps;
|
||||
|
||||
export default WithLegend;
|
@ -0,0 +1,104 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { AxisBottom, AxisTop } from '@vx/axis';
|
||||
import { axisStylesShape, tickStylesShape } from '@data-ui/xy-chart/esm/utils/propShapes';
|
||||
|
||||
const propTypes = {
|
||||
axisStyles: axisStylesShape,
|
||||
hideZero: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
labelOffset: PropTypes.number,
|
||||
labelProps: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])),
|
||||
numTicks: PropTypes.number,
|
||||
orientation: PropTypes.oneOf(['bottom', 'top']),
|
||||
rangePadding: PropTypes.number,
|
||||
tickStyles: tickStylesShape,
|
||||
tickComponent: PropTypes.func,
|
||||
tickLabelProps: PropTypes.func,
|
||||
tickFormat: PropTypes.func,
|
||||
tickValues: PropTypes.arrayOf(
|
||||
// number or date/moment object
|
||||
PropTypes.oneOfType([PropTypes.number, PropTypes.object, PropTypes.string]),
|
||||
),
|
||||
|
||||
// probably injected by parent
|
||||
innerHeight: PropTypes.number,
|
||||
scale: PropTypes.func,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
axisStyles: {},
|
||||
hideZero: false,
|
||||
innerHeight: null,
|
||||
label: null,
|
||||
labelOffset: 14,
|
||||
labelProps: null,
|
||||
numTicks: null,
|
||||
orientation: 'bottom',
|
||||
rangePadding: null,
|
||||
scale: null,
|
||||
tickComponent: null,
|
||||
tickFormat: null,
|
||||
tickLabelProps: null,
|
||||
tickStyles: {},
|
||||
tickValues: undefined,
|
||||
};
|
||||
|
||||
export default class XAxis extends React.PureComponent {
|
||||
render() {
|
||||
const {
|
||||
axisStyles,
|
||||
innerHeight,
|
||||
hideZero,
|
||||
label,
|
||||
labelOffset,
|
||||
labelProps,
|
||||
numTicks,
|
||||
orientation,
|
||||
rangePadding,
|
||||
scale,
|
||||
tickComponent,
|
||||
tickFormat,
|
||||
tickLabelProps: passedTickLabelProps,
|
||||
tickStyles,
|
||||
tickValues,
|
||||
} = this.props;
|
||||
if (!scale || !innerHeight) return null;
|
||||
const Axis = orientation === 'bottom' ? AxisBottom : AxisTop;
|
||||
|
||||
let tickLabelProps = passedTickLabelProps;
|
||||
if (!tickLabelProps) {
|
||||
tickLabelProps =
|
||||
tickStyles.label && tickStyles.label[orientation]
|
||||
? () => tickStyles.label[orientation]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<Axis
|
||||
top={orientation === 'bottom' ? innerHeight : 0}
|
||||
left={0}
|
||||
rangePadding={rangePadding}
|
||||
hideTicks={numTicks === 0}
|
||||
hideZero={hideZero}
|
||||
label={label}
|
||||
labelOffset={labelOffset}
|
||||
labelProps={labelProps || (axisStyles.label || {})[orientation]}
|
||||
numTicks={numTicks}
|
||||
scale={scale}
|
||||
stroke={axisStyles.stroke}
|
||||
strokeWidth={axisStyles.strokeWidth}
|
||||
tickComponent={tickComponent}
|
||||
tickFormat={tickFormat}
|
||||
tickLabelProps={tickLabelProps}
|
||||
tickLength={tickStyles.tickLength}
|
||||
tickStroke={tickStyles.stroke}
|
||||
tickValues={tickValues}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
XAxis.propTypes = propTypes;
|
||||
XAxis.defaultProps = defaultProps;
|
||||
XAxis.displayName = 'XAxis';
|
Binary file not shown.
After Width: | Height: | Size: 9.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
@ -16,7 +16,24 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
.with-legend .legend-container {
|
||||
padding-top: 5px;
|
||||
font-size: 0.9em;
|
||||
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 HistogramChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./BoxPlot'),
|
||||
metadata,
|
||||
transformProps,
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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 { getNumberFormatter } from '@superset-ui/number-format';
|
||||
|
||||
export default function transformProps(chartProps) {
|
||||
const { width, height, datasource = {}, formData, payload } = chartProps;
|
||||
const { verboseMap = {} } = datasource;
|
||||
const { colorScheme, groupby, metrics } = formData;
|
||||
|
||||
const data = payload.data.map(({ label, values }) => ({
|
||||
label,
|
||||
min: values.whisker_low,
|
||||
max: values.whisker_high,
|
||||
firstQuartile: values.Q1,
|
||||
median: values.Q2,
|
||||
thirdQuartile: values.Q3,
|
||||
outliers: values.outliers,
|
||||
}));
|
||||
|
||||
const xAxisLabel = groupby.join('/');
|
||||
const yAxisLabel = metrics.length > 0 ? verboseMap[metrics[0]] || metrics[0] : '';
|
||||
|
||||
const boxPlotValues = data.reduce((r, e) => r.push(e.min, e.max, ...e.outliers) && r, []);
|
||||
const minBoxPlotValue = Math.min(...boxPlotValues);
|
||||
const maxBoxPlotValue = Math.max(...boxPlotValues);
|
||||
const valueDomain = [
|
||||
minBoxPlotValue - 0.1 * Math.abs(minBoxPlotValue),
|
||||
maxBoxPlotValue + 0.1 * Math.abs(maxBoxPlotValue),
|
||||
];
|
||||
|
||||
const formatValue = getNumberFormatter();
|
||||
|
||||
return {
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
// margin,
|
||||
// theme,
|
||||
encoding: {
|
||||
x: {
|
||||
field: 'label',
|
||||
scale: {
|
||||
type: 'band',
|
||||
paddingInner: 0.15,
|
||||
paddingOuter: 0.3,
|
||||
},
|
||||
axis: {
|
||||
label: xAxisLabel,
|
||||
orientation: 'bottom',
|
||||
},
|
||||
},
|
||||
y: {
|
||||
scale: {
|
||||
type: 'linear',
|
||||
domain: valueDomain,
|
||||
},
|
||||
axis: {
|
||||
label: yAxisLabel,
|
||||
numTicks: 5,
|
||||
orientation: 'left',
|
||||
tickFormat: formatValue,
|
||||
},
|
||||
},
|
||||
color: {
|
||||
field: 'label',
|
||||
scale: {
|
||||
scheme: colorScheme,
|
||||
},
|
||||
legend: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
/* eslint-disable sort-keys, no-magic-numbers */
|
||||
|
||||
import React from 'react';
|
||||
import collectScalesFromProps from '@data-ui/xy-chart/esm/utils/collectScalesFromProps';
|
||||
import { 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 XAxis from '../XAxis';
|
||||
import ChartFrame from '../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,16 @@
|
||||
export default function adjustMargin(baseMargin = {}, minMargin = {}) {
|
||||
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),
|
||||
};
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
/* eslint-disable sort-keys, no-magic-numbers */
|
||||
|
||||
import React from 'react';
|
||||
import collectScalesFromProps from '@data-ui/xy-chart/esm/utils/collectScalesFromProps';
|
||||
import { 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 XAxis from '../XAxis';
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/* 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({
|
||||
text,
|
||||
style: tickTextStyle,
|
||||
}),
|
||||
);
|
||||
|
||||
const maxWidth = Math.max(...labelDimensions.map(d => d.width));
|
||||
const widthPerTick = axisWidth / (tickLabels.length + 1);
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/* 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({
|
||||
text,
|
||||
style: tickTextStyle,
|
||||
}),
|
||||
);
|
||||
|
||||
const maxWidth = Math.ceil(Math.max(...labelDimensions.map(d => d.width)));
|
||||
let labelOffset = Math.ceil(maxWidth + gapBetweenTickLabelsAndAxisLabel + axisLabelHeight);
|
||||
|
||||
let margin = tickLength + gapBetweenTickAndTickLabel + labelOffset + gapBetweenAxisLabelAndBorder;
|
||||
|
||||
return {
|
||||
labelOffset: labelOffset,
|
||||
minMargin: {
|
||||
[orientation]: margin,
|
||||
},
|
||||
orientation,
|
||||
};
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function createBoxPlotTooltip(formatValue) {
|
||||
const propTypes = {
|
||||
color: PropTypes.string,
|
||||
datum: PropTypes.shape({
|
||||
firstQuartile: PropTypes.number,
|
||||
max: PropTypes.number,
|
||||
median: PropTypes.number,
|
||||
min: PropTypes.number,
|
||||
outliers: PropTypes.arrayOf(PropTypes.number),
|
||||
thirdQuartile: PropTypes.number,
|
||||
}).isRequired,
|
||||
};
|
||||
const defaultProps = {
|
||||
color: '#222',
|
||||
};
|
||||
|
||||
function BoxPlotTooltip({ datum, color }) {
|
||||
const { label, min, max, median, firstQuartile, thirdQuartile, outliers } = datum;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<strong style={{ color }}>{label}</strong>
|
||||
</div>
|
||||
{min && (
|
||||
<div>
|
||||
<strong style={{ color }}>Min </strong>
|
||||
{formatValue(min)}
|
||||
</div>
|
||||
)}
|
||||
{max && (
|
||||
<div>
|
||||
<strong style={{ color }}>Max </strong>
|
||||
{formatValue(max)}
|
||||
</div>
|
||||
)}
|
||||
{median && (
|
||||
<div>
|
||||
<strong style={{ color }}>Median </strong>
|
||||
{formatValue(median)}
|
||||
</div>
|
||||
)}
|
||||
{firstQuartile && (
|
||||
<div>
|
||||
<strong style={{ color }}>First quartile </strong>
|
||||
{formatValue(firstQuartile)}
|
||||
</div>
|
||||
)}
|
||||
{thirdQuartile && (
|
||||
<div>
|
||||
<strong style={{ color }}>Third quartile </strong>
|
||||
{formatValue(thirdQuartile)}
|
||||
</div>
|
||||
)}
|
||||
{outliers && outliers.length > 0 && (
|
||||
<div>
|
||||
<strong style={{ color }}># Outliers </strong>
|
||||
{outliers.length}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
BoxPlotTooltip.propTypes = propTypes;
|
||||
BoxPlotTooltip.defaultProps = defaultProps;
|
||||
|
||||
return BoxPlotTooltip;
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/* 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,13 @@
|
||||
/* 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);
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
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 { field, scale } = colorEncoding;
|
||||
const { scheme, namespace } = scale;
|
||||
const colorFn = CategoricalColorNamespace.getScale(scheme, namespace);
|
||||
const keySet = new Set();
|
||||
data.forEach(d => {
|
||||
keySet.add(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, i) => {
|
||||
const size = 8;
|
||||
|
||||
return (
|
||||
<LegendItem
|
||||
key={`legend-quantile-${i}`}
|
||||
margin="0 5px"
|
||||
onClick={event => {
|
||||
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>
|
||||
);
|
||||
}
|
@ -98,6 +98,7 @@ class CustomHistogram extends React.PureComponent {
|
||||
normalized={normalized}
|
||||
binCount={binCount}
|
||||
binType="numeric"
|
||||
margin={{ top: 20, right: 20 }}
|
||||
renderTooltip={({ datum, color }) => (
|
||||
<div>
|
||||
<strong style={{ color }}>
|
||||
|
@ -20,7 +20,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ParentSize } from '@vx/responsive';
|
||||
import './WithLegend.css';
|
||||
|
||||
const propTypes = {
|
||||
className: PropTypes.string,
|
||||
@ -44,6 +43,8 @@ const LEGEND_STYLE_BASE = {
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
order: -1,
|
||||
paddingTop: '5px',
|
||||
fontSize: '0.9em',
|
||||
};
|
||||
|
||||
const CHART_STYLE_BASE = {
|
||||
|
Loading…
Reference in New Issue
Block a user