Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[xy-chart] add support for confidence bands #71

Merged
merged 5 commits into from
Nov 3, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/data-ui-theme/src/svgLabel.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ export const baseLabel = {
pointerEvents: 'none',
};

export const labelTiny = {
...font.tiny,
...font.bold,
...font.middle,
pointerEvents: 'none',
};

export const baseTickLabel = {
...font.small,
...font.light,
Expand Down
15 changes: 5 additions & 10 deletions packages/demo/examples/01-xy-chart/ScatterWithHistograms.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,6 @@ function renderTooltip({ datum }) { // eslint-disable-line react/prop-types
);
}

const RotatedLabel = ({ x, y }) => ( // eslint-disable-line react/prop-types
<text
{...theme.yAxisStyles.label.right}
transform={`translate(${-y},${x})rotate(270)`}
>
y counts
</text>
);

const propTypes = {
parentWidth: PropTypes.number.isRequired,
};
Expand Down Expand Up @@ -146,7 +137,11 @@ class ScatterWithHistogram extends React.PureComponent {
))}
<HistYAxis
orientation="right"
label={<RotatedLabel />}
label="y counts"
labelProps={{
...theme.yAxisStyles.label.right,
transform: `translate(${height / 1.75}, ${height})rotate(270)`,
}}
/>
</Histogram>
</div>
Expand Down
56 changes: 56 additions & 0 deletions packages/demo/examples/01-xy-chart/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,62 @@ export const pointData = genRandomNormalPoints(n).map(([x, y], i) => ({
label: (i % n) === 0 ? `(${parseInt(x, 10)},${parseInt(y, 10)})` : null,
}));

// band data
const stdDev = 0.1;
export const temperatureBands = groupKeys.map((city, cityIndex) => (
cityTemperature.slice(0, 25).map((d) => {
const y = Number(d[city]) - (20 * cityIndex);
return {
key: city,
x: d.date,
y,
y0: y + (stdDev * y),
y1: y - (stdDev * y),
};
})
));

export const priceBandData = {
band: [
{ x: new Date('2017-06-01'), y0: 215, y1: 260 },
{ x: new Date('2017-06-02'), y0: 203, y1: 290 },
{ x: new Date('2017-06-03'), y0: 196, y1: 279 },
{ x: new Date('2017-06-04'), y0: 190, y1: 261 },
{ x: new Date('2017-06-05'), y0: 140, y1: 250 },
{ x: new Date('2017-06-06'), y0: 120, y1: 231 },
{ x: new Date('2017-06-07'), y0: 131, y1: 211 },
{ x: new Date('2017-06-08'), y0: 123, y1: 196 },
{ x: new Date('2017-06-09'), y0: 105, y1: 171 },
{ x: new Date('2017-06-10'), y0: 100, y1: 175 },
{ x: new Date('2017-06-11'), y0: 80, y1: 150 },
{ x: new Date('2017-06-12'), y0: 83, y1: 164 },
{ x: new Date('2017-06-13'), y0: 86, y1: 155 },
{ x: new Date('2017-06-14'), y0: 80, y1: 132 },
{ x: new Date('2017-06-15'), y0: 73, y1: 125 },
{ x: new Date('2017-06-16'), y0: 71, y1: 132 },
{ x: new Date('2017-06-17'), y0: 78, y1: 123 },
{ x: new Date('2017-06-18'), y0: 82, y1: 156 },
{ x: new Date('2017-06-19'), y0: 76, y1: 150 },
{ x: new Date('2017-06-20'), y0: 87, y1: 173 },
{ x: new Date('2017-06-21'), y0: 95, y1: 168 },
{ x: new Date('2017-06-22'), y0: 105, y1: 182 },
{ x: new Date('2017-06-23'), y0: 100, y1: 202 },
{ x: new Date('2017-06-24'), y0: 116, y1: 211 },
{ x: new Date('2017-06-25'), y0: 126, y1: 230 },
{ x: new Date('2017-06-26'), y0: 137, y1: 246 },
{ x: new Date('2017-06-27'), y0: 142, y1: 262 },
{ x: new Date('2017-06-28'), y0: 170, y1: 273 },
{ x: new Date('2017-06-29'), y0: 190, y1: 285 },
{ x: new Date('2017-06-30'), y0: 201, y1: 301 },
],
};

priceBandData.points = priceBandData.band.map(({ x, y0, y1 }) => ({
x,
// Introduce noise within the y0-y1 range
y: ((y1 + y0) / 2) + ((Math.random() > 0.5 ? -1 : 1) * Math.random() * ((y1 - y0) / 4)),
}));

// interval data
const intervals = [[5, 8], [15, 19]];

Expand Down
104 changes: 101 additions & 3 deletions packages/demo/examples/01-xy-chart/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import {
pointData,
intervalLineData,
intervalData,
temperatureBands,
priceBandData,
} from './data';

