victory-chart
Version:
Chart Component for Victory
447 lines (427 loc) • 16.6 kB
JavaScript
import { assign, defaults, omit } from "lodash";
import React, { PropTypes } from "react";
import {
PropTypes as CustomPropTypes, Helpers, VictoryTransition, VictoryLabel
} from "victory-core";
import Bar from "./bar";
import Data from "../../helpers/data";
import Domain from "../../helpers/domain";
import Scale from "../../helpers/scale";
const defaultStyles = {
data: {
width: 8,
padding: 6,
stroke: "transparent",
strokeWidth: 0,
fill: "#756f6a",
opacity: 1
},
labels: {
fontSize: 12,
padding: 4,
fill: "black"
}
};
const defaultData = [
{x: 1, y: 1},
{x: 2, y: 2},
{x: 3, y: 3},
{x: 4, y: 4}
];
export default class VictoryBar extends React.Component {
static role = "bar";
static defaultTransitions = {
onExit: {
duration: 500,
before: () => ({ y: 0, yOffset: 0 })
},
onEnter: {
duration: 500,
before: () => ({ y: 0, yOffset: 0 }),
after: (datum) => ({ y: datum.y, yOffset: datum.yOffset })
}
};
static propTypes = {
/**
* The animate prop specifies props for VictoryAnimation to use. The animate prop should
* also be used to specify enter and exit transition configurations with the `onExit`
* and `onEnter` namespaces respectively.
* @examples {duration: 500, onEnd: () => {}, onEnter: {duration: 500, before: () => ({y: 0})})}
*/
animate: PropTypes.object,
/**
* The categories prop specifies how categorical data for a chart should be ordered.
* This prop should be given as an array of string values, or an object with
* these arrays of values specified for x and y. If this prop is not set,
* categorical data will be plotted in the order it was given in the data array
* @examples ["dogs", "cats", "mice"]
*/
categories: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.string),
PropTypes.shape({
x: PropTypes.arrayOf(PropTypes.string),
y: PropTypes.arrayOf(PropTypes.string)
})
]),
/**
* The data prop specifies the data to be plotted. Data should be in the form of an array
* of data points. Each data point may be any format you wish
* (depending on the `x` and `y` accessor props), but by default, an object
* with x and y properties is expected.
* @examples [{x: 1, y: 2}, {x: 2, y: 3}], [[1, 2], [2, 3]],
* [[{x: "a", y: 1}, {x: "b", y: 2}], [{x: "a", y: 2}, {x: "b", y: 3}]]
*/
data: PropTypes.array,
/**
* The dataComponent prop takes an entire component which will be used to create bars for
* each datum in the chart. The new element created from the passed dataComponent will be
* provided with the following properties calculated by VictoryBar: datum, index, scale,
* style, events, horizontal (boolean), x, y, and y0. Any of these props may be overridden
* by passing in props to the supplied component, or modified or ignored within the custom
* component itself. If a dataComponent is not provided, VictoryBar will use its default
* Bar component.
*/
dataComponent: PropTypes.element,
/**
* The domain prop describes the range of values your bar chart will cover. This prop can be
* given as a array of the minimum and maximum expected values for your bar chart,
* or as an object that specifies separate arrays for x and y.
* If this prop is not provided, a domain will be calculated from data, or other
* available information.
* @examples [-1, 1], {x: [0, 100], y: [0, 1]}
*/
domain: PropTypes.oneOfType([
CustomPropTypes.domain,
PropTypes.shape({
x: CustomPropTypes.domain,
y: CustomPropTypes.domain
})
]),
/**
* The events prop attaches arbitrary event handlers to data and label elements
* Event handlers are called with their corresponding events, corresponding component props,
* and their index in the data array, and event name. The return value of event handlers
* will be stored by index and namespace on the state object of VictoryBar
* i.e. `this.state[index].data = {style: {fill: "red"}...}`, and will be
* applied by index to the appropriate child component. Event props on the
* parent namespace are just spread directly on to the top level svg of VictoryBar
* if one exists. If VictoryBar is set up to render g elements i.e. when it is
* rendered within chart, or when `standalone={false}` parent events will not be applied.
*
* @examples {data: {
* onClick: () => return {data: {style: {fill: "green"}}, labels: {style: {fill: "black"}}}
*}}
*/
events: PropTypes.shape({
data: PropTypes.object,
labels: PropTypes.object,
parent: PropTypes.object
}),
/**
* The height props specifies the height the svg viewBox of the chart container.
* This value should be given as a number of pixels
*/
height: CustomPropTypes.nonNegative,
/**
* The horizontal prop determines whether the bars will be laid vertically or
* horizontally. The bars will be vertical if this prop is false or unspecified,
* or horizontal if the prop is set to true.
*/
horizontal: PropTypes.bool,
/**
* The labels prop defines labels that will appear above each bar in your bar chart.
* This prop should be given as an array of values or as a function of data.
* If given as an array, the number of elements in the array should be equal to
* the length of the data array. Labels may also be added directly to the data object
* like data={[{x: 1, y: 1, label: "first"}]}.
* @examples: ["spring", "summer", "fall", "winter"], (datum) => datum.title
*/
labels: PropTypes.oneOfType([
PropTypes.func,
PropTypes.array
]),
/**
* The labelComponent prop takes in an entire label component which will be used
* to create labels for each bar in the bar chart. The new element created from
* the passed labelComponent will be supplied with the following properties:
* x, y, y0, index, datum, verticalAnchor, textAnchor, angle, style, text, and events.
* Any of these props may be overridden by passing in props to the supplied component,
* or modified or ignored within the custom component itself. If labelComponent is omitted,
* a new VictoryLabel will be created with props described above.
*/
labelComponent: PropTypes.element,
/**
* The padding props specifies the amount of padding in number of pixels between
* the edge of the chart and any rendered child components. This prop can be given
* as a number or as an object with padding specified for top, bottom, left
* and right.
*/
padding: PropTypes.oneOfType([
PropTypes.number,
PropTypes.shape({
top: PropTypes.number,
bottom: PropTypes.number,
left: PropTypes.number,
right: PropTypes.number
})
]),
/**
* The scale prop determines which scales your chart should use. This prop can be
* given as a string specifying a supported scale ("linear", "time", "log", "sqrt"),
* as a d3 scale function, or as an object with scales specified for x and y
* @exampes d3Scale.time(), {x: "linear", y: "log"}
*/
scale: PropTypes.oneOfType([
CustomPropTypes.scale,
PropTypes.shape({
x: CustomPropTypes.scale,
y: CustomPropTypes.scale
})
]),
/**
* The standalone prop determines whether the component will render a standalone svg
* or a <g> tag that will be included in an external svg. Set standalone to false to
* compose VictoryBar with other components within an enclosing <svg> tag.
*/
standalone: PropTypes.bool,
/**
* The style prop specifies styles for your VictoryBar. Any valid inline style properties
* will be applied. Height, width, and padding should be specified via the height,
* width, and padding props, as they are used to calculate the alignment of
* components within chart. In addition to normal style properties, angle and verticalAnchor
* may also be specified via the labels object, and they will be passed as props to
* VictoryLabel, or any custom labelComponent.
* @examples {data: {fill: "red", width: 8}, labels: {fontSize: 12}}
*/
style: PropTypes.shape({
parent: PropTypes.object,
data: PropTypes.object,
labels: PropTypes.object
}),
/**
* The width props specifies the width of the svg viewBox of the chart container
* This value should be given as a number of pixels
*/
width: CustomPropTypes.nonNegative,
/**
* The x prop specifies how to access the X value of each data point.
* If given as a function, it will be run on each data point, and returned value will be used.
* If given as an integer, it will be used as an array index for array-type data points.
* If given as a string, it will be used as a property key for object-type data points.
* If given as an array of strings, or a string containing dots or brackets,
* it will be used as a nested object property path (for details see Lodash docs for _.get).
* If `null` or `undefined`, the data value will be used as is (identity function/pass-through).
* @examples 0, 'x', 'x.value.nested.1.thing', 'x[2].also.nested', null, d => Math.sin(d)
*/
x: PropTypes.oneOfType([
PropTypes.func,
CustomPropTypes.allOfType([CustomPropTypes.integer, CustomPropTypes.nonNegative]),
PropTypes.string,
PropTypes.arrayOf(PropTypes.string)
]),
/**
* The y prop specifies how to access the Y value of each data point.
* If given as a function, it will be run on each data point, and returned value will be used.
* If given as an integer, it will be used as an array index for array-type data points.
* If given as a string, it will be used as a property key for object-type data points.
* If given as an array of strings, or a string containing dots or brackets,
* it will be used as a nested object property path (for details see Lodash docs for _.get).
* If `null` or `undefined`, the data value will be used as is (identity function/pass-through).
* @examples 0, 'y', 'y.value.nested.1.thing', 'y[2].also.nested', null, d => Math.sin(d)
*/
y: PropTypes.oneOfType([
PropTypes.func,
CustomPropTypes.allOfType([CustomPropTypes.integer, CustomPropTypes.nonNegative]),
PropTypes.string,
PropTypes.arrayOf(PropTypes.string)
])
};
static defaultProps = {
data: defaultData,
dataComponent: <Bar/>,
labelComponent: <VictoryLabel/>,
events: {},
height: 300,
padding: 50,
scale: "linear",
standalone: true,
width: 450,
x: "x",
y: "y"
};
static getDomain = Domain.getDomainWithZero.bind(Domain);
static getData = Data.getData.bind(Data);
constructor() {
super();
this.state = {};
this.getEvents = Helpers.getEvents.bind(this);
this.getEventState = Helpers.getEventState.bind(this);
}
getScale(props) {
const range = {
x: Helpers.getRange(props, "x"),
y: Helpers.getRange(props, "y")
};
const domain = {
x: Domain.getDomainWithZero(props, "x"),
y: Domain.getDomainWithZero(props, "y")
};
return {
x: Scale.getBaseScale(props, "x").domain(domain.x).range(range.x),
y: Scale.getBaseScale(props, "y").domain(domain.y).range(range.y)
};
}
getBarPosition(props, datum, scale) {
const yOffset = datum.yOffset || 0;
const xOffset = datum.xOffset || 0;
const y0 = yOffset;
const y = datum.y + yOffset;
const x = datum.x + xOffset;
const formatValue = (value, axis) => {
return datum[axis] instanceof Date ? new Date(value) : value;
};
return {
x: scale.x(formatValue(x, "x")),
y0: scale.y(formatValue(y0, "y")),
y: scale.y(formatValue(y, "y"))
};
}
getBarStyle(datum, baseStyle) {
const styleData = omit(datum, [
"xName", "yName", "x", "y", "label"
]);
return defaults({}, styleData, baseStyle);
}
getLabelStyle(style, datum) {
const labelStyle = defaults({
angle: datum.angle,
textAnchor: datum.textAnchor,
verticalAnchor: datum.verticalAnchor
}, style);
return Helpers.evaluateStyle(labelStyle, datum);
}
getLabel(props, datum, index) {
const propsLabel = Array.isArray(props.labels) ?
props.labels[index] : Helpers.evaluateProp(props.labels, datum);
return datum.label || propsLabel;
}
getLabelAnchors(datum, horizontal) {
const sign = datum.y >= 0 ? 1 : -1;
if (!horizontal) {
return {
vertical: sign >= 0 ? "end" : "start",
text: "middle"
};
} else {
return {
vertical: "middle",
text: sign >= 0 ? "start" : "end"
};
}
}
getlabelPadding(style, horizontal) {
const defaultPadding = style.padding || 0;
return {
x: horizontal ? defaultPadding : 0,
y: horizontal ? 0 : defaultPadding
};
}
renderData(props, data, style) {
const scale = this.getScale(props);
const dataEvents = this.getEvents(props.events.data, "data");
const labelEvents = this.getEvents(props.events.labels, "labels");
const { horizontal, labelComponent } = props;
return data.map((datum, index) => {
const position = this.getBarPosition(props, datum, scale);
const barStyle = this.getBarStyle(datum, style.data);
const dataProps = defaults(
{},
this.getEventState(index, "data"),
props.dataComponent.props,
position,
{
key: `bar-${index}`,
style: Helpers.evaluateStyle(barStyle, datum),
index,
datum,
scale,
horizontal
}
);
const barComponent = React.cloneElement(props.dataComponent, assign(
{}, dataProps, {events: Helpers.getPartialEvents(dataEvents, index, dataProps)}
));
const text = this.getLabel(props, dataProps.datum, index);
if (text !== null && text !== undefined) {
const labelStyle = this.getLabelStyle(style.labels, dataProps.datum);
const padding = this.getlabelPadding(labelStyle, horizontal);
const anchors = this.getLabelAnchors(dataProps.datum, horizontal);
const labelPosition = {
x: horizontal ? position.y : position.x,
y: horizontal ? position.x : position.y
};
const labelProps = defaults(
{},
this.getEventState(index, "labels"),
labelComponent.props,
{
key: `bar-label-${index}`,
style: labelStyle,
x: labelPosition.x + padding.x,
y: labelPosition.y - padding.y,
y0: position.y0,
text,
index,
scale,
datum: dataProps.datum,
textAnchor: labelStyle.textAnchor || anchors.text,
verticalAnchor: labelStyle.verticalAnchor || anchors.vertical,
angle: labelStyle.angle
}
);
const barLabel = React.cloneElement(labelComponent, assign({
events: Helpers.getPartialEvents(labelEvents, index, labelProps)
}, labelProps));
return (
<g key={`bar-group-${index}`}>
{barComponent}
{barLabel}
</g>
);
}
return barComponent;
});
}
render() {
// If animating, return a `VictoryAnimation` element that will create
// a new `VictoryBar` with nearly identical props, except (1) tweened
// and (2) `animate` set to null so we don't recurse forever.
if (this.props.animate) {
const whitelist = [
"data", "domain", "height", "padding", "style", "width"
];
return (
<VictoryTransition animate={this.props.animate} animationWhitelist={whitelist}>
<VictoryBar {...this.props}/>
</VictoryTransition>
);
}
const style = Helpers.getStyles(
this.props.style,
defaultStyles,
"auto",
"100%"
);
const data = Data.getData(this.props);
const group = <g style={style.parent}>{this.renderData(this.props, data, style)}</g>;
return this.props.standalone ?
<svg
style={style.parent}
viewBox={`0 0 ${this.props.width} ${this.props.height}`}
{...this.props.events.parent}
>
{group}
</svg> :
group;
}
}