victory-chart
Version:
Chart Component for Victory
226 lines (201 loc) • 7.95 kB
JavaScript
import { assign, omit, defaults, isArray, flatten, pick } from "lodash";
import { Helpers, Log, Scale, Domain, Data } from "victory-core";
export default {
getBaseProps(props, fallbackProps) {
props = Helpers.modifyProps(props, fallbackProps, "errorbar");
const { data, style, scale } = this.getCalculatedValues(props, fallbackProps);
const { groupComponent, height, width, borderWidth } = props;
const childProps = { parent: {style: style.parent, scale, data, height, width} };
for (let index = 0, len = data.length; index < len; index++) {
const datum = data[index];
const eventKey = datum.eventKey || index;
const x = scale.x(datum.x1 || datum.x);
const y = scale.y(datum.y1 || datum.y);
const dataProps = {
x, y, scale, datum, data, index, groupComponent, borderWidth,
style: this.getDataStyles(datum, style.data),
errorX: this.getErrors(datum, scale, "x"),
errorY: this.getErrors(datum, scale, "y")
};
childProps[eventKey] = {
data: dataProps
};
const text = this.getLabelText(props, datum, index);
if (text !== undefined && text !== null || props.events || props.sharedEvents) {
childProps[eventKey].labels = this.getLabelProps(dataProps, text, style);
}
}
return childProps;
},
getLabelProps(dataProps, text, calculatedStyle) {
const { x, index, scale, errorY} = dataProps;
const error = errorY && Array.isArray(errorY) ? errorY[0] : errorY;
const y = error || dataProps.y;
const labelStyle = this.getLabelStyle(calculatedStyle.labels, dataProps) || {};
return {
style: labelStyle,
y: y - (labelStyle.padding || 0),
x,
text,
index,
scale,
datum: dataProps.datum,
data: dataProps.data,
textAnchor: labelStyle.textAnchor,
verticalAnchor: labelStyle.verticalAnchor || "end",
angle: labelStyle.angle
};
},
getErrorData(props) {
if (props.data) {
if (props.data.length < 1) {
Log.warn("This is an empty dataset.");
return [];
}
return this.formatErrorData(props.data, props);
} else {
const generatedData = (props.errorX || props.errorY) && this.generateData(props);
return this.formatErrorData(generatedData, props);
}
},
getErrors(datum, scale, axis) {
/**
* check if it is asymmetric error or symmetric error, asymmetric error should be an array
* and the first value is the positive error, the second is the negative error
* @param {Boolean} isArray(errorX)
* @return {String or Array}
*/
const errorNames = {x: "errorX", y: "errorY"};
const errors = datum[errorNames[axis]];
if (errors === 0) {
return false;
}
return isArray(errors) ?
[ errors[0] === 0 ? false : scale[axis](errors[0] + datum[axis]),
errors[1] === 0 ? false : scale[axis](datum[axis] - errors[1]) ] :
[ scale[axis](errors + datum[axis]), scale[axis](datum[axis] - errors) ];
},
formatErrorData(dataset, props) {
if (!dataset) {
return [];
}
const accessor = {
x: Helpers.createAccessor(props.x !== undefined ? props.x : "x"),
y: Helpers.createAccessor(props.y !== undefined ? props.y : "y"),
errorX: Helpers.createAccessor(props.errorX !== undefined ? props.errorX : "errorX"),
errorY: Helpers.createAccessor(props.errorY !== undefined ? props.errorY : "errorY")
};
const replaceNegatives = (errors) => {
// check if the value is negative, if it is set to 0
const replaceNeg = (val) => !val || val < 0 ? 0 : val;
return isArray(errors) ? errors.map((err) => replaceNeg(err)) : replaceNeg(errors);
};
const stringMap = {
x: Data.createStringMap(props, "x"),
y: Data.createStringMap(props, "y")
};
return dataset.map((datum, index) => {
const evaluatedX = accessor.x(datum);
const evaluatedY = accessor.y(datum);
const x = evaluatedX !== undefined ? evaluatedX : index;
const y = evaluatedY !== undefined ? evaluatedY : datum;
const errorX = replaceNegatives(accessor.errorX(datum));
const errorY = replaceNegatives(accessor.errorY(datum));
return assign(
{},
datum,
{ x, y, errorX, errorY },
// map string data to numeric values, and add names
typeof x === "string" ? { x: stringMap.x[x], xName: x } : {},
typeof y === "string" ? { y: stringMap.y[y], yName: y } : {}
);
});
},
getDomain(props, axis) {
const propsDomain = Domain.getDomainFromProps(props, axis);
if (propsDomain) {
return Domain.padDomain(propsDomain, props, axis);
}
const categoryDomain = Domain.getDomainFromCategories(props, axis);
if (categoryDomain) {
return Domain.padDomain(categoryDomain, props, axis);
}
const dataset = this.getErrorData(props);
if (dataset.length < 1) {
return Scale.getBaseScale(props, axis).domain();
}
const domain = this.getDomainFromData(props, axis, dataset);
return Domain.cleanDomain(Domain.padDomain(domain, props, axis), props);
},
getDomainFromData(props, axis, dataset) {
const currentAxis = Helpers.getCurrentAxis(axis, props.horizontal);
let error;
if (currentAxis === "x") {
error = "errorX";
} else if (currentAxis === "y") {
error = "errorY";
}
const axisData = flatten(dataset).map((datum) => datum[currentAxis]);
const errorData = flatten(flatten(dataset).map((datum) => {
let errorMax;
let errorMin;
if (isArray(datum[error])) {
errorMax = datum[error][0] + datum[currentAxis];
errorMin = datum[currentAxis] - datum[error][1];
} else {
errorMax = datum[error] + datum[currentAxis];
errorMin = datum[currentAxis] - datum[error];
}
return [errorMax, errorMin];
}));
const allData = axisData.concat(errorData);
const min = Math.min(...allData);
const max = Math.max(...allData);
// TODO: is this the correct behavior, or should we just error. How do we
// handle charts with just one data point?
if (min === max) {
const adjustedMax = max === 0 ? 1 : max;
return [0, adjustedMax];
}
return [min, max];
},
getCalculatedValues(props) {
const defaultStyles = props.theme && props.theme.errorbar && props.theme.errorbar.style ?
props.theme.errorbar.style : {};
const style = Helpers.getStyles(props.style, defaultStyles, "auto", "100%") || {};
const dataWithErrors = assign(Data.getData(props), this.getErrorData(props));
const data = Data.addEventKeys(props, dataWithErrors);
const range = {
x: Helpers.getRange(props, "x"),
y: Helpers.getRange(props, "y")
};
const domain = {
x: this.getDomain(props, "x"),
y: this.getDomain(props, "y")
};
const scale = {
x: Scale.getBaseScale(props, "x").domain(domain.x).range(range.x),
y: Scale.getBaseScale(props, "y").domain(domain.y).range(range.y)
};
return {data, scale, style};
},
getDataStyles(datum, style) {
const stylesFromData = omit(datum, [
"x", "y", "name", "errorX", "errorY", "eventKey"
]);
const baseDataStyle = defaults({}, stylesFromData, style);
return Helpers.evaluateStyle(baseDataStyle, datum);
},
getLabelText(props, datum, index) {
return datum.label || (Array.isArray(props.labels) ?
props.labels[index] : Helpers.evaluateProp(props.labels, datum));
},
getLabelStyle(labelStyle, dataProps) {
labelStyle = labelStyle || {};
const { datum, size, style } = dataProps;
const matchedStyle = pick(style, ["opacity", "fill"]);
const padding = labelStyle.padding || size * 0.25;
const baseLabelStyle = defaults({}, labelStyle, matchedStyle, {padding});
return Helpers.evaluateStyle(baseLabelStyle, datum) || {};
}
};