victory-pie
Version:
Pie Component for Victory
394 lines (363 loc) • 11.8 kB
text/typescript
/* eslint no-magic-numbers: ["error", { "ignore": [-1, 0, 1, 2, 45, 90, 135, 180, 225, 270, 315, 360] }]*/
import defaults from "lodash/defaults";
import isPlainObject from "lodash/isPlainObject";
import * as d3Shape from "victory-vendor/d3-shape";
import { Helpers, Data, Style } from "victory-core";
import { VictoryPieProps } from "./victory-pie";
const checkForValidText = (text) => {
if (text === undefined || text === null || Helpers.isFunction(text)) {
return text;
}
return `${text}`;
};
const getColor = (style, colors, index) => {
if (style && style.data && style.data.fill) {
return style.data.fill;
}
return colors && colors[index % colors.length];
};
const getRadius = (props, padding) => {
if (typeof props.radius === "number") {
return props.radius;
}
return (
Math.min(
props.width - padding.left - padding.right,
props.height - padding.top - padding.bottom,
) / 2
);
};
const getOrigin = (props, padding) => {
const { width, height } = props;
const origin = isPlainObject(props.origin) ? props.origin : {};
return {
x:
origin.x !== undefined
? origin.x
: (padding.left - padding.right + width) / 2,
y:
origin.y !== undefined
? origin.y
: (padding.top - padding.bottom + height) / 2,
};
};
const getSlices = (props, data) => {
const padAngle = Helpers.isFunction(props.padAngle) ? 0 : props.padAngle;
const layoutFunction = d3Shape
.pie()
.sort(null)
.startAngle(Helpers.degreesToRadians(props.startAngle))
.endAngle(Helpers.degreesToRadians(props.endAngle))
.padAngle(Helpers.degreesToRadians(padAngle))
.value((datum: any) => {
return datum._y;
});
return layoutFunction(data);
};
const getCategoriesFromProps = (props: VictoryPieProps) =>
Array.isArray(props.categories)
? props.categories
: ((props?.categories as { x: string[] })?.x ?? []);
/**
* Sorts data by props.categories or props.categories.x. If all of the data keys aren't
* included in categories, any remaining data will be appended to the data array.
* If extraneous categories are included in the categories prop, the will be ignored and
* have no effect on the rendered component.
*/
const getDataSortedByCategories = (props: VictoryPieProps, data) => {
const sorted: string[] = [];
getCategoriesFromProps(props).forEach((category) => {
const idx = data.findIndex(({ x }) => x === category);
if (idx >= 0) {
const datum = data.splice(idx, 1)[0];
sorted.push(datum);
}
});
return [...sorted, ...data];
};
const getCalculatedValues = (props) => {
const { colorScale, theme } = props;
const styleObject = Helpers.getDefaultStyles(props, "pie");
const style = Helpers.getStyles(props.style, styleObject);
const colors = Array.isArray(colorScale)
? colorScale
: Style.getColorScale(colorScale, theme);
const padding = Helpers.getPadding(props.padding);
const defaultRadius = getRadius(props, padding);
const origin = getOrigin(props, padding);
const data = getDataSortedByCategories(props, Data.getData(props));
const slices = getSlices(props, data);
return Object.assign({}, props, {
style,
colors,
padding,
defaultRadius,
data,
slices,
origin,
});
};
const getSliceStyle = (index, calculatedValues) => {
const { style, colors } = calculatedValues;
const fill = getColor(style, colors, index);
return Object.assign({ fill }, style.data);
};
const getLabelText = (props, datum, index) => {
let text;
if (datum.label) {
text = datum.label;
} else if (Array.isArray(props.labels)) {
text = props.labels[index];
} else {
text = Helpers.isFunction(props.labels)
? props.labels
: datum.xName || datum._x;
}
return checkForValidText(text);
};
const getLabelArc = (labelRadius) => {
return d3Shape.arc().outerRadius(labelRadius).innerRadius(labelRadius);
};
const getCalculatedLabelRadius = (radius, labelRadius, style) => {
const padding = (style && style.padding) || 0;
return labelRadius || radius + padding;
};
const getLabelPosition = (arc, slice, position) => {
const construct = {
startAngle: position === "startAngle" ? slice.startAngle : slice.endAngle,
endAngle: position === "endAngle" ? slice.endAngle : slice.startAngle,
};
const clonedArc = Object.assign({}, slice, construct);
return arc.centroid(clonedArc);
};
const getLabelOrientation = (degree, labelPlacement) => {
if (labelPlacement === "perpendicular") {
return degree > 90 && degree < 270 ? "bottom" : "top";
} else if (labelPlacement === "parallel") {
return degree >= 0 && degree <= 180 ? "right" : "left";
}
if (degree < 45 || degree > 315) {
return "top";
} else if (degree >= 45 && degree < 135) {
return "right";
} else if (degree >= 135 && degree < 225) {
return "bottom";
}
return "left";
};
const getTextAnchor = (orientation) => {
if (orientation === "top" || orientation === "bottom") {
return "middle";
}
return orientation === "right" ? "start" : "end";
};
const getVerticalAnchor = (orientation) => {
if (orientation === "left" || orientation === "right") {
return "middle";
}
return orientation === "bottom" ? "start" : "end";
};
const getBaseLabelAngle = (slice, labelPosition, labelStyle) => {
let baseAngle = 0;
if (labelPosition.angle !== undefined) {
baseAngle = labelStyle.angle;
} else if (labelPosition === "centroid") {
baseAngle = Helpers.radiansToDegrees(
(slice.startAngle + slice.endAngle) / 2,
);
} else {
baseAngle =
labelPosition === "startAngle"
? Helpers.radiansToDegrees(slice.startAngle)
: Helpers.radiansToDegrees(slice.endAngle);
}
const positiveAngle = baseAngle < 0 ? 360 - baseAngle : baseAngle;
return positiveAngle % 360;
};
const getLabelAngle = (baseAngle, labelPlacement) => {
if (labelPlacement === "vertical") {
return 0;
}
if (labelPlacement === "parallel") {
return baseAngle > 180 && baseAngle < 360 ? baseAngle + 90 : baseAngle - 90;
}
return baseAngle > 90 && baseAngle < 270 ? baseAngle - 180 : baseAngle;
};
const getLabelProps = (text, dataProps, calculatedValues) => {
const { index, datum, data, slice, labelComponent, theme } = dataProps;
const { style, defaultRadius, origin, width, height } = calculatedValues;
const labelRadius = Helpers.evaluateProp(
calculatedValues.labelRadius,
Object.assign({ text }, dataProps),
);
const labelPosition =
Helpers.evaluateProp(
calculatedValues.labelPosition,
Object.assign({ text }, dataProps),
) || "centroid";
const labelPlacement =
Helpers.evaluateProp(
calculatedValues.labelPlacement,
Object.assign({ text }, dataProps),
) || "vertical";
const labelStyle = Object.assign({ padding: 0 }, style.labels);
const evaluatedStyle = Helpers.evaluateStyle(
labelStyle,
Object.assign({ labelRadius, text }, dataProps),
);
const calculatedLabelRadius = getCalculatedLabelRadius(
defaultRadius,
labelRadius,
evaluatedStyle,
);
const labelArc = getLabelArc(calculatedLabelRadius);
const position = getLabelPosition(labelArc, slice, labelPosition);
const baseAngle = getBaseLabelAngle(slice, labelPosition, labelStyle);
const labelAngle = getLabelAngle(baseAngle, labelPlacement);
const orientation = getLabelOrientation(baseAngle, labelPlacement);
const textAnchor = labelStyle.textAnchor || getTextAnchor(orientation);
const verticalAnchor =
labelStyle.verticalAnchor || getVerticalAnchor(orientation);
const labelProps = {
width,
height,
index,
datum,
data,
slice,
orientation,
text,
style: labelStyle,
x: Math.round(position[0]) + origin.x,
y: Math.round(position[1]) + origin.y,
textAnchor,
verticalAnchor,
angle: labelAngle,
calculatedLabelRadius,
};
if (!Helpers.isTooltip(labelComponent)) {
return labelProps;
}
const tooltipTheme = (theme && theme.tooltip) || {};
return defaults({}, labelProps, Helpers.omit(tooltipTheme, ["style"]));
};
export const getXOffsetMultiplayerByAngle = (angle) =>
Math.cos(angle - Helpers.degreesToRadians(90));
export const getYOffsetMultiplayerByAngle = (angle) =>
Math.sin(angle - Helpers.degreesToRadians(90));
export const getXOffset = (offset, angle) =>
offset * getXOffsetMultiplayerByAngle(angle);
export const getYOffset = (offset, angle) =>
offset * getYOffsetMultiplayerByAngle(angle);
export const getAverage = (array) =>
array.reduce((acc, cur) => acc + cur, 0) / array.length;
export const getLabelIndicatorPropsForLineSegment = (
props,
calculatedValues,
labelProps,
) => {
const {
innerRadius,
radius,
slice: { startAngle, endAngle },
labelIndicatorInnerOffset,
labelIndicatorOuterOffset,
index,
} = props;
const { height, width } = calculatedValues;
const { calculatedLabelRadius } = labelProps;
// calculation
const middleRadius = getAverage([innerRadius, radius]);
const midAngle = getAverage([endAngle, startAngle]);
const centerX = width / 2;
const centerY = height / 2;
const innerOffset = middleRadius + labelIndicatorInnerOffset;
const outerOffset = calculatedLabelRadius - labelIndicatorOuterOffset;
const x1 = centerX + getXOffset(innerOffset, midAngle);
const y1 = centerY + getYOffset(innerOffset, midAngle);
const x2 = centerX + getXOffset(outerOffset, midAngle);
const y2 = centerY + getYOffset(outerOffset, midAngle);
const labelIndicatorProps = {
x1,
y1,
x2,
y2,
index,
};
return defaults({}, labelIndicatorProps);
};
export const getBaseProps = (initialProps, fallbackProps) => {
const props = Helpers.modifyProps(initialProps, fallbackProps, "pie");
const calculatedValues = getCalculatedValues(props);
const {
slices,
style,
data,
origin,
defaultRadius,
labels,
events,
sharedEvents,
height,
width,
standalone,
name,
innerRadius,
cornerRadius,
padAngle,
disableInlineStyles,
labelIndicator,
} = calculatedValues;
const radius = props.radius || defaultRadius;
const initialChildProps = {
parent: { standalone, height, width, slices, name, style: style.parent },
};
return slices.reduce((childProps, slice, index) => {
const datum = defaults({}, data[index], {
startAngle: Helpers.radiansToDegrees(slice.startAngle),
endAngle: Helpers.radiansToDegrees(slice.endAngle),
padAngle: Helpers.radiansToDegrees(slice.padAngle),
});
const eventKey = !Helpers.isNil(datum.eventKey) ? datum.eventKey : index;
const dataProps = {
index,
slice,
datum,
data,
origin,
innerRadius,
radius,
cornerRadius,
padAngle,
style: disableInlineStyles ? {} : getSliceStyle(index, calculatedValues),
disableInlineStyles,
};
childProps[eventKey] = {
data: dataProps,
};
const text = getLabelText(props, datum, index);
if (
(text !== undefined && text !== null) ||
(labels && (events || sharedEvents))
) {
const evaluatedText = Helpers.evaluateProp(text, dataProps);
childProps[eventKey].labels = getLabelProps(
evaluatedText,
Object.assign({}, props, dataProps),
calculatedValues,
);
if (labelIndicator) {
const labelProps = childProps[eventKey].labels;
if (labelProps.calculatedLabelRadius > radius) {
childProps[eventKey].labelIndicators =
getLabelIndicatorPropsForLineSegment(
Object.assign({}, props, dataProps),
calculatedValues,
labelProps,
);
}
}
}
return childProps;
}, initialChildProps);
};