victory-chart
Version:
Chart Component for Victory
432 lines (416 loc) • 15.7 kB
JSX
import defaults from "lodash/defaults";
import React, { PropTypes } from "react";
import { PropTypes as CustomPropTypes, VictoryTransition, Helpers } from "victory-core";
import AxisLine from "./axis-line";
import AxisLabel from "./axis-label";
import GridLine from "./grid";
import Tick from "./tick";
import TickLabel from "./tick-label";
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,
/**
* 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 unique index on the state object of VictoryAxis
* i.e. `this.state.axisState[axisIndex] = {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 {axis: {
* onClick: () => return {style: {stroke: "green"}}
*}}
*/
events: PropTypes.shape({
parent: PropTypes.object,
axis: PropTypes.object,
axisLabel: PropTypes.object,
grid: PropTypes.object,
ticks: PropTypes.object,
tickLabels: 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 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 tickCount prop specifies how many ticks should be drawn on the axis if
* tickValues are not explicitly provided.
*/
tickCount: CustomPropTypes.nonNegative,
/**
* 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 = {
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;
componentWillMount() {
this.state = {
axisState: {},
axisLabelState: {},
gridState: {},
ticksState: {},
tickLabelsState: {}
};
}
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 getBoundEvents = Helpers.getEvents.bind(this);
return (
<AxisLine key="line"
events={getBoundEvents(this.props.events.axis, "axis")}
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}
{...this.state.axisState[0]}
/>
);
}
renderTicks(props, layoutProps, tickProps) {
const {style, orientation} = layoutProps;
const {scale, ticks, stringTicks} = tickProps;
const tickFormat = AxisHelpers.getTickFormat(props, tickProps);
return ticks.map((tick, index) => {
const isVertical = orientation === "left" || orientation === "right";
const tickPosition = AxisHelpers.getTickPosition(style.ticks, orientation, isVertical);
const getBoundEvents = Helpers.getEvents.bind(this);
const tickComponent = (
<Tick key={`tick-${index}`}
index={index}
events={getBoundEvents(this.props.events.ticks, "ticks")}
position={tickPosition}
tick={stringTicks ? props.tickValues[tick - 1] : tick}
style={style.ticks}
{...this.state.ticksState[index]}
/>
);
const label = tickFormat.call(this, tick, index);
let labelComponent;
if (label) {
labelComponent = (
<TickLabel key={`tick-label-${index}`}
index={index}
events={getBoundEvents(this.props.events.tickLabels, "tickLabels")}
position={tickPosition}
label={label}
tick={stringTicks ? props.tickValues[tick - 1] : tick}
orientation={orientation}
isVertical={isVertical}
style={style.tickLabels}
{...this.state.tickLabelsState[index]}
/>
);
}
const groupPosition = scale(tick);
const transform = isVertical ?
`translate(0, ${groupPosition})` : `translate(${groupPosition}, 0)`;
return (
<g key={`tick-group-${index}`} transform={transform}>
{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));
return ticks.map((tick, index) => {
// determine the position and translation of each gridline
const position = scale(tick);
const getBoundEvents = Helpers.getEvents.bind(this);
return (
<GridLine key={`grid-${index}`}
index={index}
events={getBoundEvents(this.props.events.grid, "grid")}
tick={stringTicks ? props.tickValues[tick - 1] : tick}
x2={x2}
y2={y2}
xTransform={isVertical ? -xOffset : position}
yTransform={isVertical ? position : yOffset}
style={style.grid}
{...this.state.gridState[index]}
/>
);
});
}
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 getBoundEvents = Helpers.getEvents.bind(this);
return (
<AxisLabel
events={getBoundEvents(this.props.events.axisLabel, "axisLabels")}
verticalAnchor={verticalAnchor}
transform={transform}
position={{x, y}}
label={this.props.label}
style={style.axisLabel}
{...this.state.axisLabelState[0]}
/>
);
}
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;
}
}