victory-legend
Version:
Legend Component for Victory
351 lines (350 loc) • 10.9 kB
JavaScript
import defaults from "lodash/defaults";
import groupBy from "lodash/groupBy";
import range from "lodash/range";
import { Helpers, Style, TextSize } from "victory-core";
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 = function (props, styleObject) {
if (styleObject === void 0) {
styleObject = {};
}
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((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((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) {
if (array && array.length) {
let value = 0;
for (let i = 0; i < array.length; i++) {
value += array[i];
}
return value;
}
return 0;
}