diff --git a/demo/victory-legend-demo.js b/demo/victory-legend-demo.js index 02aeef2..09c284f 100644 --- a/demo/victory-legend-demo.js +++ b/demo/victory-legend-demo.js @@ -1,5 +1,5 @@ import React from "react"; -import { VictoryLegend } from "../src/index"; +import { VictoryLegend, VictoryLabel, Border } from "../src/index"; const containerStyle = { display: "flex", @@ -9,33 +9,45 @@ const containerStyle = { justifyContent: "center" }; -const legendStyle = { parent: { border: "1px solid #ccc", margin: "2%", maxHeight: 300 } }; +const legendStyle = { + labels: { fontSize: 14, fontFamily: "Palatino" }, + border: { stroke: "black", strokeWidth: 2 }, + title: { padding: 5, fill: "red" } +}; + +const symbolSize = 5; +const symbolSpacer = 10; const data = [{ name: "Series 1", symbol: { + size: symbolSize, type: "circle", fill: "green" } }, { - name: "Long Series Name", + name: "Long Series Name -- so long", symbol: { + size: symbolSize, type: "triangleUp", fill: "blue" } }, { name: "Series 3", symbol: { + size: symbolSize, type: "diamond", fill: "pink" } }, { name: "Series 4", symbol: { + size: symbolSize, type: "plus" } }, { name: "Series 5", symbol: { + size: symbolSize, type: "star", fill: "red" }, @@ -43,8 +55,9 @@ const data = [{ fill: "purple" } }, { - name: "Series 6", + name: "Series 6: also quite long", symbol: { + size: symbolSize, type: "circle", fill: "orange" }, @@ -56,14 +69,19 @@ const data = [{ const LegendDemo = () => (
} events={[{ target: "data", eventHandlers: { @@ -75,24 +93,59 @@ const LegendDemo = () => ( } }]} /> - - + + + + + } + centerTitle + title={["TITLE"]} + gutter={30} + symbolSpacer={symbolSpacer} + itemsPerRow={3} data={data} - itemsPerRow={4} - orientation="horizontal" style={legendStyle} />
diff --git a/src/index.js b/src/index.js index b3c2518..cb4b064 100644 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,7 @@ export { default as Portal } from "./victory-portal/portal"; export { default as Arc } from "./victory-primitives/arc"; export { default as Area } from "./victory-primitives/area"; export { default as Bar } from "./victory-primitives/bar"; +export { default as Border } from "./victory-primitives/border"; export { default as Candle } from "./victory-primitives/candle"; export { default as ClipPath } from "./victory-primitives/clip-path"; export { default as Curve } from "./victory-primitives/curve"; diff --git a/src/victory-label/victory-label.js b/src/victory-label/victory-label.js index e21d120..c835acb 100644 --- a/src/victory-label/victory-label.js +++ b/src/victory-label/victory-label.js @@ -187,7 +187,8 @@ export default class VictoryLabel extends React.Component { } getDy(props, style, content, lineHeight) { //eslint-disable-line max-params - const fontSize = style[0].fontSize; + style = Array.isArray(style) ? style[0] : style; + const fontSize = style.fontSize; const datum = props.datum || props.data; const dy = props.dy ? Helpers.evaluateProp(props.dy, datum) : 0; const length = content.length; diff --git a/src/victory-legend/helper-methods.js b/src/victory-legend/helper-methods.js index 2a90834..7b9347d 100644 --- a/src/victory-legend/helper-methods.js +++ b/src/victory-legend/helper-methods.js @@ -1,38 +1,8 @@ -import { defaults, assign, maxBy, sumBy } from "lodash"; +import { defaults, assign, groupBy, keys, sum, range } from "lodash"; import Helpers from "../victory-util/helpers"; import Style from "../victory-util/style"; import TextSize from "../victory-util/textsize"; - -const calculateLegendHeight = (props, textSizes) => { - const { gutter, itemsPerRow, padding, isHorizontal } = props; - const itemCount = textSizes.length; - const rowCount = itemsPerRow ? Math.ceil(itemCount / itemsPerRow) : 1; - const contentHeight = isHorizontal - ? maxBy(textSizes, "height").height * rowCount + gutter * (rowCount - 1) - : (sumBy(textSizes, "height") + gutter * (itemCount - 1)) / rowCount; - - return padding.top + contentHeight + padding.bottom; -}; - -const calculateLegendWidth = (props, itemCount, maxTextWidth) => { - const { gutter, itemsPerRow, symbolSpacer, padding, isHorizontal } = props; - const rowCount = itemsPerRow ? Math.ceil(itemCount / itemsPerRow) : 1; - const rowItemCount = itemsPerRow || itemCount; - let contentWidth; - - if (isHorizontal) { - const gutterWidth = gutter * rowItemCount; - const symbolWidth = symbolSpacer * 3 * rowItemCount; // eslint-disable-line no-magic-numbers - const textWidth = maxTextWidth * rowItemCount; - contentWidth = symbolWidth + textWidth + gutterWidth; - } else { - contentWidth = (maxTextWidth + symbolSpacer * 2 + gutter) * rowCount; - } - - return padding.left + contentWidth + padding.right; -}; - const getColorScale = (props) => { const { colorScale } = props; return typeof colorScale === "string" ? Style.getColorScale(colorScale) : colorScale || []; @@ -46,70 +16,220 @@ const getLabelStyles = (props) => { }); }; -const getTextSizes = (props, labelStyles) => { - return props.data.map((datum, i) => { - return TextSize.approximateTextSize(datum.name, labelStyles[i]); - }); +const getStyles = (props, styleObject) => { + const style = props.style || {}; + styleObject = styleObject || {}; + 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 = Helpers.getStyles(props.style, defaultStyles); + const style = getStyles(props, defaultStyles); const colorScale = getColorScale(props); const isHorizontal = orientation === "horizontal"; - const padding = Helpers.getPadding(props); + const borderPadding = Helpers.formatPadding(props.borderPadding); + return assign({}, props, { style, isHorizontal, colorScale, borderPadding }); +}; - return assign({}, props, { style, isHorizontal, colorScale, padding }); +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 getSymbolSize = (datum, fontSize) => { // eslint-disable-next-line no-magic-numbers return datum.symbol && datum.symbol.size ? datum.symbol.size : fontSize / 2.5; }; +const groupData = (props) => { + const { data } = props; + const style = props.style && props.style.data || {}; + const labelStyles = getLabelStyles(props); + return data.map((datum, index) => { + const { fontSize } = labelStyles[index]; + const size = style.size || getSymbolSize(datum, fontSize); + 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 dataByColumn = groupBy(data, "column"); + const columns = keys(dataByColumn); + return columns.reduce((memo, curr, index) => { + const gutter = index === columns.length - 1 ? 0 : props.gutter; + const lengths = dataByColumn[curr].map((d) => { + const symbolWidth = index && index === columns.length - 1 ? 0 : d.size + d.symbolSpacer; + return d.textSize.width + gutter + symbolWidth; + }); + memo[index] = Math.max(...lengths); + return memo; + }, []); +}; + +const getRowHeights = (props, data) => { + const dataByRow = groupBy(data, "row"); + return keys(dataByRow).reduce((memo, curr, index) => { + const rows = dataByRow[curr]; + const lengths = rows.map((d) => d.textSize.height + d.symbolSpacer); + 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]; + return memo; + }, 0), + y: range(row).reduce((memo, curr) => { + memo += rowHeights[curr]; + return memo; + }, 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" + }; + } else { + 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); +}; + +// eslint-disable-next-line complexity +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 + borderPadding.top + borderPadding.bottom; + const width = contentWidth + borderPadding.left + borderPadding.right; + return { x, y, height, width, style: style.border }; +}; + export default (props, fallbackProps) => { const modifiedProps = Helpers.modifyProps(props, fallbackProps, "legend"); props = assign({}, modifiedProps, getCalculatedValues(modifiedProps)); const { data, standalone, theme, padding, style, colorScale, - itemsPerRow, gutter, isHorizontal, symbolSpacer + borderPadding, title, titleOrientation, x = 0, y = 0 } = props; + const groupedData = groupData(props); + const columnWidths = getColumnWidths(props, groupedData); + const rowHeights = getRowHeights(props, groupedData); const labelStyles = getLabelStyles(props); - const textSizes = getTextSizes(props, labelStyles); - const maxTextWidth = Math.max(...textSizes.map((text) => text.width)); - const height = props.height || calculateLegendHeight(props, textSizes); - const width = props.width || calculateLegendWidth(props, textSizes.width, maxTextWidth); - const initialChildProps = { parent: { - width, height, data, standalone, theme, padding, style: style.parent - } }; - - return data.reduce((childProps, datum, i) => { - const { fontSize } = labelStyles[i]; - const symbolShift = fontSize / 2; - const symbolWidth = fontSize + symbolSpacer; - const rowHeight = fontSize + gutter; - const itemIndex = itemsPerRow ? i % itemsPerRow : i; - const rowIndex = itemsPerRow ? Math.floor(i / itemsPerRow) : 0; - const rowSpacer = itemsPerRow ? rowHeight * rowIndex : 0; - const eventKey = datum.eventKey || i; - const y = isHorizontal ? - padding.top + symbolShift + rowSpacer : padding.top + symbolShift + rowHeight * itemIndex; + const titleDimensions = title ? getTitleDimensions(props) : { height: 0, width: 0 }; + const titleOffset = { + x: titleOrientation === "left" ? titleDimensions.width : 0, + y: titleOrientation === "top" ? titleDimensions.height : 0 + }; + + const contentHeight = titleOrientation === "left" || titleOrientation === "right" ? + Math.max(sum(rowHeights), titleDimensions.height) : sum(rowHeights) + titleDimensions.height; + const contentWidth = titleOrientation === "left" || titleOrientation === "right" ? + sum(columnWidths) + titleDimensions.width : Math.max(sum(columnWidths), titleDimensions.width); + + const initialProps = { + parent: { + data, standalone, theme, padding, + height: props.height, + width: props.width, + style: style.parent + } + }; + const borderProps = getBorderProps(props, contentHeight, contentWidth); + const titleProps = getTitleProps(props, borderProps); + return groupedData.reduce((childProps, datum, i) => { const color = colorScale[i % colorScale.length]; const dataStyle = defaults({}, datum.symbol, style.data, { fill: color }); - + const 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, key: `legend-symbol-${i}`, - symbol: dataStyle.type || "circle", - size: getSymbolSize(datum, fontSize), + symbol: dataStyle.type || dataStyle.symbol || "circle", + size: datum.size, style: dataStyle, - y, - x: isHorizontal ? - padding.left + symbolShift + (fontSize + symbolSpacer + maxTextWidth + gutter) * itemIndex : - padding.left + symbolShift + (rowHeight + maxTextWidth) * rowIndex + y: originY + offset.y + titleOffset.y, + x: originX + offset.x + titleOffset.x }; const labelProps = { @@ -117,15 +237,14 @@ export default (props, fallbackProps) => { key: `legend-label-${i}`, text: datum.name, style: labelStyles[i], - y, - x: isHorizontal ? - padding.left + symbolWidth * (itemIndex + 1) + (maxTextWidth + gutter) * itemIndex : - padding.left + symbolWidth + (rowHeight + maxTextWidth) * rowIndex + y: originY + offset.y + titleOffset.y, + x: originX + offset.x + titleOffset.x + datum.symbolSpacer + (datum.size / 2) }; - - childProps[eventKey] = { data: dataProps, labels: labelProps }; + childProps[eventKey] = eventKey === 0 ? + { data: dataProps, labels: labelProps, border: borderProps, title: titleProps } : + { data: dataProps, labels: labelProps }; return childProps; - }, initialChildProps); + }, initialProps); }; diff --git a/src/victory-legend/victory-legend.js b/src/victory-legend/victory-legend.js index d07270b..c616b27 100644 --- a/src/victory-legend/victory-legend.js +++ b/src/victory-legend/victory-legend.js @@ -9,9 +9,12 @@ import VictoryLabel from "../victory-label/victory-label"; import VictoryContainer from "../victory-container/victory-container"; import VictoryTheme from "../victory-theme/victory-theme"; import Point from "../victory-primitives/point"; +import Border from "../victory-primitives/border"; const fallbackProps = { orientation: "vertical", + width: 450, + height: 300, x: 0, y: 0 }; @@ -27,6 +30,17 @@ class VictoryLegend extends React.Component { static role = "legend"; static propTypes = { + borderComponent: PropTypes.element, + borderPadding: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.shape({ + top: PropTypes.number, + bottom: PropTypes.number, + left: PropTypes.number, + right: PropTypes.number + }) + ]), + centerTitle: PropTypes.bool, colorScale: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.string), PropTypes.oneOf([ @@ -58,10 +72,7 @@ class VictoryLegend extends React.Component { })), groupComponent: PropTypes.element, gutter: PropTypes.number, - height: PropTypes.oneOfType([ - CustomPropTypes.nonNegative, - PropTypes.func - ]), + height: CustomPropTypes.nonNegative, itemsPerRow: PropTypes.number, labelComponent: PropTypes.element, orientation: PropTypes.oneOf(["horizontal", "vertical"]), @@ -80,40 +91,72 @@ class VictoryLegend extends React.Component { }), standalone: PropTypes.bool, style: PropTypes.shape({ + border: PropTypes.object, data: PropTypes.object, labels: PropTypes.object, - parent: PropTypes.object + parent: PropTypes.object, + title: PropTypes.object }), symbolSpacer: PropTypes.number, theme: PropTypes.object, - width: PropTypes.oneOfType([ - CustomPropTypes.nonNegative, - PropTypes.func - ]), + title: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), + titleComponent: PropTypes.element, + titleOrientation: PropTypes.oneOf(["top", "bottom", "left", "right"]), + width: CustomPropTypes.nonNegative, x: PropTypes.number, y: PropTypes.number }; static defaultProps = { + borderComponent: , data: defaultLegendData, containerComponent: , dataComponent: , groupComponent: , labelComponent: , standalone: true, - theme: VictoryTheme.grayscale + theme: VictoryTheme.grayscale, + titleComponent: }; static getBaseProps = partialRight(getBaseProps, fallbackProps); static expectedComponents = [ - "dataComponent", "labelComponent", "groupComponent", "containerComponent" + "borderComponent", "containerComponent", "dataComponent", + "groupComponent", "labelComponent", "titleComponent" ]; + renderChildren(props) { + const { dataComponent, labelComponent, title } = props; + const dataComponents = this.dataKeys.map((_dataKey, index) => { + const dataProps = this.getComponentProps(dataComponent, "data", index); + return React.cloneElement(dataComponent, dataProps); + }); + + const labelComponents = this.dataKeys.map((_dataKey, index) => { + const labelProps = this.getComponentProps(labelComponent, "labels", index); + if (typeof labelProps.text !== "undefined" && labelProps.text !== null) { + return React.cloneElement(labelComponent, labelProps); + } + return undefined; + }).filter(Boolean); + + const borderProps = this.getComponentProps(props.borderComponent, "border", 0); + const borderComponent = React.cloneElement(props.borderComponent, borderProps); + if (title) { + const titleProps = this.getComponentProps(props.title, "title", 0); + const titleComponent = React.cloneElement(props.titleComponent, titleProps); + return [borderComponent, ...dataComponents, titleComponent, ...labelComponents]; + } + return [borderComponent, ...dataComponents, ...labelComponents]; + } + render() { const { role } = this.constructor; const props = Helpers.modifyProps((this.props), fallbackProps, role); - const children = this.renderData(props); - return props.standalone ? this.renderContainer(props.containerComponent, children) : children; + const children = [this.renderChildren(props)]; + return props.standalone ? + this.renderContainer(props.containerComponent, children) : + React.cloneElement(props.groupComponent, {}, children); } } diff --git a/src/victory-primitives/border.js b/src/victory-primitives/border.js new file mode 100644 index 0000000..7742e71 --- /dev/null +++ b/src/victory-primitives/border.js @@ -0,0 +1,61 @@ +import React from "react"; +import PropTypes from "prop-types"; +import Helpers from "../victory-util/helpers"; +import Collection from "../victory-util/collection"; +import { assign } from "lodash"; +import CommonProps from "./common-props"; + +export default class Line extends React.Component { + static propTypes = { + ...CommonProps, + height: PropTypes.number, + width: PropTypes.number, + x: PropTypes.number, + y: PropTypes.number + }; + + componentWillMount() { + this.style = this.getStyle(this.props); + } + + shouldComponentUpdate(nextProps) { + const { className, x, y } = this.props; + const style = this.getStyle(nextProps); + if (!Collection.allSetsEqual([ + [className, nextProps.className], + [x, nextProps.x], + [y, nextProps.y], + [style, this.style] + ])) { + this.style = style; + return true; + } + return false; + } + + getStyle(props) { + const { style, datum, active } = props; + return Helpers.evaluateStyle(assign({ fill: "none" }, style), datum, active); + } + + // Overridden in victory-core-native + renderBorder(props, style, events) { + const { role, shapeRendering, className } = this.props; + return ( + + ); + } + + render() { + const { x, y, width, height, events } = this.props; + return this.renderBorder({ x, y, width, height }, this.style, events); + } +} diff --git a/src/victory-primitives/index.js b/src/victory-primitives/index.js deleted file mode 100644 index cdb713d..0000000 --- a/src/victory-primitives/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import Area from "./area"; -import Arc from "./arc"; -import Bar from "./bar"; -import Candle from "./candle"; -import ClipPath from "./clip-path"; -import Curve from "./curve"; -import ErrorBar from "./error-bar"; -import Line from "./line"; -import Point from "./point"; -import Slice from "./slice"; -import Voronoi from "./voronoi"; -import Flyout from "./flyout"; - - -export { - Arc, Area, Bar, Candle, ClipPath, Curve, ErrorBar, Line, Point, Slice, Voronoi, Flyout -}; diff --git a/src/victory-theme/grayscale.js b/src/victory-theme/grayscale.js index 496f4ee..b6079c5 100644 --- a/src/victory-theme/grayscale.js +++ b/src/victory-theme/grayscale.js @@ -187,12 +187,13 @@ export default { colorScale: colors, gutter: 10, orientation: "vertical", + titleOrientation: "top", style: { data: { type: "circle" }, - labels: baseLabelStyles - }, - symbolSpacer: 8 + labels: baseLabelStyles, + title: assign({}, baseLabelStyles, { padding: 5 }) + } } }; diff --git a/src/victory-theme/material.js b/src/victory-theme/material.js index 53862d9..8c283d4 100644 --- a/src/victory-theme/material.js +++ b/src/victory-theme/material.js @@ -204,12 +204,13 @@ export default { colorScale: colors, gutter: 10, orientation: "vertical", + titleOrientation: "top", style: { data: { type: "circle" }, - labels: baseLabelStyles - }, - symbolSpacer: 8 + labels: baseLabelStyles, + title: assign({}, baseLabelStyles, { padding: 5 }) + } } }; diff --git a/src/victory-util/helpers.js b/src/victory-util/helpers.js index 52b7a37..bbbfcc6 100644 --- a/src/victory-util/helpers.js +++ b/src/victory-util/helpers.js @@ -32,17 +32,21 @@ export default { }; }, - getPadding(props) { - const padding = typeof props.padding === "number" ? props.padding : 0; - const paddingObj = typeof props.padding === "object" ? props.padding : {}; + formatPadding(padding) { + const paddingVal = typeof padding === "number" ? padding : 0; + const paddingObj = typeof padding === "object" ? padding : {}; return { - top: paddingObj.top || padding, - bottom: paddingObj.bottom || padding, - left: paddingObj.left || padding, - right: paddingObj.right || padding + top: paddingObj.top || paddingVal, + bottom: paddingObj.bottom || paddingVal, + left: paddingObj.left || paddingVal, + right: paddingObj.right || paddingVal }; }, + getPadding(props) { + return this.formatPadding(props.padding); + }, + getStyles(style, defaultStyles) { const width = "100%"; const height = "100%"; diff --git a/src/victory-util/textsize.js b/src/victory-util/textsize.js index 3cc5339..162e51f 100644 --- a/src/victory-util/textsize.js +++ b/src/victory-util/textsize.js @@ -84,7 +84,7 @@ const convertLengthToPixels = (length, fontSize) => { if (absoluteMeasurementUnitsToPixels.hasOwnProperty(attribute)) { result = value * absoluteMeasurementUnitsToPixels[attribute]; } else if (relativeMeasurementUnitsCoef.hasOwnProperty(attribute)) { - result = (fontSize ? value * fontSize : value * coefficients.defaultFontSize) + result = (fontSize ? value * fontSize : value * defaultStyle.fontSize) * relativeMeasurementUnitsCoef[attribute]; } else { result = value; @@ -116,7 +116,9 @@ const _approximateTextWidthInternal = (text, style) => { const _approximateTextHeightInternal = (text, style) => { return _splitToLines(text).reduce((total, line, index) => { const lineStyle = _prepareParams(style, index); - const height = lineStyle.fontSize * coefficients.lineCapitalCoef; + const containsCaps = line.match(/[(A-Z)(0-9)]/); + const height = containsCaps ? + lineStyle.fontSize * coefficients.lineCapitalCoef : lineStyle.fontSize; const emptySpace = index === 0 ? 0 : lineStyle.fontSize * coefficients.lineSpaceHeightCoef; return total + lineStyle.lineHeight * (height + emptySpace); }, 0); diff --git a/test/client/spec/victory-legend/victory-legend.spec.js b/test/client/spec/victory-legend/victory-legend.spec.js index 11071c1..e53240b 100644 --- a/test/client/spec/victory-legend/victory-legend.spec.js +++ b/test/client/spec/victory-legend/victory-legend.spec.js @@ -33,40 +33,40 @@ describe("components/victory-legend", () => { const wrappedLegend = shallow(); const output = wrappedLegend.find("Point"); - expect(output.at(0).prop("x")).to.equal(7); - expect(output.at(1).prop("x")).to.equal(95.68016194331983); - expect(output.at(0).prop("y")).to.equal(7); - expect(output.at(1).prop("y")).to.equal(7); + expect(output.at(0).prop("x")).to.equal(14); + expect(output.at(1).prop("x")).to.equal(100.28016194331983); + expect(output.at(0).prop("y")).to.equal(14); + expect(output.at(1).prop("y")).to.equal(14); }); it("has expected vertical symbol position", () => { const wrappedLegend = shallow(); const output = wrappedLegend.find("Point"); - expect(output.at(0).prop("x")).to.equal(7); - expect(output.at(1).prop("x")).to.equal(7); - expect(output.at(0).prop("y")).to.equal(7); - expect(output.at(1).prop("y")).to.equal(31); + expect(output.at(0).prop("x")).to.equal(14); + expect(output.at(1).prop("x")).to.equal(14); + expect(output.at(0).prop("y")).to.equal(14); + expect(output.at(1).prop("y")).to.equal(44.905); }); it("has expected horizontal legend labels position", () => { const wrappedLegend = render(); const output = wrappedLegend.find("text"); - expect(output.eq(0).prop("x")).to.equal("22"); - expect(output.eq(1).prop("x")).to.equal("110.68016194331983"); - expect(output.eq(0).prop("y")).to.equal("7"); - expect(output.eq(1).prop("y")).to.equal("7"); + expect(output.eq(0).prop("x")).to.equal("30.8"); + expect(output.eq(1).prop("x")).to.equal("117.08016194331982"); + expect(output.eq(0).prop("y")).to.equal("14"); + expect(output.eq(1).prop("y")).to.equal("14"); }); it("has expected vertical legend labels position", () => { const wrappedLegend = render(); const output = wrappedLegend.find("text"); - expect(output.eq(0).prop("x")).to.equal("22"); - expect(output.eq(1).prop("x")).to.equal("22"); - expect(output.eq(0).prop("y")).to.equal("7"); - expect(output.eq(1).prop("y")).to.equal("31"); + expect(output.eq(0).prop("x")).to.equal("30.8"); + expect(output.eq(1).prop("x")).to.equal("30.8"); + expect(output.eq(0).prop("y")).to.equal("14"); + expect(output.eq(1).prop("y")).to.equal("44.905"); }); describe("symbols", () => { diff --git a/test/client/spec/victory-util/textsize.spec.js b/test/client/spec/victory-util/textsize.spec.js index 8a69c7a..8d427e3 100644 --- a/test/client/spec/victory-util/textsize.spec.js +++ b/test/client/spec/victory-util/textsize.spec.js @@ -1,6 +1,8 @@ /* eslint no-unused-expressions: 0 */ - import { TextSize } from "src/index"; + +const testString = "ABC"; + describe("helpers/textsize", () => { describe("convertLengthToPixels", () => { it("translate pixels as number of pixels", () => { @@ -16,106 +18,109 @@ describe("helpers/textsize", () => { describe("approximateWidth", () => { it("return zero width when no style", () => { - expect(TextSize.approximateTextSize("abc").width).to.eql(0); + expect(TextSize.approximateTextSize(testString).width).to.eql(0); }); it("return correct width with signed angle", () => { expect( - TextSize.approximateTextSize("abc", { angle: -45, fontSize: 14 }).width.toFixed(2) + TextSize.approximateTextSize(testString, { angle: -45, fontSize: 14 }).width.toFixed(2) ).to.be.eql("31.36"); }); it("return correct width with pixel fontsize", () => { expect( - TextSize.approximateTextSize("abc", { fontSize: "14px" }).width.toFixed(2) + TextSize.approximateTextSize(testString, { fontSize: "14px" }).width.toFixed(2) ).to.be.eql("24.22"); }); it("return appropriate width with defined fontSize", () => { expect( - TextSize.approximateTextSize("abc", { fontSize: 12 }).width.toFixed(2) + TextSize.approximateTextSize(testString, { fontSize: 12 }).width.toFixed(2) ).to.be.eql("20.76"); }); it("consider font", () => { expect( - TextSize.approximateTextSize("abc", { fontSize: 16 }).width.toFixed(2) + TextSize.approximateTextSize(testString, { fontSize: 16 }).width.toFixed(2) ).to.be.eql("27.68"); }); it("consider letterSpacing", () => { expect( - TextSize.approximateTextSize("abc", { fontSize: 12, letterSpacing: "1px" }).width.toFixed(2) + TextSize.approximateTextSize( + testString, + { fontSize: 12, letterSpacing: "1px" } + ).width.toFixed(2) ).to.be.eql("23.26"); }); it("consider angle", () => { expect( - TextSize.approximateTextSize("abc", { fontSize: 12, angle: 30 }).width.toFixed(2) + TextSize.approximateTextSize(testString, { fontSize: 12, angle: 30 }).width.toFixed(2) ).to.be.eql("26.60"); }); it("not consider lineHeight without angle", () => { expect( - TextSize.approximateTextSize("abc", { fontSize: 12, lineHeight: 2 }).width.toFixed(2) + TextSize.approximateTextSize(testString, { fontSize: 12, lineHeight: 2 }).width.toFixed(2) ).to.eql("20.76"); }); it("consider lineHeight with angle", () => { expect( - TextSize.approximateTextSize("abc", { fontSize: 12, lineHeight: 2, angle: 30 } + TextSize.approximateTextSize(testString, { fontSize: 12, lineHeight: 2, angle: 30 } ).width.toFixed(2) ).to.eql("35.23"); }); it("return width of widest string in text", () => { expect( - TextSize.approximateTextSize("abc\ndefgh\nijk", { fontSize: 12 }).width.toFixed(2) + TextSize.approximateTextSize("ABC\nDEFGH\nIJK", { fontSize: 12 }).width.toFixed(2) ).to.eql("34.60"); }); }); describe("approximateHeight", () => { it("return zero width when no style", () => { - expect(TextSize.approximateTextSize("abc").height).to.eql(0); + expect(TextSize.approximateTextSize(testString).height).to.eql(0); }); it("return correct height with signed angle", () => { expect( - TextSize.approximateTextSize("abc", + TextSize.approximateTextSize(testString, { angle: -45, fontSize: 14 }).height.toFixed(2) ).to.be.eql("26.34"); }); it("return correct height with pixel fontsize", () => { expect( - TextSize.approximateTextSize("abc", { fontSize: "14px" }).height.toFixed(2) + TextSize.approximateTextSize(testString, { fontSize: "14px" }).height.toFixed(2) ).to.be.eql("16.90"); }); it("return appropriate height with expected precision", () => { expect( - TextSize.approximateTextSize("abc", { fontSize: 12 }).height.toFixed(2) + TextSize.approximateTextSize(testString, { fontSize: 12 }).height.toFixed(2) ).to.be.eql("14.49"); }); it("consider font", () => { expect( - TextSize.approximateTextSize("abc", { fontSize: 16 }).height.toFixed(2) + TextSize.approximateTextSize(testString, { fontSize: 16 }).height.toFixed(2) ).to.be.eql("19.32"); }); it("consider angle", () => { expect( - TextSize.approximateTextSize("abc", { fontSize: 12, angle: 30 }).height.toFixed(2) + TextSize.approximateTextSize(testString, { fontSize: 12, angle: 30 }).height.toFixed(2) ).to.be.eql("21.27"); }); it("not consider letterSpacing without angle", () => { expect( - TextSize.approximateTextSize("abc", { fontSize: 12, letterSpacing: "1px" }) + TextSize.approximateTextSize(testString, { fontSize: 12, letterSpacing: "1px" }) .height.toFixed(2) ).to.eql("14.49"); }); it("consider letterSpacing with angle", () => { expect( - TextSize.approximateTextSize("abc", { fontSize: 12, angle: 30, letterSpacing: "1px" }) + TextSize.approximateTextSize(testString, { fontSize: 12, angle: 30, letterSpacing: "1px" }) .height.toFixed(2) ).to.be.eql("22.32"); }); it("consider lineHeight", () => { expect( - TextSize.approximateTextSize("abc", { fontSize: 12, lineHeight: 2 }).height.toFixed(2) + TextSize.approximateTextSize(testString, { fontSize: 12, lineHeight: 2 }).height.toFixed(2) ).to.be.eql("28.98"); }); it("consider multiLines text", () => { expect( - TextSize.approximateTextSize(`abc\n${"dbcdefg"}\n123`, { fontSize: 12 }).height.toFixed(2) + TextSize.approximateTextSize(`ABC\n${"DBCDEFG"}\n123`, { fontSize: 12 }).height.toFixed(2) ).to.be.eql("48.51"); }); });