Skip to content

Commit

Permalink
Ability to specify the d3 defined prop in Line-, LineMark- and AreaSe…
Browse files Browse the repository at this point in the history
…ries. (uber#585)

* Added the d3 define prop to Line-, LineMark- and AreaSeries.

* Made requested changes; renamed defined to nullAccessor, added hover test, code style improvements
  • Loading branch information
tijp authored and mcnuttandrew committed Sep 5, 2017
1 parent f43f08b commit 306544f
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 54 deletions.
10 changes: 10 additions & 0 deletions docs/markdown/area-series.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ Type: `string|number`
Default: see [colors](colors.md)
A color for the fill of the area series. Will override the color property if both are provided.

#### nullAccessor (optional)
Type: `function`
Default: `null`
A function that will be invoked for each data element that will return a boolean that specifies if the data point should be drawn or not. For more information see [the D3 documentation](https://github.com/d3/d3-shape#area_defined).

```javascript
// Only draw datapoints where the y value is not equal to null
<AreaSeries nullAccessor={(d) => d.y !== null} data={data} />
```

#### opacity (optional)
Type: `number`
Default: 1
Expand Down
10 changes: 10 additions & 0 deletions docs/markdown/line-mark-series.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ Some, but not all line interpolations have the resulting curve going through the
Type: `string|number`
The inner color for all the marks in the series, this property will be over-ridden by fill specified in the data attribute. See [colors](colors.md)

#### nullAccessor (optional)
Type: `function`
Default: `null`
A function that will be invoked for each data element that will return a boolean that specifies if the data point should be drawn or not. For more information see [the D3 documentation](https://github.com/d3/d3-shape#line_defined).

```javascript
// Only draw datapoints where the y value is not equal to null
<LineMarkSeries nullAccessor={(d) => d.y !== null} data={data} />
```

#### stroke (optional)
Type: `string|number`
Default: see [colors](colors.md)
Expand Down
10 changes: 10 additions & 0 deletions docs/markdown/line-series.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ const funcCurveProp = <LineSeries data={data} curve={configuredCurve} .../>;
Type: `Array<Object>`
Array of data for the series. See above data format reference.

#### nullAccessor (optional)
Type: `function`
Default: `null`
A function that will be invoked for each data element that will return a boolean that specifies if the data point should be drawn or not. For more information see [the D3 documentation](https://github.com/d3/d3-shape#line_defined).

```javascript
// Only draw datapoints where the y value is not equal to null
<LineSeries nullAccessor={(d) => d.y !== null} data={data} />
```

#### opacity (optional)
Type: `number`
Default: 1
Expand Down
2 changes: 2 additions & 0 deletions showcase/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ import HorizontalDiscreteCustomPalette from './legends/horizontal-discrete-custo
import AnimationExample from './misc/animation-example';
import LabelSeriesExample from './misc/label-series-example';
import GradientExample from './misc/gradient-example';
import NullDataExample from './misc/null-data-example';
import SyncedCharts from './misc/synced-charts';
import TimeChart from './misc/time-chart';
import TriangleExample from './misc/triangle-example';
Expand Down Expand Up @@ -184,6 +185,7 @@ export const showCase = {
CustomAxes,
LabelSeriesExample,
GradientExample,
NullDataExample,

SensibleDefaults,
ColorInXYPlot,
Expand Down
77 changes: 77 additions & 0 deletions showcase/misc/null-data-example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) 2016 - 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import React from 'react';

import {
AreaSeries,
Crosshair,
XYPlot,
XAxis,
YAxis,
HorizontalGridLines,
VerticalGridLines,
LineMarkSeries
} from 'index';

const DATA = [
[
{x: 1, y: 10},
{x: 2, y: 10},
{x: 3, y: 13},
{x: 4, y: 7},
{x: 5, y: null}
],
[
{x: 1, y: 30},
{x: 2, y: 0},
{x: 5, y: null},
{x: 4, y: 15},
{x: 5, y: null}
]
];

export default class NullDataExample extends React.Component {
state = {
crosshairValues: []
};

onMouseLeave = () => this.setState({crosshairValues: []});
onNearestX = (value, {index}) =>
this.setState({crosshairValues: DATA.map(d => d[index].y !== null && d[index])});

render() {
return (
<XYPlot
width={300}
height={300}
onMouseLeave={this.onMouseLeave}>
<XAxis/>
<YAxis/>
<HorizontalGridLines />
<VerticalGridLines />
<AreaSeries nullAccessor={(d) => d.y !== null} onNearestX={this.onNearestX} data={DATA[0]} />
<LineMarkSeries nullAccessor={(d) => d.y !== null} data={DATA[1]} />
<Crosshair
values={this.state.crosshairValues}/>
</XYPlot>
);
}
}
4 changes: 4 additions & 0 deletions showcase/showcase-sections/misc-showcase.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const {
AnimationExample,
LabelSeriesExample,
GradientExample,
NullDataExample,
SyncedCharts,
TimeChart,
TriangleExample,
Expand Down Expand Up @@ -38,6 +39,9 @@ const MISC = [{
component: LabelSeriesExample,
sourceLink: 'https://github.com/uber/react-vis/blob/master/src/plot/series/label-series.js',
docsLink: 'http://uber.github.io/react-vis/#/documentation/xy-plot-series/label-series'
}, {
name: 'Null Data Example',
component: NullDataExample
}];

class MiscShowcase extends Component {
Expand Down
15 changes: 12 additions & 3 deletions src/plot/series/area-series.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
// THE SOFTWARE.

import React from 'react';
import PropTypes from 'prop-types';
import * as d3Shape from 'd3-shape';

import Animation from 'animation';
Expand All @@ -30,7 +31,7 @@ import AbstractSeries from './abstract-series';
const predefinedClassName = 'rv-xy-plot__series rv-xy-plot__series--line';

class AreaSeries extends AbstractSeries {
_renderArea(data, x, y0, y, curve) {
_renderArea(data, x, y0, y, curve, nullAccessor) {
let area = d3Shape.area();
if (curve !== null) {
if (typeof curve === 'string' && d3Shape[curve]) {
Expand All @@ -39,13 +40,14 @@ class AreaSeries extends AbstractSeries {
area = area.curve(curve);
}
}
area = area.defined(nullAccessor);
area = area.x(x).y0(y0).y1(y);
return area(data);
}

render() {
const {
animation, className, curve, data, marginLeft, marginTop, style
animation, className, curve, data, marginLeft, marginTop, nullAccessor, style
} = this.props;
if (!data) {
return null;
Expand All @@ -67,7 +69,7 @@ class AreaSeries extends AbstractSeries {
this._getAttributeValue('color');
const newOpacity = this._getAttributeValue('opacity');
const opacity = Number.isFinite(newOpacity) ? newOpacity : DEFAULT_OPACITY;
const d = this._renderArea(data, x, y0, y, curve);
const d = this._renderArea(data, x, y0, y, curve, nullAccessor);
return (
<path
d={d}
Expand All @@ -89,5 +91,12 @@ class AreaSeries extends AbstractSeries {
}

AreaSeries.displayName = 'AreaSeries';
AreaSeries.propTypes = {
...AbstractSeries.propTypes,
nullAccessor: PropTypes.func
};
AreaSeries.defaultProps = {
nullAccessor: () => true
};

export default AreaSeries;
44 changes: 21 additions & 23 deletions src/plot/series/line-series.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,26 +35,8 @@ const STROKE_STYLES = {
solid: null
};

const defaultProps = {
strokeStyle: 'solid',
style: {},
opacity: 1,
curve: null,
className: ''
};

const propTypes = {
...AbstractSeries.propTypes,
strokeStyle: PropTypes.oneOf(Object.keys(STROKE_STYLES)),
curve: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func
])
};

class LineSeries extends AbstractSeries {

_renderLine(data, x, y, curve) {
_renderLine(data, x, y, curve, nullAccessor) {
let line = d3Shape.line();
if (curve !== null) {
if (typeof curve === 'string' && d3Shape[curve]) {
Expand All @@ -63,6 +45,7 @@ class LineSeries extends AbstractSeries {
line = line.curve(curve);
}
}
line = line.defined(nullAccessor);
line = line.x(x).y(y);
return line(data);
}
Expand All @@ -81,7 +64,7 @@ class LineSeries extends AbstractSeries {
}

const {
strokeStyle, strokeDasharray, strokeWidth, marginLeft, marginTop, curve, style
curve, marginLeft, marginTop, nullAccessor, strokeDasharray, strokeStyle, strokeWidth, style
} = this.props;

const x = this._getAttributeFunctor('x');
Expand All @@ -90,7 +73,7 @@ class LineSeries extends AbstractSeries {
this._getAttributeValue('color');
const newOpacity = this._getAttributeValue('opacity');
const opacity = Number.isFinite(newOpacity) ? newOpacity : DEFAULT_OPACITY;
const d = this._renderLine(data, x, y, curve);
const d = this._renderLine(data, x, y, curve, nullAccessor);

return (
<path
Expand All @@ -113,7 +96,22 @@ class LineSeries extends AbstractSeries {
}

LineSeries.displayName = 'LineSeries';
LineSeries.defaultProps = defaultProps;
LineSeries.propTypes = propTypes;
LineSeries.propTypes = {
...AbstractSeries.propTypes,
strokeStyle: PropTypes.oneOf(Object.keys(STROKE_STYLES)),
curve: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func
]),
nullAccessor: PropTypes.func
};
LineSeries.defaultProps = {
strokeStyle: 'solid',
style: {},
opacity: 1,
curve: null,
className: '',
nullAccessor: () => true
};

export default LineSeries;
64 changes: 36 additions & 28 deletions src/plot/series/mark-series.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,39 @@ const predefinedClassName = 'rv-xy-plot__series rv-xy-plot__series--mark';
const DEFAULT_STROKE_WIDTH = 1;

class MarkSeries extends AbstractSeries {
_renderCircle(d, i, strokeWidth, style) {
const sizeFunctor = this._getAttributeFunctor('size');
const opacityFunctor = this._getAttributeFunctor('opacity');
const fillFunctor = this._getAttributeFunctor('fill') ||
this._getAttributeFunctor('color');
const strokeFunctor = this._getAttributeFunctor('stroke') ||
this._getAttributeFunctor('color');
const xFunctor = this._getAttributeFunctor('x');
const yFunctor = this._getAttributeFunctor('y');

const attrs = {
r: sizeFunctor ? sizeFunctor(d) : DEFAULT_SIZE,
cx: xFunctor(d),
cy: yFunctor(d),
style: {
opacity: opacityFunctor ? opacityFunctor(d) : DEFAULT_OPACITY,
stroke: strokeFunctor && strokeFunctor(d),
fill: fillFunctor && fillFunctor(d),
strokeWidth: strokeWidth || DEFAULT_STROKE_WIDTH,
...style
},
key: i,
onClick: e => this._valueClickHandler(d, e),
onContextMenu: e => this._valueRightClickHandler(d, e),
onMouseOver: e => this._valueMouseOverHandler(d, e),
onMouseOut: e => this._valueMouseOutHandler(d, e)
};
return <circle {...attrs} />;
}

render() {
const {
animation, className, data, marginLeft, marginTop, strokeWidth, style
animation, className, data, marginLeft, marginTop, nullAccessor, strokeWidth, style
} = this.props;
if (!data) {
return null;
Expand All @@ -48,38 +77,12 @@ class MarkSeries extends AbstractSeries {
);
}

const sizeFunctor = this._getAttributeFunctor('size');
const opacityFunctor = this._getAttributeFunctor('opacity');
const fillFunctor = this._getAttributeFunctor('fill') ||
this._getAttributeFunctor('color');
const strokeFunctor = this._getAttributeFunctor('stroke') ||
this._getAttributeFunctor('color');
const xFunctor = this._getAttributeFunctor('x');
const yFunctor = this._getAttributeFunctor('y');

return (
<g className={`${predefinedClassName} ${className}`}
ref="container"
transform={`translate(${marginLeft},${marginTop})`}>
{data.map((d, i) => {
const attrs = {
r: sizeFunctor ? sizeFunctor(d) : DEFAULT_SIZE,
cx: xFunctor(d),
cy: yFunctor(d),
style: {
opacity: opacityFunctor ? opacityFunctor(d) : DEFAULT_OPACITY,
stroke: strokeFunctor && strokeFunctor(d),
fill: fillFunctor && fillFunctor(d),
strokeWidth: strokeWidth || DEFAULT_STROKE_WIDTH,
...style
},
key: i,
onClick: e => this._valueClickHandler(d, e),
onContextMenu: e => this._valueRightClickHandler(d, e),
onMouseOver: e => this._valueMouseOverHandler(d, e),
onMouseOut: e => this._valueMouseOutHandler(d, e)
};
return <circle {...attrs} />;
return nullAccessor(d) && this._renderCircle(d, i, strokeWidth, style);
})}
</g>
);
Expand All @@ -89,6 +92,11 @@ class MarkSeries extends AbstractSeries {
MarkSeries.displayName = 'MarkSeries';
MarkSeries.propTypes = {
...AbstractSeries.propTypes,
nullAccessor: PropTypes.func,
strokeWidth: PropTypes.number
};
MarkSeries.defaultProps = {
nullAccessor: () => true
};

export default MarkSeries;
Loading

0 comments on commit 306544f

Please sign in to comment.