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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

[xy-chart] add renderLabel to <BarSeries /> #147

Merged
merged 9 commits into from
Nov 14, 2018
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
93 changes: 65 additions & 28 deletions packages/demo/examples/01-xy-chart/HorizontalBarChart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,46 @@
import React from 'react';
import { timeParse, timeFormat } from 'd3-time-format';

import { CrossHair, XAxis, YAxis, BarSeries, PatternCircles, Brush } from '@data-ui/xy-chart';

import { CrossHair, XAxis, YAxis, BarSeries, PatternLines, Brush, Text } from '@data-ui/xy-chart';
import { svgLabel } from '@data-ui/theme';
import { allColors } from '@data-ui/theme/lib/color';
import { xTickStyles, yTickStyles } from '@data-ui/theme/lib/chartTheme';
import ResponsiveXYChart from './ResponsiveXYChart';

import { timeSeriesData } from './data';

const { baseLabel } = svgLabel;

export const parseDate = timeParse('%Y%m%d');
export const formatDate = timeFormat('%b %d');
export const formatYear = timeFormat('%Y');
export const dateFormatter = date => formatYear(parseDate(date));
const COLOR_1 = 'grape';
const COLOR_2 = 'gray';
const BRIGHTNESS = 5;
const BRIGHTNESS_DARK = 7;
const xTickLabelProps = {
...xTickStyles.label.bottom,
stroke: allColors[COLOR_1][BRIGHTNESS_DARK],
};

const yTickLabelProps = {
...yTickStyles.label.left,
stroke: allColors[COLOR_1][BRIGHTNESS_DARK],
};

const categoryHorizontalData = timeSeriesData.map((d, i) => ({
x: d.y,
y: i + 1,
selected: false,
label: i === 3 ? 'Long long label' : (i === 5 && 'Label') || '',
}));

const categoryData = timeSeriesData.map((d, i) => ({
x: i + 1,
y: d.y,
selected: false,
label: i === 3 ? 'Long long label' : (i === 5 && 'Label') || '',
}));

