victory-chart
Version:
Chart Component for Victory
531 lines (511 loc) • 20.8 kB
JavaScript
import { assign, defaults } from "lodash";
import React, { PropTypes } from "react";
import {
PropTypes as CustomPropTypes, Helpers, VictoryTransition, VictoryLabel
} from "victory-core";
import AxisLine from "./axis-line";
import GridLine from "./grid";
import Tick from "./tick";
import AxisHelpers from "./helper-methods";
import Axis from "../../helpers/axis";
const defaultStyles = {
axis: {
stroke: "#756f6a",
fill: "none",
strokeWidth: 2,
strokeLinecap: "round"
},
axisLabel: {
stroke: "transparent",
fill: "#756f6a",
fontSize: 16,
fontFamily: "Helvetica"
},
grid: {
stroke: "none",
fill: "none",
strokeLinecap: "round"
},
ticks: {
stroke: "#756f6a",
fill: "none",
padding: 5,
strokeWidth: 2,
strokeLinecap: "round",
size: 4
},
tickLabels: {
stroke: "transparent",
fill: "#756f6a",
fontFamily: "Helvetica",
fontSize: 10,
padding: 5
}
};
const orientationSign = {
top: -1,
left: -1,
right: 1,
bottom: 1
};
const getStyles = (props) => {
const style = props.style || {};
const parentStyleProps = { height: "auto", width: "100%" };
return {
parent: defaults(parentStyleProps, style.parent, defaultStyles.parent),
axis: defaults({}, style.axis, defaultStyles.axis),
axisLabel: defaults({}, style.axisLabel, defaultStyles.axisLabel),
grid: defaults({}, style.grid, defaultStyles.grid),
ticks: defaults({}, style.ticks, defaultStyles.ticks),
tickLabels: defaults({}, style.tickLabels, defaultStyles.tickLabels)
};
};
export default class VictoryAxis extends React.Component {
static role = "axis";
static defaultTransitions = {
onExit: {
duration: 500
},
onEnter: {
duration: 500
}
};
static propTypes = {
/**
* The animate prop specifies props for victory-animation to use. It this prop is
* not given, the axis will not tween between changing data / style props.
* Large datasets might animate slowly due to the inherent limits of svg rendering.
* @examples {duration: 500, onEnd: () => alert("done!")}
*/
animate: PropTypes.object,
/**
* The axisComponent prop takes in an entire component which will be used
* to create the axis line. The new element created from the passed axisComponent
* will be supplied with the following properties: x1, y1, x2, y2, style 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 an axisComponent
* is not supplied, VictoryAxis will render its default AxisLine component.
*/
axisComponent: PropTypes.element,
/**
* The axisLabelComponent prop takes in an entire component which will be used
* to create the axis label. The new element created from the passed axisLabelComponent
* will be supplied with the following properties: x, y, verticalAnchor, textAnchor,
* angle, transform, style 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 an axisLabelComponent is not supplied, a new
* VictoryLabel will be created with props described above
*/
axisLabelComponent: PropTypes.element,
/**
* This prop specifies whether a given axis is intended to cross another axis.
*/
crossAxis: PropTypes.bool,
/**
* The dependentAxis prop specifies whether the axis corresponds to the
* dependent variable (usually y). This prop is useful when composing axis
* with other components to form a chart.
*/
dependentAxis: PropTypes.bool,
/**
* The domain prop describes the range of values your axis will include. This prop should be
* given as a array of the minimum and maximum expected values for your axis.
* If this value is not given it will be calculated based on the scale or tickValues.
* @examples [-1, 1]
*/
domain: 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 VictoryAxis
* i.e. `this.state.[index].axis = {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 VictoryAxis
* if one exists. If VictoryAxis 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 {ticks: {
* onClick: () =>
* return {ticks: {style: {stroke: "green"}}, tickLabels: {style: {stroke: "black"}}
*}}
*/
events: PropTypes.shape({
parent: PropTypes.object,
axis: PropTypes.object,
axisLabel: PropTypes.object,
grid: PropTypes.object,
ticks: PropTypes.object,
tickLabels: PropTypes.object
}),
/**
* The gridComponent prop takes in an entire component which will be used
* to create grid lines. The new element created from the passed gridComponent
* will be supplied with the following properties: x1, y1, x2, y2, tick, style 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 a gridComponent
* is not supplied, VictoryAxis will render its default GridLine component.
*/
gridComponent: PropTypes.element,
/**
* 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 label prop defines the label that will appear along the axis. This
* prop should be given as a value or an entire, HTML-complete label
* component. If a label component is given, it will be cloned. The new
* element's properties x, y, textAnchor, verticalAnchor, and transform
* will have defaults provided by the axis; styles filled out with
* defaults provided by the axis, and overrides from the label component.
* If a value is given, a new VictoryLabel will be created with props and
* styles from the axis.
*/
label: PropTypes.any,
/**
* This value describes how far from the "edge" of its permitted area each axis
* will be set back in the x-direction. If this prop is not given,
* the offset is calculated based on font size, axis orientation, and label padding.
*/
offsetX: PropTypes.number,
/**
* This value describes how far from the "edge" of its permitted area each axis
* will be set back in the y-direction. If this prop is not given,
* the offset is calculated based on font size, axis orientation, and label padding.
*/
offsetY: PropTypes.number,
/**
* The orientation prop specifies the position and orientation of your axis.
*/
orientation: PropTypes.oneOf(["top", "bottom", "left", "right"]),
/**
* 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 axis should use. This prop can be
* given as a `d3-scale@0.3.0` function or as a string corresponding to a supported d3-string
* function.
* @examples d3Scale.time(), "linear", "time", "log", "sqrt"
*/
scale: 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 VictoryAxis with other components within an enclosing <svg> tag.
*/
standalone: PropTypes.bool,
/**
* The style prop specifies styles for your VictoryAxis. 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.
* @examples {axis: {stroke: "#756f6a"}, grid: {stroke: "grey"}, ticks: {stroke: "grey"},
* tickLabels: {fontSize: 10, padding: 5}, axisLabel: {fontSize: 16, padding: 20}}
*/
style: PropTypes.shape({
parent: PropTypes.object,
axis: PropTypes.object,
axisLabel: PropTypes.object,
grid: PropTypes.object,
ticks: PropTypes.object,
tickLabels: PropTypes.object
}),
/**
* The tickComponent prop takes in an entire component which will be used
* to create tick lines. The new element created from the passed tickComponent
* will be supplied with the following properties: x1, y1, x2, y2, tick, style 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 a tickComponent
* is not supplied, VictoryAxis will render its default Tick component.
*/
tickComponent: PropTypes.element,
/**
* The tickCount prop specifies approximately how many ticks should be drawn on the axis if
* tickValues are not explicitly provided. This values is calculated by d3 scale and
* prioritizes returning "nice" values and evenly spaced ticks over an exact numnber of ticks
*/
tickCount: CustomPropTypes.nonNegative,
/**
* The tickLabelComponent prop takes in an entire component which will be used
* to create the tick labels. The new element created from the passed tickLabelComponent
* will be supplied with the following properties: x, y, verticalAnchor, textAnchor,
* angle, tick, style 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 an tickLabelComponent is not supplied, a new
* VictoryLabel will be created with props described above
*/
tickLabelComponent: PropTypes.element,
/**
* The tickFormat prop specifies how tick values should be expressed visually.
* tickFormat can be given as a function to be applied to every tickValue, or as
* an array of display values for each tickValue.
* @examples d3.time.format("%Y"), (x) => x.toPrecision(2), ["first", "second", "third"]
*/
tickFormat: PropTypes.oneOfType([
PropTypes.func,
CustomPropTypes.homogeneousArray
]),
/**
* The tickValues prop explicitly specifies which tick values to draw on the axis.
* @examples ["apples", "bananas", "oranges"], [2, 4, 6, 8]
*/
tickValues: CustomPropTypes.homogeneousArray,
/**
* 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
};
static defaultProps = {
axisComponent: <AxisLine/>,
axisLabelComponent: <VictoryLabel/>,
tickLabelComponent: <VictoryLabel/>,
tickComponent: <Tick/>,
gridComponent: <GridLine/>,
events: {},
height: 300,
padding: 50,
scale: "linear",
standalone: true,
tickCount: 5,
width: 450
};
static getDomain = AxisHelpers.getDomain.bind(AxisHelpers);
static getAxis = AxisHelpers.getAxis.bind(AxisHelpers);
static getScale = AxisHelpers.getScale.bind(AxisHelpers);
static getStyles = getStyles;
constructor() {
super();
this.state = {};
this.getEvents = Helpers.getEvents.bind(this);
this.getEventState = Helpers.getEventState.bind(this);
}
getTickProps(props) {
const stringTicks = Axis.stringTicks(props);
const scale = AxisHelpers.getScale(props);
const ticks = AxisHelpers.getTicks(props, scale);
return {scale, ticks, stringTicks};
}
getLayoutProps(props) {
const style = getStyles(props);
const padding = Helpers.getPadding(props);
const orientation = props.orientation || (props.dependentAxis ? "left" : "bottom");
const isVertical = Axis.isVertical(props);
const labelPadding = AxisHelpers.getLabelPadding(props, style);
const offset = AxisHelpers.getOffset(props, style);
return {style, padding, orientation, isVertical, labelPadding, offset};
}
renderLine(props, layoutProps) {
const {style, padding, isVertical} = layoutProps;
const axisEvents = this.getEvents(props.events.axis, "axis");
const axisProps = defaults(
{},
this.getEventState(0, "axis"),
props.axisComponent.props,
{
style: style.axis,
x1: isVertical ? null : padding.left,
x2: isVertical ? null : props.width - padding.right,
y1: isVertical ? padding.top : null,
y2: isVertical ? props.height - padding.bottom : null
}
);
return React.cloneElement(props.axisComponent, assign(
{}, axisProps, { events: Helpers.getPartialEvents(axisEvents, 0, axisProps) }
));
}
getAnchors(orientation, isVertical) {
const anchorOrientation = { top: "end", left: "end", right: "start", bottom: "start" };
const anchor = anchorOrientation[orientation];
return {
textAnchor: isVertical ? anchor : "middle",
verticalAnchor: isVertical ? "middle" : anchor
};
}
renderTicks(props, layoutProps, dataProps) {
const {style, orientation, isVertical} = layoutProps;
const {scale, ticks, stringTicks} = dataProps;
const tickFormat = AxisHelpers.getTickFormat(props, dataProps);
const tickPosition = AxisHelpers.getTickPosition(style.ticks, orientation, isVertical);
const tickEvents = this.getEvents(props.events.ticks, "ticks");
const labelEvents = this.getEvents(props.events.tickLabels, "tickLabels");
return ticks.map((data, index) => {
const tick = stringTicks ? props.tickValues[data - 1] : data;
const groupPosition = scale(data);
const yTransform = isVertical ? groupPosition : 0;
const xTransform = isVertical ? 0 : groupPosition;
const tickProps = defaults(
{},
this.getEventState(index, "ticks"),
props.tickComponent.props,
{
key: `tick-${index}`,
style: Helpers.evaluateStyle(style.ticks, tick),
x1: xTransform,
y1: yTransform,
x2: xTransform + tickPosition.x2,
y2: yTransform + tickPosition.y2,
tick
}
);
const tickComponent = React.cloneElement(props.tickComponent, assign(
{}, tickProps, {events: Helpers.getPartialEvents(tickEvents, index, tickProps)}
));
let labelComponent;
const label = tickFormat.call(this, tick, index);
if (label !== null && label !== undefined) {
const anchors = this.getAnchors(orientation, isVertical);
const labelStyle = Helpers.evaluateStyle(style.tickLabels, tick);
const labelProps = defaults(
{},
this.getEventState(index, "tickLabels"),
props.tickLabelComponent.props,
{
key: `tick-label-${index}`,
style: labelStyle,
x: xTransform + tickPosition.x,
y: yTransform + tickPosition.y,
verticalAnchor: labelStyle.verticalAnchor || anchors.verticalAnchor,
textAnchor: labelStyle.textAnchor || anchors.textAnchor,
angle: labelStyle.angle,
text: label,
tick
}
);
labelComponent = React.cloneElement(props.tickLabelComponent, assign(
{}, labelProps, {events: Helpers.getPartialEvents(labelEvents, index, labelProps)}
));
}
return (
<g key={`tick-group-${index}`}>
{tickComponent}
{labelComponent}
</g>
);
});
}
renderGrid(props, layoutProps, tickProps) {
const {scale, ticks, stringTicks} = tickProps;
const {style, padding, isVertical, offset, orientation} = layoutProps;
const xPadding = orientation === "right" ? padding.right : padding.left;
const yPadding = orientation === "top" ? padding.top : padding.bottom;
const sign = -orientationSign[orientation];
const xOffset = props.crossAxis ? offset.x - xPadding : 0;
const yOffset = props.crossAxis ? offset.y - yPadding : 0;
const x2 = isVertical ?
sign * (props.width - (padding.left + padding.right)) : 0;
const y2 = isVertical ?
0 : sign * (props.height - (padding.top + padding.bottom));
const gridEvents = this.getEvents(props.events.grid, "grid");
return ticks.map((data, index) => {
const tick = stringTicks ? props.tickValues[data - 1] : data;
// determine the position and translation of each gridline
const position = scale(data);
const xTransform = isVertical ? -xOffset : position;
const yTransform = isVertical ? position : yOffset;
const gridProps = defaults(
{},
this.getEventState(index, "grid"),
props.gridComponent.props,
{
key: `grid-${index}`,
style: Helpers.evaluateStyle(style.grid, tick),
x1: xTransform,
y1: yTransform,
x2: x2 + xTransform,
y2: y2 + yTransform,
tick
}
);
const gridComponent = React.cloneElement(props.gridComponent, assign(
{}, gridProps, {events: Helpers.getPartialEvents(gridEvents, index, gridProps)}
));
return gridComponent;
});
}
renderLabel(props, layoutProps) {
if (!props.label) {
return undefined;
}
const {style, orientation, padding, labelPadding, isVertical} = layoutProps;
const sign = orientationSign[orientation];
const hPadding = padding.left + padding.right;
const vPadding = padding.top + padding.bottom;
const x = isVertical ?
-((props.height - vPadding) / 2) - padding.top :
((props.width - hPadding) / 2) + padding.left;
const y = sign * labelPadding;
const verticalAnchor = sign < 0 ? "end" : "start";
const transform = isVertical ? "rotate(-90)" : "";
const labelEvents = this.getEvents(props.events.axisLabel, "axisLabel");
const labelStyle = style.axisLabel;
const labelProps = defaults(
{},
this.getEventState(0, "axisLabel"),
props.axisLabelComponent.props,
{
verticalAnchor: labelStyle.verticalAnchor || verticalAnchor,
textAnchor: labelStyle.textAnchor || "middle",
angle: labelStyle.angle,
style: labelStyle,
transform,
x,
y,
text: props.label
}
);
return React.cloneElement(props.axisLabelComponent, assign(
{}, labelProps, {events: Helpers.getPartialEvents(labelEvents, 0, labelProps)}
));
}
render() {
if (this.props.animate) {
// Do less work by having `VictoryAnimation` tween only values that
// make sense to tween. In the future, allow customization of animated
// prop whitelist/blacklist?
const whitelist = [
"style", "domain", "range", "tickCount", "tickValues",
"offsetX", "offsetY", "padding", "width", "height"
];
return (
<VictoryTransition animate={this.props.animate} animationWhitelist={whitelist}>
<VictoryAxis {...this.props}/>
</VictoryTransition>
);
}
const layoutProps = this.getLayoutProps(this.props);
const tickProps = this.getTickProps(this.props);
const {style} = layoutProps;
const transform = AxisHelpers.getTransform(this.props, layoutProps);
const group = (
<g style={style.parent} transform={transform}>
{this.renderGrid(this.props, layoutProps, tickProps)}
{this.renderLine(this.props, layoutProps)}
{this.renderTicks(this.props, layoutProps, tickProps)}
{this.renderLabel(this.props, layoutProps)}
</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;
}
}