import WithToggle from '../shared/WithToggle';
Expand Down Expand Up @@ -123,7 +125,7 @@ export default {
},
},
{
description: 'AreaSeries',
description: 'AreaSeries -- closed',
components: [AreaSeries],
example: () => (
<ResponsiveXYChart
Expand Down Expand Up @@ -159,13 +161,109 @@ export default {
<CrossHair
showHorizontalLine={false}
fullHeight
stroke={colors.categories[2]}
circleFill={colors.categories[2]}
stroke={colors.darkGray}
circleFill="white"
circleStroke={colors.darkGray}
/>
<XAxis label="Time" numTicks={5} />
</ResponsiveXYChart>
),
},
{
description: 'AreaSeries -- band',
components: [XYChart, AreaSeries, LineSeries],
example: () => (
<ResponsiveXYChart
ariaLabel="Required label"
xScale={{ type: 'time' }}
yScale={{ type: 'linear' }}
>
{temperatureBands.map((data, i) => ([
<PatternLines
id={`band-${i}`}
height={5}
width={5}
stroke={colors.categories[i + 1]}
strokeWidth={1}
orientation={['diagonal']}
/>,
<AreaSeries
key={`band-${data[0].key}`}
label="Temperature range"
data={data}
strokeWidth={0.5}
stroke={colors.categories[i + 1]}
fill={`url(#band-${i})`}
/>,
<LineSeries
key={`line-${data[0].key}`}
data={data}
stroke={colors.categories[i + 1]}
label="Temperature avg"
/>,
]))}
<YAxis label="Temperature (°F)" numTicks={4} />
<CrossHair
showHorizontalLine={false}
fullHeight
stroke={colors.gray}
circleStroke={colors.gray}
/>
</ResponsiveXYChart>
),
},
{
description: 'AreaSeries -- confidence intervals',
components: [XYChart, AreaSeries, LineSeries, PointSeries],
example: (reference = 150) => (
<ResponsiveXYChart
ariaLabel="Required label"
xScale={{ type: 'time' }}
yScale={{ type: 'linear' }}
useVoronoi
>
<XAxis numTicks={5} />
<YAxis label="Price" tickFormat={val => `$${val}`} />
<LinearGradient
id="confidence-interval-fill"
from={colors.categories[3]}
to={colors.categories[4]}
/>
<HorizontalReferenceLine
reference={reference}
label={`Min $${reference}`}
strokeDasharray="3 3"
strokeLinecap="butt"
/>
<AreaSeries
label="band"
data={priceBandData.band}
fill="url(#confidence-interval-fill)"
strokeWidth={0}
/>
<LineSeries
label="line"
data={priceBandData.points.map(d => (d.y >= reference ? d : { ...d, y: reference }))}
stroke={colors.categories[3]}
strokeWidth={2}
/>
<PointSeries
label="line"
data={priceBandData.points.filter(d => d.y < reference)}
fill="#fff"
fillOpacity={1}
stroke={colors.categories[3]}
/>
<CrossHair
showHorizontalLine={false}
fullHeight
stroke={colors.categories[3]}
circleStroke={colors.categories[3]}
circleFill="transparent"
/>
</ResponsiveXYChart>
),
},
{
description: 'PointSeries with Histogram',
components: [PointSeries],
Expand Down
2 changes: 1 addition & 1 deletion packages/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"react-remarkable": "^1.1.1",
"react-with-styles": "^1.3.0",
"react-with-styles-interface-aphrodite": "^1.2.0",
"recompose": "^0.23.5",
"recompose": "^0.26.0",
"style-loader": "^0.18.2"
},
"peerDependencies": {
Expand Down
18 changes: 13 additions & 5 deletions packages/xy-chart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ yScale | scaleShape.isRequired | - | scale config, see below.
showXGrid | PropTypes.bool | false | whether to show vertical gridlines
showYGrid | PropTypes.bool | false | whether to show vertical gridlines
theme | themeShape | false | theme shape, see below
useVoronoi | PropTypes.bool | false | whether to compute and use a voronoi for all datapoints / mouse interactions
useVoronoi | PropTypes.bool | false | whether to compute and use a voronoi for all datapoints (with x, y values) / mouse interactions
showVoronoi | PropTypes.bool | false | convenience prop for debugging to view the underlying voronoi if used


Expand All @@ -86,7 +86,9 @@ const scaleConfigShape = PropTypes.shape({
'band',
]).isRequired,
includeZero: PropTypes.bool,

// these would override any computation done by xyplot, allowing specific ranges or colors
// see storybook for more examples
range: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])),
rangeRound: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])),
domain: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])),
Expand Down Expand Up @@ -157,17 +159,23 @@ tickValues | PropTypes.arrayOf( PropTypes.oneOfType([ PropTypes.number, PropType
Several types of series types are exported by the package, and can be used in combination. See the storybook source for more proptables for your series of interest. Here is an overview of scale support and data shapes:


Series | supported x scale type | supported y scale types | data shape | voronoi recommended for tooltips?
Series | supported x scale type | supported y scale types | data shape | voronoi compatible for tooltips?
------------ | ------------- | ------- | ---- | ----
`<AreaSeries/>` | time, linear | linear | `{ x, y [, fill, stroke] }` | yes
`<AreaSeries/>` | time, linear | linear | `{ x, y [, y0, y1, fill, stroke] }`* | yes*
`<BarSeries/>` | time, linear, band | linear | `{ x, y [, fill, stroke] }` | no
`<LineSeries/>` | time, linear | linear | `{ x, y [, stroke] }` | yes
`<PointSeries/>` | time, linear | time, linear | `{ x, y [size, fill, stroke, label] }` | yes
`<StackedBarSeries/>` | band | linear | `{ x, y }` (colors controlled with stackFills & stackKeys) | no
`<GroupedBarSeries/>` | band | linear | `{ x, y }` (colors controlled with groupFills & groupKeys) | no
`<CirclePackSeries/>` | time, linear | y is computed | `{ x [, size] }` | not compatible
`<CirclePackSeries/>` | time, linear | y is computed | `{ x [, size] }` | no
`<IntervalSeries/>` | time, linear | linear | `{ x0, x1 [, fill, stroke] }` | no

\* The y boundaries of the `<AreaSeries/>` may be specified by either
- defined `y0` and `y1` values or
- a single `y` value, in which case its lower bound is set to 0 (a "closed" area series)

It is worth noting that voronoi overlays require a defined `y` attribute, so use of voronoi with only `y0` and `y1` values will not work.

#### CirclePackSeries

<p align="center">
Expand Down Expand Up @@ -205,7 +213,7 @@ tooltipTimeout | PropTypes.number | 200 | Timeout in ms for the tooltip to hide
<img src="https://user-images.githubusercontent.com/4496521/29235861-015f9526-7eb8-11e7-964f-62301e5c6426.gif" width="500" />
</p>

For series components that have "small" mouse areas, such as `PointSeries` and `LineSeries`, you may opt to use an invisible <a href="https://github.com/hshoff/vx/tree/master/packages/vx-voronoi" target="_blank">Voronoi overlay</a> on top of the visualization to increase the target area of interaction sites and improve user experience. To enable this simply set `useVoronoi` to `true` on the `<XYChart />` component and optionally use the convenience prop `showVoronoi` to view or debug it. Note that this will compute a voronoi layout for _all_ data points across all series.
For series components that have "small" mouse areas, such as `PointSeries` and `LineSeries`, you may opt to use an invisible <a href="https://github.com/hshoff/vx/tree/master/packages/vx-voronoi" target="_blank">Voronoi overlay</a> on top of the visualization to increase the target area of interaction sites and improve user experience. To enable this simply set `useVoronoi` to `true` on the `<XYChart />` component and optionally use the convenience prop `showVoronoi` to view or debug it. Note that this will compute a voronoi layout for _all_ data points (with defined `x` and `y` datum values!) across all series.

#### Note ‼️
Because of the polygonal shapes generated by the voronoi layout, you probably _don't_ want to use this option if you are e.g., only rendering a `BarSeries` because the bar points represent the tops of the bars and thus polygons for one bar may overlap the rect of another bar (again, you may use `showVoronoi` to debug this).
Expand Down
8 changes: 5 additions & 3 deletions packages/xy-chart/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"license": "MIT",
"dependencies": {
"@data-ui/theme": "0.0.9",
"@vx/axis": "0.0.140",
"@vx/axis": "0.0.145",
"@vx/curve": "0.0.140",
"@vx/event": "0.0.140",
"@vx/glyph": "0.0.140",
Expand All @@ -33,7 +33,7 @@
"@vx/point": "0.0.136",
"@vx/responsive": "0.0.140",
"@vx/scale": "0.0.140",
"@vx/shape": "0.0.140",
"@vx/shape": "0.0.145",
"@vx/tooltip": "0.0.140",
"@vx/voronoi": "0.0.140",
"d3-array": "^1.2.0",
Expand All @@ -49,11 +49,13 @@
"enzyme-adapter-react-16": "^1.0.0",
"jest": "^20.0.3",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-test-renderer": "^16.0.0",
"webpack": "^2.4.1"
},
"peerDependencies": {
"react": "^15.0.0-0 || ^16.0.0-0"
"react": "^15.0.0-0 || ^16.0.0-0",
"react-dom": "^15.0.0-0 || ^16.0.0-0"
},
"jest": {
"setupFiles": [
Expand Down
Loading