class HorizontalBarChartExample extends React.PureComponent {
Expand Down Expand Up @@ -105,7 +123,7 @@ class HorizontalBarChartExample extends React.PureComponent {

render() {
const { direction, data } = this.state;
const categoryScale = { type: 'band', paddingInner: 0.4 };
const categoryScale = { type: 'band', paddingInner: 0.1 };
const valueScale = { type: 'linear' };
const horizontal = direction === 'horizontal';

Expand All @@ -119,49 +137,68 @@ class HorizontalBarChartExample extends React.PureComponent {
yScale={horizontal ? categoryScale : valueScale}
margin={{ left: 100, top: 64, bottom: 64 }}
>
<PatternCircles
id="horizontal_bar_circles"
<PatternLines
id="brush_pattern"
height={6}
width={6}
stroke={allColors[COLOR_1][BRIGHTNESS]}
strokeWidth={1}
orientation={['diagonal']}
/>
<PatternLines
id="bar_pattern_1"
height={6}
radius={2}
fill={allColors.blue[horizontal ? 2 : 8]}
strokeWidth={0}
width={6}
stroke={allColors[COLOR_1][BRIGHTNESS]}
strokeWidth={1}
orientation={['diagonal']}
/>
<BarSeries
fill={bar => {
const color = bar.selected ? allColors.red : allColors.blue;

return color[horizontal ? 8 : 2];
}}
horizontal={horizontal}
data={data}
<PatternLines
id="bar_pattern_2"
height={6}
width={6}
stroke={allColors[COLOR_2][BRIGHTNESS]}
strokeWidth={1}
orientation={['diagonal']}
/>
<BarSeries
fill="url(#horizontal_bar_circles)"
fill={bar => `url(#${bar.selected ? 'bar_pattern_1' : 'bar_pattern_2'})`}
horizontal={horizontal}
data={data}
stroke={allColors.blue[8]}
strokeWidth={1.5}
renderLabel={({ datum, labelProps, index: i }) =>
datum.label ? (
<Text
{...labelProps}
fill={allColors[datum.selected ? COLOR_2 : COLOR_1][BRIGHTNESS_DARK]}
angle={datum.selected ? 20 * (i >= 5 ? -1 : 1) : 0}
>
{datum.label}
</Text>
) : null
}
/>
<CrossHair
showHorizontalLine={!horizontal}
showVerticalLine={horizontal}
showVerticalLine
showHorizontalLine={false}
fullHeight
fullWidth
strokeDasharray=""
stroke={allColors.blue[8]}
circleFill={allColors.blue[7]}
stroke={allColors[COLOR_1][BRIGHTNESS_DARK]}
circleFill={allColors[COLOR_1][BRIGHTNESS_DARK]}
circleStroke="white"
/>
<YAxis numTicks={5} orientation="left" />
<XAxis numTicks={5} />

<Brush
brushDirection={horizontal ? 'vertical' : 'horizontal'}
ref={this.Brush}
onChange={this.handleBrushChange}
selectedBoxStyle={{
fill: 'url(#brush_pattern)',
fillOpacity: 0.2,
stroke: allColors[COLOR_1][BRIGHTNESS_DARK],
}}
/>
<YAxis numTicks={5} orientation="left" tickLabelProps={() => yTickLabelProps} />
<XAxis numTicks={5} tickLabelProps={() => xTickLabelProps} />
</ResponsiveXYChart>

<style type="text/css">
{`
.bar-demo--form > div {
Expand Down
35 changes: 33 additions & 2 deletions packages/xy-chart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,11 @@ Entries in scale objects are shallow checked so new objects don't trigger re-ren
| Name | Type | Default | Description |
| ------------------ | -------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------ |
| axisStyles | axisStylesShape | `{}` | config object for axis and axis label styles, see theme above. |
| label | PropTypes.oneOfType( [PropTypes.string, PropTypes.element] ) | `<text {...axisStyles.label[ orientation ]} />` | string or component for axis labels |
| label | PropTypes.oneOfType( [PropTypes.string, PropTypes.element] ) | `<Text {...axisStyles.label[ orientation ]} />` | string or component for axis labels |
| numTicks | PropTypes.number | null | _approximate_ number of ticks (actual number depends on the data and d3's algorithm) |
| orientation | PropTypes.oneOf(['top', 'right', 'bottom', 'left']) | bottom (XAxis), right (YAxis) | orientation of axis |
| tickStyles | tickStylesShape | `{}` | config object for styling ticks and tick labels, see theme above. |
| tickLabelComponent | PropTypes.element | `<text {...tickStyles.label[ orientation ]} />` | component to use for tick labels |
| tickLabelComponent | PropTypes.element | `<Text {...tickStyles.label[ orientation ]} />` | component to use for tick labels |
| tickFormat | PropTypes.func | null | `(tick, tickIndex) => formatted tick` |
| tickValues | PropTypes.arrayOf( PropTypes.oneOfType([ PropTypes.number, PropTypes.string ]) ) | null | custom tick values |

Expand Down Expand Up @@ -153,6 +153,37 @@ support and data shapes:
- defined `y0` and `y1` values or
- a single `y` value, in which case its lower bound is set to 0 (a "closed" area series)

#### Series labels

The `<PointSeries />` and `<BarSeries />` components support rendering labels per-datum via the
`renderLabel` and `defaultLabelProps` props.

- by default, if a datum has a label property, it will have a label rendered out of the box using
the `@vx/text` `<Text />` component (which wraps svg text, etc.). labels are always rendered on
top of the `Bar`s and `Point`s themeselves.
- The label has "smart" default aesthetics (taking from the `@data-ui` theme), text anchors, and
wrapping behavior, but you can override them by setting `defaultLabelProps` to your own object. By
default these props are passed to the underlying `<Text />` label component, and `d.label` is
rendered as the child
- to support full label customization, you may define a `renderLabel` function with the signature
`({ datum, index, labelProps }) => node`. labelProps includes all values from `defaultLabelProps`
as well as "smart" default values for `width`, `x`, `y`, `dx`, `dy`, `verticalAnchor`, and
`textAnchor` based on `Bar` and `Point` position, size, and orientation (horizontal vs vertical).
- Example usage:

```javascript
<BarSeries
{...restProps}
renderLabel={({ datum, labelProps, index: i }) =>
datum.label ? (
<Text {...labelProps} fill={datum.selected ? COLOR_2 : COLOR_1}>
{datum.label}
</Text>
) : null
}
/>
```

#### `<CirclePackSeries />`

<p align="center">
Expand Down
2 changes: 1 addition & 1 deletion packages/xy-chart/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"@vx/scale": "^0.0.165",
"@vx/shape": "^0.0.165",
"@vx/stats": "^0.0.165",
"@vx/text": "0.0.165",
"@vx/text": "0.0.179",
"@vx/threshold": "0.0.170",
"@vx/tooltip": "^0.0.165",
"@vx/voronoi": "^0.0.165",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ function HorizontalReferenceLine({
strokeWidth={strokeWidth}
vectorEffect="non-scaling-stroke"
/>
{Boolean(label) && (
{!!label && (
<Text y={scaledRef} {...defaultLabelProps} {...labelProps}>
{label}
</Text>
Expand Down
3 changes: 3 additions & 0 deletions packages/xy-chart/src/annotation/Text.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Text from '@vx/text/build/Text';

export default Text;
2 changes: 1 addition & 1 deletion packages/xy-chart/src/annotation/VerticalReferenceLine.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ function VerticalReferenceLine({
strokeWidth={strokeWidth}
vectorEffect="non-scaling-stroke"
/>
{Boolean(label) && (
{!!label && (
<Text x={scaledRef} {...defaultLabelProps} {...labelProps}>
{label}
</Text>
Expand Down
20 changes: 15 additions & 5 deletions packages/xy-chart/src/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
// Chart
export { default as XYChart, propTypes as xyChartPropTypes } from './chart/XYChart';
export { default as ParentSize } from './composer/ParentSize';
export { default as withScreenSize } from './enhancer/withScreenSize';
export { default as withParentSize } from './enhancer/withParentSize';

// Axis
export { default as XAxis } from './axis/XAxis';
export { default as YAxis } from './axis/YAxis';
export { default as XYChart, propTypes as xyChartPropTypes } from './chart/XYChart';

// Series
export { default as AreaSeries } from './series/AreaSeries';
export { default as BarSeries } from './series/BarSeries';
export { default as BoxPlotSeries } from './series/BoxPlotSeries';
Expand All @@ -17,15 +24,18 @@ export { default as AreaDifferenceSeries } from './series/AreaDifferenceSeries';
export { default as ViolinPlotSeries } from './series/ViolinPlotSeries';
export { computeStats } from '@vx/stats';

// Annotation
export { default as HorizontalReferenceLine } from './annotation/HorizontalReferenceLine';
export { default as VerticalReferenceLine } from './annotation/VerticalReferenceLine';
export { default as Text } from './annotation/Text';

// Interactions
export { default as Brush } from './selection/Brush';
export { default as CrossHair } from './chart/CrossHair';
export { default as WithTooltip, withTooltipPropTypes } from './composer/WithTooltip';

// Aesthetic
export { default as LinearGradient } from './aesthetic/LinearGradient';
export { PatternLines, PatternCircles, PatternWaves, PatternHexagons } from './aesthetic/Patterns';
export { default as ParentSize } from './composer/ParentSize';
export { default as withScreenSize } from './enhancer/withScreenSize';
export { default as withParentSize } from './enhancer/withParentSize';
export { default as withTheme } from './enhancer/withTheme';
export { default as theme } from './aesthetic/chartTheme';
export { default as Brush } from './selection/Brush';
49 changes: 46 additions & 3 deletions packages/xy-chart/src/series/BarSeries.jsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,44 @@
import { Bar } from '@vx/shape';
import { FocusBlurHandler } from '@data-ui/shared';
import { Group } from '@vx/group';
import { Text } from '@vx/text';
import PropTypes from 'prop-types';
import React from 'react';
import { color as themeColors } from '@data-ui/theme';
import { color as themeColors, svgLabel } from '@data-ui/theme';

import { barSeriesDataShape } from '../utils/propShapes';
import { callOrValue, isDefined } from '../utils/chartUtils';
import sharedSeriesProps from '../utils/sharedSeriesProps';

const { baseLabel } = svgLabel;

export const defaultLabelProps = {
...baseLabel,
pointerEvents: 'none',
stroke: '#fff',
strokeWidth: 2,
paintOrder: 'stroke',
fontSize: 12,
};

const propTypes = {
...sharedSeriesProps,
data: barSeriesDataShape.isRequired,
defaultLabelProps: PropTypes.object, // eslint-disable-line react/forbid-prop-types
fill: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
fillOpacity: PropTypes.oneOfType([PropTypes.func, PropTypes.number]),
renderLabel: PropTypes.func,
stroke: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
strokeWidth: PropTypes.oneOfType([PropTypes.func, PropTypes.number]),
horizontal: PropTypes.bool,
};

const defaultProps = {
defaultLabelProps,
fill: themeColors.default,
fillOpacity: null,
renderLabel: ({ datum, labelProps }) =>
datum.label ? <Text {...labelProps}>{datum.label}</Text> : null,
stroke: '#FFFFFF',
strokeWidth: 1,
horizontal: false,
Expand All @@ -35,6 +52,7 @@ export default class BarSeries extends React.PureComponent {
render() {
const {
data,
defaultLabelProps: labelProps,
disableMouseEvents,
fill,
fillOpacity,
Expand All @@ -45,6 +63,7 @@ export default class BarSeries extends React.PureComponent {
onClick,
onMouseMove,
onMouseLeave,
renderLabel,
horizontal,
} = this.props;
if (!xScale || !yScale) return null;
Expand All @@ -57,20 +76,43 @@ export default class BarSeries extends React.PureComponent {

const maxBarLength = Math.max(...valueScale.range());
const offset = categoryScale.offset || 0;
const Labels = []; // Labels on top

return (
<Group style={disableMouseEvents ? noEventsStyles : null}>
{data.map((d, i) => {
const barPosition = categoryScale(categoryField(d)) - offset;
const barLength = horizontal
? valueScale(valueField(d))
: maxBarLength - valueScale(valueField(d));

const color = d.fill || callOrValue(fill, d, i);
const barPosition = categoryScale(categoryField(d)) - offset;
const key = `bar-${barPosition}`;

if (renderLabel) {
const Label = renderLabel({
datum: d,
index: i,
labelProps: {
key,
...labelProps,
x: horizontal ? barLength : barPosition + barWidth / 2,
y: horizontal ? barPosition + barWidth / 2 : maxBarLength - barLength,
dx: horizontal ? '0.5em' : 0,
dy: horizontal ? 0 : '-0.74em',
textAnchor: horizontal ? 'start' : 'middle',
verticalAnchor: horizontal ? 'middle' : 'end',
width: horizontal ? null : barWidth,
},
});

if (Label) Labels.push(Label);
}

return (
isDefined(horizontal ? d.x : d.y) && (
<FocusBlurHandler
key={`bar-${barPosition}`}
key={key}
onBlur={disableMouseEvents ? null : onMouseLeave}
onFocus={
disableMouseEvents
Expand Down Expand Up @@ -117,6 +159,7 @@ export default class BarSeries extends React.PureComponent {
)
);
})}
{Labels.map(Label => Label)}
</Group>
);
}
Expand Down
Loading