victory-chart
Version:
Chart Component for Victory
273 lines (254 loc) • 9.27 kB
JavaScript
import { assign, defaults } from "lodash";
import React, { PropTypes } from "react";
import {
PropTypes as CustomPropTypes, Helpers, VictorySharedEvents, VictoryContainer,
VictoryTheme, Scale
} from "victory-core";
import Wrapper from "../../helpers/wrapper";
const fallbackProps = {
width: 450,
height: 300,
padding: 50
};
export default class VictoryStack extends React.Component {
static displayName = "VictoryStack";
static role = "stack";
static propTypes = {
animate: PropTypes.object,
categories: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.string),
PropTypes.shape({
x: PropTypes.arrayOf(PropTypes.string), y: PropTypes.arrayOf(PropTypes.string)
})
]),
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.node), React.PropTypes.node
]),
colorScale: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.string),
PropTypes.oneOf([
"grayscale", "qualitative", "heatmap", "warm", "cool", "red", "green", "blue"
])
]),
containerComponent: PropTypes.element,
domainPadding: PropTypes.oneOfType([
PropTypes.shape({
x: PropTypes.oneOfType([ PropTypes.number, CustomPropTypes.domain ]),
y: PropTypes.oneOfType([ PropTypes.number, CustomPropTypes.domain ])
}),
PropTypes.number
]),
dataComponent: PropTypes.element,
domain: PropTypes.oneOfType([
CustomPropTypes.domain,
PropTypes.shape({ x: CustomPropTypes.domain, y: CustomPropTypes.domain })
]),
events: PropTypes.arrayOf(PropTypes.shape({
childName: PropTypes.oneOfType([
PropTypes.string,
PropTypes.array
]),
target: PropTypes.oneOf(["data", "labels", "parent"]),
eventKey: PropTypes.oneOfType([
PropTypes.array,
PropTypes.func,
CustomPropTypes.allOfType([CustomPropTypes.integer, CustomPropTypes.nonNegative]),
PropTypes.string
]),
eventHandlers: PropTypes.object
})),
eventKey: PropTypes.oneOfType([
PropTypes.func,
CustomPropTypes.allOfType([CustomPropTypes.integer, CustomPropTypes.nonNegative]),
PropTypes.string
]),
groupComponent: PropTypes.element,
height: CustomPropTypes.nonNegative,
horizontal: PropTypes.bool,
labels: PropTypes.oneOfType([ PropTypes.func, PropTypes.array ]),
labelComponent: PropTypes.element,
name: PropTypes.string,
padding: PropTypes.oneOfType([
PropTypes.number,
PropTypes.shape({
top: PropTypes.number, bottom: PropTypes.number,
left: PropTypes.number, right: PropTypes.number
})
]),
samples: CustomPropTypes.nonNegative,
scale: PropTypes.oneOfType([
CustomPropTypes.scale,
PropTypes.shape({ x: CustomPropTypes.scale, y: CustomPropTypes.scale })
]),
sharedEvents: PropTypes.shape({
events: PropTypes.array,
getEventState: PropTypes.func
}),
standalone: PropTypes.bool,
style: PropTypes.shape({
parent: PropTypes.object, data: PropTypes.object, labels: PropTypes.object
}),
theme: PropTypes.object,
width: CustomPropTypes.nonNegative,
xOffset: PropTypes.number
};
static defaultProps = {
scale: "linear",
standalone: true,
containerComponent: <VictoryContainer/>,
groupComponent: <g/>,
theme: VictoryTheme. grayscale
};
static getDomain = Wrapper.getStackedDomain.bind(Wrapper);
static getData = Wrapper.getData.bind(Wrapper);
constructor(props) {
super(props);
if (props.animate) {
this.state = {
nodesShouldLoad: false,
nodesDoneLoad: false,
animating: true
};
this.setAnimationState = Wrapper.setAnimationState.bind(this);
}
}
componentWillReceiveProps(nextProps) {
if (this.props.animate) {
this.setAnimationState(this.props, nextProps);
}
}
getCalculatedProps(props, childComponents, style) {
const horizontal = props.horizontal || childComponents.every(
(component) => component.props.horizontal
);
const datasets = Wrapper.getDataFromChildren(props);
const domain = {
x: Wrapper.getStackedDomain(props, "x", datasets),
y: Wrapper.getStackedDomain(props, "y", datasets)
};
const range = {
x: Helpers.getRange(props, "x"),
y: Helpers.getRange(props, "y")
};
const baseScale = {
x: Scale.getScaleFromProps(props, "x") || Scale.getDefaultScale(),
y: Scale.getScaleFromProps(props, "y") || Scale.getDefaultScale()
};
const xScale = baseScale.x.domain(domain.x).range(range.x);
const yScale = baseScale.y.domain(domain.y).range(range.y);
const scale = {
x: horizontal ? yScale : xScale,
y: horizontal ? xScale : yScale
};
const categories = {
x: Wrapper.getCategories(props, "x"),
y: Wrapper.getCategories(props, "y")
};
const colorScale = props.colorScale;
return {datasets, categories, range, domain, horizontal, scale, style, colorScale};
}
addLayoutData(props, calculatedProps, datasets, index) { // eslint-disable-line max-params
const xOffset = props.xOffset || 0;
return datasets[index].map((datum) => {
const yOffset = Wrapper.getY0(datum, index, calculatedProps) || 0;
return assign({}, datum, {
y0: datum.y instanceof Date ? yOffset && new Date(yOffset) || datum.y : yOffset,
y1: datum.y instanceof Date ? new Date(+datum.y + +yOffset) : datum.y + yOffset,
x1: datum.x instanceof Date ? new Date(+datum.x + +xOffset) : datum.x + xOffset
});
});
}
getLabels(props, datasets, index) {
if (!props.labels) {
return undefined;
}
return datasets.length === index + 1 ? props.labels : undefined;
}
getChildProps(props, calculatedProps) {
const { categories, domain, scale, horizontal } = calculatedProps;
return {
height: props.height,
width: props.width,
padding: Helpers.getPadding(props),
standalone: false,
theme: props.theme,
categories,
domain,
scale,
horizontal
};
}
getColorScale(props, child) {
const role = child.type && child.type.role;
const colorScaleOptions = child.props.colorScale || props.colorScale;
if (role !== "group" && role !== "stack") {
return undefined;
}
return props.theme ? colorScaleOptions || props.theme.props.colorScale
: colorScaleOptions;
}
// the old ones were bad
getNewChildren(props, childComponents, calculatedProps) {
const { datasets } = calculatedProps;
const childProps = this.getChildProps(props, calculatedProps);
const getAnimationProps = Wrapper.getAnimationProps.bind(this);
const newChildren = [];
for (let index = 0, len = childComponents.length; index < len; index++) {
const child = childComponents[index];
const data = this.addLayoutData(props, calculatedProps, datasets, index);
const style = Wrapper.getChildStyle(child, index, calculatedProps);
const labels = props.labels ? this.getLabels(props, datasets, index) : child.props.labels;
newChildren[index] = React.cloneElement(child, assign({
animate: getAnimationProps(props, child, index),
key: index,
labels,
domainPadding: child.props.domainPadding || props.domainPadding,
theme: props.theme,
labelComponent: props.labelComponent || child.props.labelComponent,
style,
colorScale: this.getColorScale(props, child),
data
}, childProps));
}
return newChildren;
}
getContainer(props, calculatedProps) {
const { width, height, containerComponent } = props;
const { scale, style } = calculatedProps;
const parentProps = defaults(
{},
containerComponent.props,
{style: style.parent, scale, width, height}
);
return React.cloneElement(containerComponent, parentProps);
}
renderGroup(children, style) {
return React.cloneElement(
this.props.groupComponent,
{ role: "presentation", style},
children
);
}
render() {
const props = this.state && this.state.nodesWillExit ?
this.state.oldProps || this.props : this.props;
const modifiedProps = Helpers.modifyProps(props, fallbackProps, "stack");
const { theme, standalone, events, eventKey} = modifiedProps;
const fallbackStyle = theme && theme.stack && theme.stack.style ?
theme.stack.style : {};
const style = Helpers.getStyles(modifiedProps.style, fallbackStyle, "auto", "100%");
const childComponents = React.Children.toArray(modifiedProps.children);
const calculatedProps = this.getCalculatedProps(modifiedProps, childComponents, style);
const newChildren = this.getNewChildren(modifiedProps, childComponents, calculatedProps);
const group = this.renderGroup(newChildren, style.parent);
const container = standalone ? this.getContainer(modifiedProps, calculatedProps) : group;
if (events) {
return (
<VictorySharedEvents events={events} eventKey={eventKey} container={container}>
{newChildren}
</VictorySharedEvents>
);
}
return standalone ? React.cloneElement(container, container.props, group) : group;
}
}