victory-legend
Version:
Legend Component for Victory
350 lines (327 loc) • 11 kB
text/typescript
import defaults from "lodash/defaults";
import groupBy from "lodash/groupBy";
import range from "lodash/range";
import { Helpers, Style, TextSize } from "victory-core";
import { VictoryLegendProps } from "./victory-legend";
const getColorScale = (props) => {
const { colorScale, theme } = props;
return typeof colorScale === "string"
? Style.getColorScale(colorScale, theme)
: colorScale || [];
};
const getLabelStyles = (props) => {
const { data, style } = props;
return data.map((datum, index) => {
const baseLabelStyles = defaults({}, datum.labels, style.labels);
return Helpers.evaluateStyle(baseLabelStyles, { datum, index, data });
});
};
const getStyles = (props, styleObject: VictoryLegendProps["style"] = {}) => {
const style = props.style || {};
const parentStyleProps = { height: "100%", width: "100%" };
return {
parent: defaults(style.parent, styleObject.parent, parentStyleProps),
data: defaults({}, style.data, styleObject.data),
labels: defaults({}, style.labels, styleObject.labels),
border: defaults({}, style.border, styleObject.border),
title: defaults({}, style.title, styleObject.title),
};
};
const getCalculatedValues = (props) => {
const { orientation, theme } = props;
const defaultStyles =
theme && theme.legend && theme.legend.style ? theme.legend.style : {};
const style = getStyles(props, defaultStyles);
const colorScale = getColorScale(props);
const isHorizontal = orientation === "horizontal";
const borderPadding = Helpers.getPadding(props.borderPadding);
return Object.assign({}, props, {
style,
isHorizontal,
colorScale,
borderPadding,
});
};
const getColumn = (props, index) => {
const { itemsPerRow, isHorizontal } = props;
if (!itemsPerRow) {
return isHorizontal ? index : 0;
}
return isHorizontal ? index % itemsPerRow : Math.floor(index / itemsPerRow);
};
const getRow = (props, index) => {
const { itemsPerRow, isHorizontal } = props;
if (!itemsPerRow) {
return isHorizontal ? 0 : index;
}
return isHorizontal ? Math.floor(index / itemsPerRow) : index % itemsPerRow;
};
const groupData = (props) => {
const { data } = props;
const style = (props.style && props.style.data) || {};
const labelStyles = getLabelStyles(props);
return data.map((datum, index) => {
const symbol = datum.symbol || {};
const { fontSize } = labelStyles[index];
// eslint-disable-next-line no-magic-numbers
const size = symbol.size || style.size || fontSize / 2.5;
const symbolSpacer = props.symbolSpacer || Math.max(size, fontSize);
return {
...datum,
size,
symbolSpacer,
fontSize,
textSize: TextSize.approximateTextSize(datum.name, labelStyles[index]),
column: getColumn(props, index),
row: getRow(props, index),
};
});
};
const getColumnWidths = (props, data) => {
const gutter = props.gutter || {};
const gutterWidth =
typeof gutter === "object"
? (gutter.left || 0) + (gutter.right || 0)
: gutter || 0;
const dataByColumn = groupBy(data, "column");
const columns = Object.keys(dataByColumn);
return columns.reduce<number[]>((memo, curr, index) => {
const lengths = dataByColumn[curr].map((d) => {
return d.textSize.width + d.size + d.symbolSpacer + gutterWidth;
});
memo[index] = Math.max(...lengths);
return memo;
}, []);
};
const getRowHeights = (props, data) => {
const gutter = props.rowGutter || {};
const gutterHeight =
typeof gutter === "object"
? (gutter.top || 0) + (gutter.bottom || 0)
: gutter || 0;
const dataByRow = groupBy(data, "row");
return Object.keys(dataByRow).reduce<number[]>((memo, curr, index) => {
const rows = dataByRow[curr];
const lengths = rows.map((d) => {
return d.textSize.height + d.symbolSpacer + gutterHeight;
});
memo[index] = Math.max(...lengths);
return memo;
}, []);
};
const getTitleDimensions = (props) => {
const style = (props.style && props.style.title) || {};
const textSize = TextSize.approximateTextSize(props.title, style);
const padding = style.padding || 0;
return {
height: textSize.height + 2 * padding || 0,
width: textSize.width + 2 * padding || 0,
};
};
const getOffset = (datum, rowHeights, columnWidths) => {
const { column, row } = datum;
return {
x: range(column).reduce((memo, curr) => memo + columnWidths[curr], 0),
y: range(row).reduce((memo, curr) => memo + rowHeights[curr], 0),
};
};
const getAnchors = (titleOrientation, centerTitle) => {
const standardAnchors = {
textAnchor: titleOrientation === "right" ? "end" : "start",
verticalAnchor: titleOrientation === "bottom" ? "end" : "start",
};
if (centerTitle) {
const horizontal =
titleOrientation === "top" || titleOrientation === "bottom";
return {
textAnchor: horizontal ? "middle" : standardAnchors.textAnchor,
verticalAnchor: horizontal ? standardAnchors.verticalAnchor : "middle",
};
}
return standardAnchors;
};
const getTitleStyle = (props) => {
const { titleOrientation, centerTitle, titleComponent } = props;
const baseStyle = (props.style && props.style.title) || {};
const componentStyle =
(titleComponent.props && titleComponent.props.style) || {};
const anchors = getAnchors(titleOrientation, centerTitle);
return Array.isArray(componentStyle)
? componentStyle.map((obj) => defaults({}, obj, baseStyle, anchors))
: defaults({}, componentStyle, baseStyle, anchors);
};
const getTitleProps = (props, borderProps) => {
const { title, titleOrientation, centerTitle, borderPadding } = props;
const { height, width } = borderProps;
const style = getTitleStyle(props);
const padding = Array.isArray(style) ? style[0].padding : style.padding;
const horizontal =
titleOrientation === "top" || titleOrientation === "bottom";
const xOrientation = titleOrientation === "bottom" ? "bottom" : "top";
const yOrientation = titleOrientation === "right" ? "right" : "left";
const standardPadding = {
x: centerTitle ? width / 2 : borderPadding[xOrientation] + (padding || 0),
y: centerTitle ? height / 2 : borderPadding[yOrientation] + (padding || 0),
};
const getPadding = () => {
return borderPadding[titleOrientation] + (padding || 0);
};
const xOffset = horizontal ? standardPadding.x : getPadding();
const yOffset = horizontal ? getPadding() : standardPadding.y;
return {
x:
titleOrientation === "right"
? props.x + width - xOffset
: props.x + xOffset,
y:
titleOrientation === "bottom"
? props.y + height - yOffset
: props.y + yOffset,
style,
text: title,
};
};
const getBorderProps = (props, contentHeight, contentWidth) => {
const { x, y, borderPadding, style } = props;
const height =
(contentHeight || 0) + borderPadding.top + borderPadding.bottom;
const width = (contentWidth || 0) + borderPadding.left + borderPadding.right;
return {
x,
y,
height,
width,
style: Object.assign({ fill: "none" }, style.border),
};
};
export const getDimensions = (initialProps, fallbackProps) => {
const modifiedProps = Helpers.modifyProps(
initialProps,
fallbackProps,
"legend",
);
const props = Object.assign(
{},
modifiedProps,
getCalculatedValues(modifiedProps),
);
const { title, titleOrientation } = props;
const groupedData = groupData(props);
const columnWidths = getColumnWidths(props, groupedData);
const rowHeights = getRowHeights(props, groupedData);
const titleDimensions = title
? getTitleDimensions(props)
: { height: 0, width: 0 };
return {
height:
titleOrientation === "left" || titleOrientation === "right"
? Math.max(sum(rowHeights), titleDimensions.height)
: sum(rowHeights) + titleDimensions.height,
width:
titleOrientation === "left" || titleOrientation === "right"
? sum(columnWidths) + titleDimensions.width
: Math.max(sum(columnWidths), titleDimensions.width),
};
};
export const getBaseProps = (initialProps, fallbackProps) => {
const modifiedProps = Helpers.modifyProps(
initialProps,
fallbackProps,
"legend",
);
const props = Object.assign(
{},
modifiedProps,
getCalculatedValues(modifiedProps),
);
const {
data,
standalone,
theme,
padding,
style,
colorScale,
gutter,
rowGutter,
borderPadding,
title,
titleOrientation,
name,
x = 0,
y = 0,
} = props;
const groupedData = groupData(props);
const columnWidths = getColumnWidths(props, groupedData);
const rowHeights = getRowHeights(props, groupedData);
const labelStyles = getLabelStyles(props);
const titleDimensions = title
? getTitleDimensions(props)
: { height: 0, width: 0 };
const titleOffset = {
x: titleOrientation === "left" ? titleDimensions.width : 0,
y: titleOrientation === "top" ? titleDimensions.height : 0,
};
const gutterOffset = {
x: gutter && typeof gutter === "object" ? gutter.left || 0 : 0,
y: rowGutter && typeof rowGutter === "object" ? rowGutter.top || 0 : 0,
};
const { height, width } = getDimensions(props, fallbackProps);
const borderProps = getBorderProps(props, height, width);
const titleProps = getTitleProps(props, borderProps);
const initialChildProps = {
parent: {
data,
standalone,
theme,
padding,
name,
height: props.height,
width: props.width,
style: style.parent,
},
all: { border: borderProps, title: titleProps },
};
return groupedData.reduce((childProps, datum, i) => {
const color = colorScale[i % colorScale.length];
const dataStyle = defaults({}, datum.symbol, style.data, { fill: color });
const eventKey = !Helpers.isNil(datum.eventKey) ? datum.eventKey : i;
const offset = getOffset(datum, rowHeights, columnWidths);
const originY = y + borderPadding.top + datum.symbolSpacer;
const originX = x + borderPadding.left + datum.symbolSpacer;
const dataProps = {
index: i,
data,
datum,
symbol: dataStyle.type || dataStyle.symbol || "circle",
size: datum.size,
style: dataStyle,
y: originY + offset.y + titleOffset.y + gutterOffset.y,
x: originX + offset.x + titleOffset.x + gutterOffset.x,
};
const labelProps = {
datum,
data,
text: datum.name,
style: labelStyles[i],
y: dataProps.y,
x: dataProps.x + datum.symbolSpacer + datum.size / 2,
};
childProps[eventKey] = { data: dataProps, labels: labelProps };
return childProps;
}, initialChildProps);
};
/**
* Computes the sum of the values in `array`.
* @param {Array} array The array to iterate over.
* @returns {number} Returns the sum.
*/
function sum(array: number[]) {
if (array && array.length) {
let value = 0;
for (let i = 0; i < array.length; i++) {
value += array[i];
}
return value;
}
return 0;
}