From 45b8a3cd0585abf21cc9aa6a73f31920160a49cc Mon Sep 17 00:00:00 2001 From: saghan Date: Sun, 26 Jun 2022 17:32:50 -0700 Subject: [PATCH 1/6] accessibility fix for letterSpacing --- src/cartesian/CartesianAxis.tsx | 1010 ++++++++++++++++--------------- 1 file changed, 529 insertions(+), 481 deletions(-) diff --git a/src/cartesian/CartesianAxis.tsx b/src/cartesian/CartesianAxis.tsx index f3be476109..5a9232b93c 100644 --- a/src/cartesian/CartesianAxis.tsx +++ b/src/cartesian/CartesianAxis.tsx @@ -1,484 +1,532 @@ /** * @fileOverview Cartesian Axis */ -import React, { ReactElement, ReactNode, Component, SVGProps } from 'react'; -import _ from 'lodash'; -import classNames from 'classnames'; -import { shallowEqual } from '../util/ShallowEqual'; -import { getStringSize } from '../util/DOMUtils'; -import { Layer } from '../container/Layer'; -import { Text } from '../component/Text'; -import { Label } from '../component/Label'; -import { Global } from '../util/Global'; -import { isNumber, mathSign } from '../util/DataUtils'; -import { - CartesianViewBox, - filterProps, - TickItem, - adaptEventsOfChild, - PresentationAttributesAdaptChildEvent, -} from '../util/types'; - -interface CartesianTickItem extends TickItem { - tickCoord?: number; - tickSize?: number; - isShow?: boolean; -} - -export interface CartesianAxisProps { - className?: string; - x?: number; - y?: number; - width?: number; - height?: number; - unit?: string | number; - orientation?: 'top' | 'bottom' | 'left' | 'right'; - // The viewBox of svg - viewBox?: CartesianViewBox; - tick?: SVGProps | ReactElement | ((props: any) => ReactElement) | boolean; - axisLine?: boolean | SVGProps; - tickLine?: boolean | SVGProps; - mirror?: boolean; - tickMargin?: number; - hide?: boolean; - label?: any; - - minTickGap?: number; - ticks?: CartesianTickItem[]; - tickSize?: number; - /** The formatter function of tick */ - tickFormatter?: (value: any, index: number) => string; - ticksGenerator?: (props?: CartesianAxisProps) => CartesianTickItem[]; - interval?: number | 'preserveStart' | 'preserveEnd' | 'preserveStartEnd'; -} - -export type Props = Omit, 'viewBox'> & CartesianAxisProps; - -export class CartesianAxis extends Component { - static displayName = 'CartesianAxis'; - - static defaultProps = { - x: 0, - y: 0, - width: 0, - height: 0, - viewBox: { x: 0, y: 0, width: 0, height: 0 }, - // The orientation of axis - orientation: 'bottom', - // The ticks - ticks: [] as CartesianAxisProps['ticks'], - - stroke: '#666', - tickLine: true, - axisLine: true, - tick: true, - mirror: false, - - minTickGap: 5, - // The width or height of tick - tickSize: 6, - tickMargin: 2, - interval: 'preserveEnd', - }; - - // todo Array - static getTicks(props: Props): any[] { - const { tick, ticks, viewBox, minTickGap, orientation, interval, tickFormatter, unit } = props; - - if (!ticks || !ticks.length || !tick) { - return []; - } - - if (isNumber(interval) || Global.isSsr) { - return CartesianAxis.getNumberIntervalTicks( - ticks, - typeof interval === 'number' && isNumber(interval) ? interval : 0, - ); - } - - if (interval === 'preserveStartEnd') { - return CartesianAxis.getTicksStart( - { - ticks, - tickFormatter, - viewBox, - orientation, - minTickGap, - unit, - }, - true, - ); - } - if (interval === 'preserveStart') { - return CartesianAxis.getTicksStart({ - ticks, - tickFormatter, - viewBox, - orientation, - minTickGap, - unit, - }); - } - - return CartesianAxis.getTicksEnd({ - ticks, - tickFormatter, - viewBox, - orientation, - minTickGap, - unit, - }); - } - - static getNumberIntervalTicks(ticks: CartesianTickItem[], interval: number) { - return ticks.filter((entry, i) => i % (interval + 1) === 0); - } - - static getTicksStart( - { ticks, tickFormatter, viewBox, orientation, minTickGap, unit }: Omit, - preserveEnd?: boolean, - ) { - const { x, y, width, height } = viewBox; - const sizeKey = orientation === 'top' || orientation === 'bottom' ? 'width' : 'height'; - const result = (ticks || []).slice(); - // we need add the width of 'unit' only when sizeKey === 'width' - const unitSize = unit && sizeKey === 'width' ? getStringSize(unit)[sizeKey] : 0; - const len = result.length; - const sign = len >= 2 ? mathSign(result[1].coordinate - result[0].coordinate) : 1; - - let start, end; - - if (sign === 1) { - start = sizeKey === 'width' ? x : y; - end = sizeKey === 'width' ? x + width : y + height; - } else { - start = sizeKey === 'width' ? x + width : y + height; - end = sizeKey === 'width' ? x : y; - } - - if (preserveEnd) { - // Try to guarantee the tail to be displayed - let tail = ticks[len - 1]; - const tailContent = _.isFunction(tickFormatter) ? tickFormatter(tail.value, len - 1) : tail.value; - const tailSize = getStringSize(tailContent)[sizeKey] + unitSize; - const tailGap = sign * (tail.coordinate + (sign * tailSize) / 2 - end); - result[len - 1] = tail = { - ...tail, - tickCoord: tailGap > 0 ? tail.coordinate - tailGap * sign : tail.coordinate, - }; - - const isTailShow = - sign * (tail.tickCoord - (sign * tailSize) / 2 - start) >= 0 && - sign * (tail.tickCoord + (sign * tailSize) / 2 - end) <= 0; - - if (isTailShow) { - end = tail.tickCoord - sign * (tailSize / 2 + minTickGap); - result[len - 1] = { ...tail, isShow: true }; - } - } - - const count = preserveEnd ? len - 1 : len; - for (let i = 0; i < count; i++) { - let entry = result[i]; - const content = _.isFunction(tickFormatter) ? tickFormatter(entry.value, i) : entry.value; - const size = getStringSize(content)[sizeKey] + unitSize; - - if (i === 0) { - const gap = sign * (entry.coordinate - (sign * size) / 2 - start); - result[i] = entry = { - ...entry, - tickCoord: gap < 0 ? entry.coordinate - gap * sign : entry.coordinate, - }; - } else { - result[i] = entry = { ...entry, tickCoord: entry.coordinate }; - } - - const isShow = - sign * (entry.tickCoord - (sign * size) / 2 - start) >= 0 && - sign * (entry.tickCoord + (sign * size) / 2 - end) <= 0; - - if (isShow) { - start = entry.tickCoord + sign * (size / 2 + minTickGap); - result[i] = { ...entry, isShow: true }; - } - } - - return result.filter(entry => entry.isShow); - } - - static getTicksEnd({ ticks, tickFormatter, viewBox, orientation, minTickGap, unit }: Omit) { - const { x, y, width, height } = viewBox; - const sizeKey = orientation === 'top' || orientation === 'bottom' ? 'width' : 'height'; - // we need add the width of 'unit' only when sizeKey === 'width' - const unitSize = unit && sizeKey === 'width' ? getStringSize(unit)[sizeKey] : 0; - const result = (ticks || []).slice(); - const len = result.length; - const sign = len >= 2 ? mathSign(result[1].coordinate - result[0].coordinate) : 1; - - let start, end; - - if (sign === 1) { - start = sizeKey === 'width' ? x : y; - end = sizeKey === 'width' ? x + width : y + height; - } else { - start = sizeKey === 'width' ? x + width : y + height; - end = sizeKey === 'width' ? x : y; - } - - for (let i = len - 1; i >= 0; i--) { - let entry = result[i]; - const content = _.isFunction(tickFormatter) ? tickFormatter(entry.value, len - i - 1) : entry.value; - const size = getStringSize(content)[sizeKey] + unitSize; - - if (i === len - 1) { - const gap = sign * (entry.coordinate + (sign * size) / 2 - end); - result[i] = entry = { - ...entry, - tickCoord: gap > 0 ? entry.coordinate - gap * sign : entry.coordinate, - }; - } else { - result[i] = entry = { ...entry, tickCoord: entry.coordinate }; - } - - const isShow = - sign * (entry.tickCoord - (sign * size) / 2 - start) >= 0 && - sign * (entry.tickCoord + (sign * size) / 2 - end) <= 0; - - if (isShow) { - end = entry.tickCoord - sign * (size / 2 + minTickGap); - result[i] = { ...entry, isShow: true }; - } - } - - return result.filter(entry => entry.isShow); - } - - shouldComponentUpdate({ viewBox, ...restProps }: Props) { - // props.viewBox is sometimes generated every time - - // check that specially as object equality is likely to fail - const { viewBox: viewBoxOld, ...restPropsOld } = this.props; - return !shallowEqual(viewBox, viewBoxOld) || !shallowEqual(restProps, restPropsOld); - } - - /** - * Calculate the coordinates of endpoints in ticks - * @param {Object} data The data of a simple tick - * @return {Object} (x1, y1): The coordinate of endpoint close to tick text - * (x2, y2): The coordinate of endpoint close to axis - */ - getTickLineCoord(data: CartesianTickItem) { - const { x, y, width, height, orientation, tickSize, mirror, tickMargin } = this.props; - let x1, x2, y1, y2, tx, ty; - - const sign = mirror ? -1 : 1; - const finalTickSize = data.tickSize || tickSize; - const tickCoord = isNumber(data.tickCoord) ? data.tickCoord : data.coordinate; - - switch (orientation) { - case 'top': - x1 = x2 = data.coordinate; - y2 = y + +!mirror * height; - y1 = y2 - sign * finalTickSize; - ty = y1 - sign * tickMargin; - tx = tickCoord; - break; - case 'left': - y1 = y2 = data.coordinate; - x2 = x + +!mirror * width; - x1 = x2 - sign * finalTickSize; - tx = x1 - sign * tickMargin; - ty = tickCoord; - break; - case 'right': - y1 = y2 = data.coordinate; - x2 = x + +mirror * width; - x1 = x2 + sign * finalTickSize; - tx = x1 + sign * tickMargin; - ty = tickCoord; - break; - default: - x1 = x2 = data.coordinate; - y2 = y + +mirror * height; - y1 = y2 + sign * finalTickSize; - ty = y1 + sign * tickMargin; - tx = tickCoord; - break; - } - - return { line: { x1, y1, x2, y2 }, tick: { x: tx, y: ty } }; - } - - getTickTextAnchor() { - const { orientation, mirror } = this.props; - let textAnchor; - - switch (orientation) { - case 'left': - textAnchor = mirror ? 'start' : 'end'; - break; - case 'right': - textAnchor = mirror ? 'end' : 'start'; - break; - default: - textAnchor = 'middle'; - break; - } - - return textAnchor; - } - - getTickVerticalAnchor() { - const { orientation, mirror } = this.props; - let verticalAnchor = 'end'; - - switch (orientation) { - case 'left': - case 'right': - verticalAnchor = 'middle'; - break; - case 'top': - verticalAnchor = mirror ? 'start' : 'end'; - break; - default: - verticalAnchor = mirror ? 'end' : 'start'; - break; - } - - return verticalAnchor; - } - - renderAxisLine() { - const { x, y, width, height, orientation, mirror, axisLine } = this.props; - let props: SVGProps = { - ...filterProps(this.props), - ...filterProps(axisLine), - fill: 'none', - }; - - if (orientation === 'top' || orientation === 'bottom') { - const needHeight = +((orientation === 'top' && !mirror) || (orientation === 'bottom' && mirror)); - props = { - ...props, - x1: x, - y1: y + needHeight * height, - x2: x + width, - y2: y + needHeight * height, - }; - } else { - const needWidth = +((orientation === 'left' && !mirror) || (orientation === 'right' && mirror)); - props = { - ...props, - x1: x + needWidth * width, - y1: y, - x2: x + needWidth * width, - y2: y + height, - }; - } - - return ; - } - - static renderTickItem(option: Props['tick'], props: any, value: ReactNode) { - let tickItem; - - if (React.isValidElement(option)) { - tickItem = React.cloneElement(option, props); - } else if (_.isFunction(option)) { - tickItem = option(props); - } else { - tickItem = ( - - {value} - - ); - } - - return tickItem; - } - - /** - * render the ticks - * @param {Array} ticks The ticks to actually render (overrides what was passed in props) - * @return {ReactComponent} renderedTicks - */ - renderTicks(ticks: CartesianTickItem[]) { - const { tickLine, stroke, tick, tickFormatter, unit } = this.props; - const finalTicks = CartesianAxis.getTicks({ ...this.props, ticks }); - const textAnchor = this.getTickTextAnchor(); - const verticalAnchor = this.getTickVerticalAnchor(); - const axisProps = filterProps(this.props); - const customTickProps = filterProps(tick); - const tickLineProps = { - ...axisProps, - fill: 'none', - ...filterProps(tickLine), - }; - const items = finalTicks.map((entry, i) => { - const { line: lineCoord, tick: tickCoord } = this.getTickLineCoord(entry); - const tickProps = { - textAnchor, - verticalAnchor, - ...axisProps, - stroke: 'none', - fill: stroke, - ...customTickProps, - ...tickCoord, - index: i, - payload: entry, - visibleTicksCount: finalTicks.length, - tickFormatter, - }; - - return ( - - {tickLine && ( - - )} - {tick && - CartesianAxis.renderTickItem( - tick, - tickProps, - `${_.isFunction(tickFormatter) ? tickFormatter(entry.value, i) : entry.value}${unit || ''}`, - )} - - ); - }); - - return {items}; - } - - render() { - const { axisLine, width, height, ticksGenerator, className, hide } = this.props; - - if (hide) { - return null; - } - - const { ticks, ...noTicksProps } = this.props; - let finalTicks = ticks; - - if (_.isFunction(ticksGenerator)) { - finalTicks = ticks && ticks.length > 0 ? ticksGenerator(this.props) : ticksGenerator(noTicksProps); - } - - if (width <= 0 || height <= 0 || !finalTicks || !finalTicks.length) { - return null; - } - - return ( - - {axisLine && this.renderAxisLine()} - {this.renderTicks(finalTicks)} - {Label.renderCallByParent(this.props)} - - ); - } -} + import React, { ReactElement, ReactNode, Component, SVGProps } from 'react'; + import _ from 'lodash'; + import classNames from 'classnames'; + import { shallowEqual } from '../util/ShallowEqual'; + import { getStringSize } from '../util/DOMUtils'; + import { Layer } from '../container/Layer'; + import { Text } from '../component/Text'; + import { Label } from '../component/Label'; + import { Global } from '../util/Global'; + import { isNumber, mathSign } from '../util/DataUtils'; + import { + CartesianViewBox, + filterProps, + TickItem, + adaptEventsOfChild, + PresentationAttributesAdaptChildEvent, + } from '../util/types'; + + interface CartesianTickItem extends TickItem { + tickCoord?: number; + tickSize?: number; + isShow?: boolean; + } + + export interface CartesianAxisProps { + className?: string; + x?: number; + y?: number; + width?: number; + height?: number; + unit?: string | number; + orientation?: 'top' | 'bottom' | 'left' | 'right'; + // The viewBox of svg + viewBox?: CartesianViewBox; + tick?: SVGProps | ReactElement | ((props: any) => ReactElement) | boolean; + axisLine?: boolean | SVGProps; + tickLine?: boolean | SVGProps; + mirror?: boolean; + tickMargin?: number; + hide?: boolean; + label?: any; + + minTickGap?: number; + ticks?: CartesianTickItem[]; + tickSize?: number; + /** The formatter function of tick */ + tickFormatter?: (value: any, index: number) => string; + ticksGenerator?: (props?: CartesianAxisProps) => CartesianTickItem[]; + interval?: number | 'preserveStart' | 'preserveEnd' | 'preserveStartEnd'; + } + + export interface IState { + fontSize: string; + letterSpacing: string; + } + export type Props = Omit, 'viewBox'> & CartesianAxisProps; + + export class CartesianAxis extends Component { + static displayName = 'CartesianAxis'; + + static defaultProps = { + x: 0, + y: 0, + width: 0, + height: 0, + viewBox: { x: 0, y: 0, width: 0, height: 0 }, + // The orientation of axis + orientation: 'bottom', + // The ticks + ticks: [] as CartesianAxisProps['ticks'], + + stroke: '#666', + tickLine: true, + axisLine: true, + tick: true, + mirror: false, + + minTickGap: 5, + // The width or height of tick + tickSize: 6, + tickMargin: 2, + interval: 'preserveEnd', + }; + + constructor(props: Props) { + super(props); + this.state = { fontSize: '14px', letterSpacing: '0px' }; + } + + // todo Array + static getTicks(props: Props, fontSize: string, letterSpacing: string): any[] { + const { tick, ticks, viewBox, minTickGap, orientation, interval, tickFormatter, unit } = props; + + if (!ticks || !ticks.length || !tick) { + return []; + } + + if (isNumber(interval) || Global.isSsr) { + return CartesianAxis.getNumberIntervalTicks( + ticks, + typeof interval === 'number' && isNumber(interval) ? interval : 0, + ); + } + + if (interval === 'preserveStartEnd') { + return CartesianAxis.getTicksStart( + { + ticks, + tickFormatter, + viewBox, + orientation, + minTickGap, + unit, + fontSize, + letterSpacing, + }, + true, + ); + } + if (interval === 'preserveStart') { + return CartesianAxis.getTicksStart({ + ticks, + tickFormatter, + viewBox, + orientation, + minTickGap, + unit, + fontSize, + letterSpacing, + }); + } + + return CartesianAxis.getTicksEnd({ + ticks, + tickFormatter, + viewBox, + orientation, + minTickGap, + unit, + fontSize, + letterSpacing, + }); + } + + static getNumberIntervalTicks(ticks: CartesianTickItem[], interval: number) { + return ticks.filter((entry, i) => i % (interval + 1) === 0); + } + + static getTicksStart( + { + ticks, + tickFormatter, + viewBox, + orientation, + minTickGap, + unit, + fontSize, + letterSpacing, + }: Omit, + preserveEnd?: boolean, + ) { + const { x, y, width, height } = viewBox; + const sizeKey = orientation === 'top' || orientation === 'bottom' ? 'width' : 'height'; + const result = (ticks || []).slice(); + // we need add the width of 'unit' only when sizeKey === 'width' + const unitSize = unit && sizeKey === 'width' ? getStringSize(unit, { fontSize, letterSpacing })[sizeKey] : 0; + const len = result.length; + const sign = len >= 2 ? mathSign(result[1].coordinate - result[0].coordinate) : 1; + + let start, end; + + if (sign === 1) { + start = sizeKey === 'width' ? x : y; + end = sizeKey === 'width' ? x + width : y + height; + } else { + start = sizeKey === 'width' ? x + width : y + height; + end = sizeKey === 'width' ? x : y; + } + + if (preserveEnd) { + // Try to guarantee the tail to be displayed + let tail = ticks[len - 1]; + const tailContent = _.isFunction(tickFormatter) ? tickFormatter(tail.value, len - 1) : tail.value; + const tailSize = getStringSize(tailContent, { fontSize, letterSpacing })[sizeKey] + unitSize; + const tailGap = sign * (tail.coordinate + (sign * tailSize) / 2 - end); + result[len - 1] = tail = { + ...tail, + tickCoord: tailGap > 0 ? tail.coordinate - tailGap * sign : tail.coordinate, + }; + + const isTailShow = + sign * (tail.tickCoord - (sign * tailSize) / 2 - start) >= 0 && + sign * (tail.tickCoord + (sign * tailSize) / 2 - end) <= 0; + + if (isTailShow) { + end = tail.tickCoord - sign * (tailSize / 2 + minTickGap); + result[len - 1] = { ...tail, isShow: true }; + } + } + + const count = preserveEnd ? len - 1 : len; + for (let i = 0; i < count; i++) { + let entry = result[i]; + const content = _.isFunction(tickFormatter) ? tickFormatter(entry.value, i) : entry.value; + const size = getStringSize(content, { fontSize, letterSpacing })[sizeKey] + unitSize; + + if (i === 0) { + const gap = sign * (entry.coordinate - (sign * size) / 2 - start); + result[i] = entry = { + ...entry, + tickCoord: gap < 0 ? entry.coordinate - gap * sign : entry.coordinate, + }; + } else { + result[i] = entry = { ...entry, tickCoord: entry.coordinate }; + } + + const isShow = + sign * (entry.tickCoord - (sign * size) / 2 - start) >= 0 && + sign * (entry.tickCoord + (sign * size) / 2 - end) <= 0; + + if (isShow) { + start = entry.tickCoord + sign * (size / 2 + minTickGap); + result[i] = { ...entry, isShow: true }; + } + } + + return result.filter(entry => entry.isShow); + } + + static getTicksEnd({ + ticks, + tickFormatter, + viewBox, + orientation, + minTickGap, + unit, + fontSize, + letterSpacing, + }: Omit) { + const { x, y, width, height } = viewBox; + const sizeKey = orientation === 'top' || orientation === 'bottom' ? 'width' : 'height'; + // we need add the width of 'unit' only when sizeKey === 'width' + const unitSize = unit && sizeKey === 'width' ? getStringSize(unit, { fontSize, letterSpacing })[sizeKey] : 0; + const result = (ticks || []).slice(); + const len = result.length; + const sign = len >= 2 ? mathSign(result[1].coordinate - result[0].coordinate) : 1; + + let start, end; + + if (sign === 1) { + start = sizeKey === 'width' ? x : y; + end = sizeKey === 'width' ? x + width : y + height; + } else { + start = sizeKey === 'width' ? x + width : y + height; + end = sizeKey === 'width' ? x : y; + } + + for (let i = len - 1; i >= 0; i--) { + let entry = result[i]; + const content = _.isFunction(tickFormatter) ? tickFormatter(entry.value, len - i - 1) : entry.value; + const size = getStringSize(content, { fontSize, letterSpacing })[sizeKey] + unitSize; + + if (i === len - 1) { + const gap = sign * (entry.coordinate + (sign * size) / 2 - end); + result[i] = entry = { + ...entry, + tickCoord: gap > 0 ? entry.coordinate - gap * sign : entry.coordinate, + }; + } else { + result[i] = entry = { ...entry, tickCoord: entry.coordinate }; + } + + const isShow = + sign * (entry.tickCoord - (sign * size) / 2 - start) >= 0 && + sign * (entry.tickCoord + (sign * size) / 2 - end) <= 0; + + if (isShow) { + end = entry.tickCoord - sign * (size / 2 + minTickGap); + result[i] = { ...entry, isShow: true }; + } + } + + return result.filter(entry => entry.isShow); + } + + shouldComponentUpdate({ viewBox, ...restProps }: Props, nextState: IState) { + // props.viewBox is sometimes generated every time - + // check that specially as object equality is likely to fail + const { viewBox: viewBoxOld, ...restPropsOld } = this.props; + return ( + !shallowEqual(viewBox, viewBoxOld) || + !shallowEqual(restProps, restPropsOld) || + !shallowEqual(nextState, this.state) + ); + } + + componentDidMount() { + const tick: Element = document.getElementsByClassName('recharts-cartesian-axis-tick-value')[0]; + if (tick) { + this.setState({ + fontSize: window.getComputedStyle(tick).fontSize, + letterSpacing: window.getComputedStyle(tick).letterSpacing, + }); + } + } + + /** + * Calculate the coordinates of endpoints in ticks + * @param {Object} data The data of a simple tick + * @return {Object} (x1, y1): The coordinate of endpoint close to tick text + * (x2, y2): The coordinate of endpoint close to axis + */ + getTickLineCoord(data: CartesianTickItem) { + const { x, y, width, height, orientation, tickSize, mirror, tickMargin } = this.props; + let x1, x2, y1, y2, tx, ty; + + const sign = mirror ? -1 : 1; + const finalTickSize = data.tickSize || tickSize; + const tickCoord = isNumber(data.tickCoord) ? data.tickCoord : data.coordinate; + + switch (orientation) { + case 'top': + x1 = x2 = data.coordinate; + y2 = y + +!mirror * height; + y1 = y2 - sign * finalTickSize; + ty = y1 - sign * tickMargin; + tx = tickCoord; + break; + case 'left': + y1 = y2 = data.coordinate; + x2 = x + +!mirror * width; + x1 = x2 - sign * finalTickSize; + tx = x1 - sign * tickMargin; + ty = tickCoord; + break; + case 'right': + y1 = y2 = data.coordinate; + x2 = x + +mirror * width; + x1 = x2 + sign * finalTickSize; + tx = x1 + sign * tickMargin; + ty = tickCoord; + break; + default: + x1 = x2 = data.coordinate; + y2 = y + +mirror * height; + y1 = y2 + sign * finalTickSize; + ty = y1 + sign * tickMargin; + tx = tickCoord; + break; + } + + return { line: { x1, y1, x2, y2 }, tick: { x: tx, y: ty } }; + } + + getTickTextAnchor() { + const { orientation, mirror } = this.props; + let textAnchor; + + switch (orientation) { + case 'left': + textAnchor = mirror ? 'start' : 'end'; + break; + case 'right': + textAnchor = mirror ? 'end' : 'start'; + break; + default: + textAnchor = 'middle'; + break; + } + + return textAnchor; + } + + getTickVerticalAnchor() { + const { orientation, mirror } = this.props; + let verticalAnchor = 'end'; + + switch (orientation) { + case 'left': + case 'right': + verticalAnchor = 'middle'; + break; + case 'top': + verticalAnchor = mirror ? 'start' : 'end'; + break; + default: + verticalAnchor = mirror ? 'end' : 'start'; + break; + } + + return verticalAnchor; + } + + renderAxisLine() { + const { x, y, width, height, orientation, mirror, axisLine } = this.props; + let props: SVGProps = { + ...filterProps(this.props), + ...filterProps(axisLine), + fill: 'none', + }; + + if (orientation === 'top' || orientation === 'bottom') { + const needHeight = +((orientation === 'top' && !mirror) || (orientation === 'bottom' && mirror)); + props = { + ...props, + x1: x, + y1: y + needHeight * height, + x2: x + width, + y2: y + needHeight * height, + }; + } else { + const needWidth = +((orientation === 'left' && !mirror) || (orientation === 'right' && mirror)); + props = { + ...props, + x1: x + needWidth * width, + y1: y, + x2: x + needWidth * width, + y2: y + height, + }; + } + + return ; + } + + static renderTickItem(option: Props['tick'], props: any, value: ReactNode) { + let tickItem; + + if (React.isValidElement(option)) { + tickItem = React.cloneElement(option, props); + } else if (_.isFunction(option)) { + tickItem = option(props); + } else { + tickItem = ( + + {value} + + ); + } + + return tickItem; + } + + /** + * render the ticks + * @param {Array} ticks The ticks to actually render (overrides what was passed in props) + * @return {ReactComponent} renderedTicks + */ + renderTicks(ticks: CartesianTickItem[], fontSize: string, letterSpacing: string) { + const { tickLine, stroke, tick, tickFormatter, unit } = this.props; + const finalTicks = CartesianAxis.getTicks({ ...this.props, ticks }, fontSize, letterSpacing); + const textAnchor = this.getTickTextAnchor(); + const verticalAnchor = this.getTickVerticalAnchor(); + const axisProps = filterProps(this.props); + const customTickProps = filterProps(tick); + const tickLineProps = { + ...axisProps, + fill: 'none', + ...filterProps(tickLine), + }; + const items = finalTicks.map((entry, i) => { + const { line: lineCoord, tick: tickCoord } = this.getTickLineCoord(entry); + const tickProps = { + textAnchor, + verticalAnchor, + ...axisProps, + stroke: 'none', + fill: stroke, + ...customTickProps, + ...tickCoord, + index: i, + payload: entry, + visibleTicksCount: finalTicks.length, + tickFormatter, + }; + + return ( + + {tickLine && ( + + )} + {tick && + CartesianAxis.renderTickItem( + tick, + tickProps, + `${_.isFunction(tickFormatter) ? tickFormatter(entry.value, i) : entry.value}${unit || ''}`, + )} + + ); + }); + + return {items}; + } + + render() { + const { axisLine, width, height, ticksGenerator, className, hide } = this.props; + + if (hide) { + return null; + } + + const { ticks, ...noTicksProps } = this.props; + let finalTicks = ticks; + + if (_.isFunction(ticksGenerator)) { + finalTicks = ticks && ticks.length > 0 ? ticksGenerator(this.props) : ticksGenerator(noTicksProps); + } + + if (width <= 0 || height <= 0 || !finalTicks || !finalTicks.length) { + return null; + } + + return ( + + {axisLine && this.renderAxisLine()} + {this.renderTicks(finalTicks, this.state.fontSize, this.state.letterSpacing)} + {Label.renderCallByParent(this.props)} + + ); + } + } + \ No newline at end of file From dc772a4b5ebfd20a096f73c20f2e38a7889d1075 Mon Sep 17 00:00:00 2001 From: saghan Date: Tue, 28 Jun 2022 22:12:25 -0700 Subject: [PATCH 2/6] fixed line ending --- src/cartesian/CartesianAxis.tsx | 1057 +++++++++++++++---------------- 1 file changed, 528 insertions(+), 529 deletions(-) diff --git a/src/cartesian/CartesianAxis.tsx b/src/cartesian/CartesianAxis.tsx index 5a9232b93c..2a4ae711da 100644 --- a/src/cartesian/CartesianAxis.tsx +++ b/src/cartesian/CartesianAxis.tsx @@ -1,532 +1,531 @@ /** * @fileOverview Cartesian Axis */ - import React, { ReactElement, ReactNode, Component, SVGProps } from 'react'; - import _ from 'lodash'; - import classNames from 'classnames'; - import { shallowEqual } from '../util/ShallowEqual'; - import { getStringSize } from '../util/DOMUtils'; - import { Layer } from '../container/Layer'; - import { Text } from '../component/Text'; - import { Label } from '../component/Label'; - import { Global } from '../util/Global'; - import { isNumber, mathSign } from '../util/DataUtils'; - import { - CartesianViewBox, - filterProps, - TickItem, - adaptEventsOfChild, - PresentationAttributesAdaptChildEvent, - } from '../util/types'; - - interface CartesianTickItem extends TickItem { - tickCoord?: number; - tickSize?: number; - isShow?: boolean; - } - - export interface CartesianAxisProps { - className?: string; - x?: number; - y?: number; - width?: number; - height?: number; - unit?: string | number; - orientation?: 'top' | 'bottom' | 'left' | 'right'; - // The viewBox of svg - viewBox?: CartesianViewBox; - tick?: SVGProps | ReactElement | ((props: any) => ReactElement) | boolean; - axisLine?: boolean | SVGProps; - tickLine?: boolean | SVGProps; - mirror?: boolean; - tickMargin?: number; - hide?: boolean; - label?: any; - - minTickGap?: number; - ticks?: CartesianTickItem[]; - tickSize?: number; - /** The formatter function of tick */ - tickFormatter?: (value: any, index: number) => string; - ticksGenerator?: (props?: CartesianAxisProps) => CartesianTickItem[]; - interval?: number | 'preserveStart' | 'preserveEnd' | 'preserveStartEnd'; - } - - export interface IState { - fontSize: string; - letterSpacing: string; - } - export type Props = Omit, 'viewBox'> & CartesianAxisProps; - - export class CartesianAxis extends Component { - static displayName = 'CartesianAxis'; - - static defaultProps = { - x: 0, - y: 0, - width: 0, - height: 0, - viewBox: { x: 0, y: 0, width: 0, height: 0 }, - // The orientation of axis - orientation: 'bottom', - // The ticks - ticks: [] as CartesianAxisProps['ticks'], - - stroke: '#666', - tickLine: true, - axisLine: true, - tick: true, - mirror: false, - - minTickGap: 5, - // The width or height of tick - tickSize: 6, - tickMargin: 2, - interval: 'preserveEnd', - }; - - constructor(props: Props) { - super(props); - this.state = { fontSize: '14px', letterSpacing: '0px' }; - } - - // todo Array - static getTicks(props: Props, fontSize: string, letterSpacing: string): any[] { - const { tick, ticks, viewBox, minTickGap, orientation, interval, tickFormatter, unit } = props; - - if (!ticks || !ticks.length || !tick) { - return []; - } - - if (isNumber(interval) || Global.isSsr) { - return CartesianAxis.getNumberIntervalTicks( - ticks, - typeof interval === 'number' && isNumber(interval) ? interval : 0, - ); - } - - if (interval === 'preserveStartEnd') { - return CartesianAxis.getTicksStart( - { - ticks, - tickFormatter, - viewBox, - orientation, - minTickGap, - unit, - fontSize, - letterSpacing, - }, - true, - ); - } - if (interval === 'preserveStart') { - return CartesianAxis.getTicksStart({ - ticks, - tickFormatter, - viewBox, - orientation, - minTickGap, - unit, - fontSize, - letterSpacing, - }); - } - - return CartesianAxis.getTicksEnd({ - ticks, - tickFormatter, - viewBox, - orientation, - minTickGap, - unit, - fontSize, - letterSpacing, - }); - } - - static getNumberIntervalTicks(ticks: CartesianTickItem[], interval: number) { - return ticks.filter((entry, i) => i % (interval + 1) === 0); - } - - static getTicksStart( - { - ticks, - tickFormatter, - viewBox, - orientation, - minTickGap, - unit, - fontSize, - letterSpacing, - }: Omit, - preserveEnd?: boolean, - ) { - const { x, y, width, height } = viewBox; - const sizeKey = orientation === 'top' || orientation === 'bottom' ? 'width' : 'height'; - const result = (ticks || []).slice(); - // we need add the width of 'unit' only when sizeKey === 'width' - const unitSize = unit && sizeKey === 'width' ? getStringSize(unit, { fontSize, letterSpacing })[sizeKey] : 0; - const len = result.length; - const sign = len >= 2 ? mathSign(result[1].coordinate - result[0].coordinate) : 1; - - let start, end; - - if (sign === 1) { - start = sizeKey === 'width' ? x : y; - end = sizeKey === 'width' ? x + width : y + height; - } else { - start = sizeKey === 'width' ? x + width : y + height; - end = sizeKey === 'width' ? x : y; - } - - if (preserveEnd) { - // Try to guarantee the tail to be displayed - let tail = ticks[len - 1]; - const tailContent = _.isFunction(tickFormatter) ? tickFormatter(tail.value, len - 1) : tail.value; - const tailSize = getStringSize(tailContent, { fontSize, letterSpacing })[sizeKey] + unitSize; - const tailGap = sign * (tail.coordinate + (sign * tailSize) / 2 - end); - result[len - 1] = tail = { - ...tail, - tickCoord: tailGap > 0 ? tail.coordinate - tailGap * sign : tail.coordinate, - }; - - const isTailShow = - sign * (tail.tickCoord - (sign * tailSize) / 2 - start) >= 0 && - sign * (tail.tickCoord + (sign * tailSize) / 2 - end) <= 0; - - if (isTailShow) { - end = tail.tickCoord - sign * (tailSize / 2 + minTickGap); - result[len - 1] = { ...tail, isShow: true }; - } - } - - const count = preserveEnd ? len - 1 : len; - for (let i = 0; i < count; i++) { - let entry = result[i]; - const content = _.isFunction(tickFormatter) ? tickFormatter(entry.value, i) : entry.value; - const size = getStringSize(content, { fontSize, letterSpacing })[sizeKey] + unitSize; - - if (i === 0) { - const gap = sign * (entry.coordinate - (sign * size) / 2 - start); - result[i] = entry = { - ...entry, - tickCoord: gap < 0 ? entry.coordinate - gap * sign : entry.coordinate, - }; - } else { - result[i] = entry = { ...entry, tickCoord: entry.coordinate }; - } - - const isShow = - sign * (entry.tickCoord - (sign * size) / 2 - start) >= 0 && - sign * (entry.tickCoord + (sign * size) / 2 - end) <= 0; - - if (isShow) { - start = entry.tickCoord + sign * (size / 2 + minTickGap); - result[i] = { ...entry, isShow: true }; - } - } - - return result.filter(entry => entry.isShow); - } - - static getTicksEnd({ - ticks, - tickFormatter, - viewBox, - orientation, - minTickGap, - unit, - fontSize, - letterSpacing, - }: Omit) { - const { x, y, width, height } = viewBox; - const sizeKey = orientation === 'top' || orientation === 'bottom' ? 'width' : 'height'; - // we need add the width of 'unit' only when sizeKey === 'width' - const unitSize = unit && sizeKey === 'width' ? getStringSize(unit, { fontSize, letterSpacing })[sizeKey] : 0; - const result = (ticks || []).slice(); - const len = result.length; - const sign = len >= 2 ? mathSign(result[1].coordinate - result[0].coordinate) : 1; - - let start, end; - - if (sign === 1) { - start = sizeKey === 'width' ? x : y; - end = sizeKey === 'width' ? x + width : y + height; - } else { - start = sizeKey === 'width' ? x + width : y + height; - end = sizeKey === 'width' ? x : y; - } - - for (let i = len - 1; i >= 0; i--) { - let entry = result[i]; - const content = _.isFunction(tickFormatter) ? tickFormatter(entry.value, len - i - 1) : entry.value; - const size = getStringSize(content, { fontSize, letterSpacing })[sizeKey] + unitSize; - - if (i === len - 1) { - const gap = sign * (entry.coordinate + (sign * size) / 2 - end); - result[i] = entry = { - ...entry, - tickCoord: gap > 0 ? entry.coordinate - gap * sign : entry.coordinate, - }; - } else { - result[i] = entry = { ...entry, tickCoord: entry.coordinate }; - } - - const isShow = - sign * (entry.tickCoord - (sign * size) / 2 - start) >= 0 && - sign * (entry.tickCoord + (sign * size) / 2 - end) <= 0; - - if (isShow) { - end = entry.tickCoord - sign * (size / 2 + minTickGap); - result[i] = { ...entry, isShow: true }; - } - } - - return result.filter(entry => entry.isShow); - } - - shouldComponentUpdate({ viewBox, ...restProps }: Props, nextState: IState) { - // props.viewBox is sometimes generated every time - - // check that specially as object equality is likely to fail - const { viewBox: viewBoxOld, ...restPropsOld } = this.props; - return ( - !shallowEqual(viewBox, viewBoxOld) || - !shallowEqual(restProps, restPropsOld) || - !shallowEqual(nextState, this.state) - ); - } - - componentDidMount() { - const tick: Element = document.getElementsByClassName('recharts-cartesian-axis-tick-value')[0]; - if (tick) { - this.setState({ - fontSize: window.getComputedStyle(tick).fontSize, - letterSpacing: window.getComputedStyle(tick).letterSpacing, - }); - } - } - - /** - * Calculate the coordinates of endpoints in ticks - * @param {Object} data The data of a simple tick - * @return {Object} (x1, y1): The coordinate of endpoint close to tick text - * (x2, y2): The coordinate of endpoint close to axis - */ - getTickLineCoord(data: CartesianTickItem) { - const { x, y, width, height, orientation, tickSize, mirror, tickMargin } = this.props; - let x1, x2, y1, y2, tx, ty; - - const sign = mirror ? -1 : 1; - const finalTickSize = data.tickSize || tickSize; - const tickCoord = isNumber(data.tickCoord) ? data.tickCoord : data.coordinate; - - switch (orientation) { - case 'top': - x1 = x2 = data.coordinate; - y2 = y + +!mirror * height; - y1 = y2 - sign * finalTickSize; - ty = y1 - sign * tickMargin; - tx = tickCoord; - break; - case 'left': - y1 = y2 = data.coordinate; - x2 = x + +!mirror * width; - x1 = x2 - sign * finalTickSize; - tx = x1 - sign * tickMargin; - ty = tickCoord; - break; - case 'right': - y1 = y2 = data.coordinate; - x2 = x + +mirror * width; - x1 = x2 + sign * finalTickSize; - tx = x1 + sign * tickMargin; - ty = tickCoord; - break; - default: - x1 = x2 = data.coordinate; - y2 = y + +mirror * height; - y1 = y2 + sign * finalTickSize; - ty = y1 + sign * tickMargin; - tx = tickCoord; - break; - } - - return { line: { x1, y1, x2, y2 }, tick: { x: tx, y: ty } }; - } - - getTickTextAnchor() { - const { orientation, mirror } = this.props; - let textAnchor; - - switch (orientation) { - case 'left': - textAnchor = mirror ? 'start' : 'end'; - break; - case 'right': - textAnchor = mirror ? 'end' : 'start'; - break; - default: - textAnchor = 'middle'; - break; - } - - return textAnchor; - } - - getTickVerticalAnchor() { - const { orientation, mirror } = this.props; - let verticalAnchor = 'end'; - - switch (orientation) { - case 'left': - case 'right': - verticalAnchor = 'middle'; - break; - case 'top': - verticalAnchor = mirror ? 'start' : 'end'; - break; - default: - verticalAnchor = mirror ? 'end' : 'start'; - break; - } - - return verticalAnchor; - } - - renderAxisLine() { - const { x, y, width, height, orientation, mirror, axisLine } = this.props; - let props: SVGProps = { - ...filterProps(this.props), - ...filterProps(axisLine), - fill: 'none', - }; - - if (orientation === 'top' || orientation === 'bottom') { - const needHeight = +((orientation === 'top' && !mirror) || (orientation === 'bottom' && mirror)); - props = { - ...props, - x1: x, - y1: y + needHeight * height, - x2: x + width, - y2: y + needHeight * height, - }; - } else { - const needWidth = +((orientation === 'left' && !mirror) || (orientation === 'right' && mirror)); - props = { - ...props, - x1: x + needWidth * width, - y1: y, - x2: x + needWidth * width, - y2: y + height, - }; - } - - return ; - } - - static renderTickItem(option: Props['tick'], props: any, value: ReactNode) { - let tickItem; - - if (React.isValidElement(option)) { - tickItem = React.cloneElement(option, props); - } else if (_.isFunction(option)) { - tickItem = option(props); - } else { - tickItem = ( - - {value} - - ); - } - - return tickItem; - } - - /** - * render the ticks - * @param {Array} ticks The ticks to actually render (overrides what was passed in props) - * @return {ReactComponent} renderedTicks - */ - renderTicks(ticks: CartesianTickItem[], fontSize: string, letterSpacing: string) { - const { tickLine, stroke, tick, tickFormatter, unit } = this.props; - const finalTicks = CartesianAxis.getTicks({ ...this.props, ticks }, fontSize, letterSpacing); - const textAnchor = this.getTickTextAnchor(); - const verticalAnchor = this.getTickVerticalAnchor(); - const axisProps = filterProps(this.props); - const customTickProps = filterProps(tick); - const tickLineProps = { - ...axisProps, - fill: 'none', - ...filterProps(tickLine), - }; - const items = finalTicks.map((entry, i) => { - const { line: lineCoord, tick: tickCoord } = this.getTickLineCoord(entry); - const tickProps = { - textAnchor, - verticalAnchor, - ...axisProps, - stroke: 'none', - fill: stroke, - ...customTickProps, - ...tickCoord, - index: i, - payload: entry, - visibleTicksCount: finalTicks.length, - tickFormatter, - }; - - return ( - - {tickLine && ( - - )} - {tick && - CartesianAxis.renderTickItem( - tick, - tickProps, - `${_.isFunction(tickFormatter) ? tickFormatter(entry.value, i) : entry.value}${unit || ''}`, - )} - - ); - }); - - return {items}; - } - - render() { - const { axisLine, width, height, ticksGenerator, className, hide } = this.props; - - if (hide) { - return null; - } - - const { ticks, ...noTicksProps } = this.props; - let finalTicks = ticks; - - if (_.isFunction(ticksGenerator)) { - finalTicks = ticks && ticks.length > 0 ? ticksGenerator(this.props) : ticksGenerator(noTicksProps); - } - - if (width <= 0 || height <= 0 || !finalTicks || !finalTicks.length) { - return null; - } - - return ( - - {axisLine && this.renderAxisLine()} - {this.renderTicks(finalTicks, this.state.fontSize, this.state.letterSpacing)} - {Label.renderCallByParent(this.props)} - - ); - } - } - \ No newline at end of file +import React, { ReactElement, ReactNode, Component, SVGProps } from 'react'; +import _ from 'lodash'; +import classNames from 'classnames'; +import { shallowEqual } from '../util/ShallowEqual'; +import { getStringSize } from '../util/DOMUtils'; +import { Layer } from '../container/Layer'; +import { Text } from '../component/Text'; +import { Label } from '../component/Label'; +import { Global } from '../util/Global'; +import { isNumber, mathSign } from '../util/DataUtils'; +import { + CartesianViewBox, + filterProps, + TickItem, + adaptEventsOfChild, + PresentationAttributesAdaptChildEvent, +} from '../util/types'; + +interface CartesianTickItem extends TickItem { + tickCoord?: number; + tickSize?: number; + isShow?: boolean; +} + +export interface CartesianAxisProps { + className?: string; + x?: number; + y?: number; + width?: number; + height?: number; + unit?: string | number; + orientation?: 'top' | 'bottom' | 'left' | 'right'; + // The viewBox of svg + viewBox?: CartesianViewBox; + tick?: SVGProps | ReactElement | ((props: any) => ReactElement) | boolean; + axisLine?: boolean | SVGProps; + tickLine?: boolean | SVGProps; + mirror?: boolean; + tickMargin?: number; + hide?: boolean; + label?: any; + + minTickGap?: number; + ticks?: CartesianTickItem[]; + tickSize?: number; + /** The formatter function of tick */ + tickFormatter?: (value: any, index: number) => string; + ticksGenerator?: (props?: CartesianAxisProps) => CartesianTickItem[]; + interval?: number | 'preserveStart' | 'preserveEnd' | 'preserveStartEnd'; +} + +export interface IState { + fontSize: string; + letterSpacing: string; +} +export type Props = Omit, 'viewBox'> & CartesianAxisProps; + +export class CartesianAxis extends Component { + static displayName = 'CartesianAxis'; + + static defaultProps = { + x: 0, + y: 0, + width: 0, + height: 0, + viewBox: { x: 0, y: 0, width: 0, height: 0 }, + // The orientation of axis + orientation: 'bottom', + // The ticks + ticks: [] as CartesianAxisProps['ticks'], + + stroke: '#666', + tickLine: true, + axisLine: true, + tick: true, + mirror: false, + + minTickGap: 5, + // The width or height of tick + tickSize: 6, + tickMargin: 2, + interval: 'preserveEnd', + }; + + constructor(props: Props) { + super(props); + this.state = { fontSize: '14px', letterSpacing: '0px' }; + } + + // todo Array + static getTicks(props: Props, fontSize: string, letterSpacing: string): any[] { + const { tick, ticks, viewBox, minTickGap, orientation, interval, tickFormatter, unit } = props; + + if (!ticks || !ticks.length || !tick) { + return []; + } + + if (isNumber(interval) || Global.isSsr) { + return CartesianAxis.getNumberIntervalTicks( + ticks, + typeof interval === 'number' && isNumber(interval) ? interval : 0, + ); + } + + if (interval === 'preserveStartEnd') { + return CartesianAxis.getTicksStart( + { + ticks, + tickFormatter, + viewBox, + orientation, + minTickGap, + unit, + fontSize, + letterSpacing, + }, + true, + ); + } + if (interval === 'preserveStart') { + return CartesianAxis.getTicksStart({ + ticks, + tickFormatter, + viewBox, + orientation, + minTickGap, + unit, + fontSize, + letterSpacing, + }); + } + + return CartesianAxis.getTicksEnd({ + ticks, + tickFormatter, + viewBox, + orientation, + minTickGap, + unit, + fontSize, + letterSpacing, + }); + } + + static getNumberIntervalTicks(ticks: CartesianTickItem[], interval: number) { + return ticks.filter((entry, i) => i % (interval + 1) === 0); + } + + static getTicksStart( + { + ticks, + tickFormatter, + viewBox, + orientation, + minTickGap, + unit, + fontSize, + letterSpacing, + }: Omit, + preserveEnd?: boolean, + ) { + const { x, y, width, height } = viewBox; + const sizeKey = orientation === 'top' || orientation === 'bottom' ? 'width' : 'height'; + const result = (ticks || []).slice(); + // we need add the width of 'unit' only when sizeKey === 'width' + const unitSize = unit && sizeKey === 'width' ? getStringSize(unit, { fontSize, letterSpacing })[sizeKey] : 0; + const len = result.length; + const sign = len >= 2 ? mathSign(result[1].coordinate - result[0].coordinate) : 1; + + let start, end; + + if (sign === 1) { + start = sizeKey === 'width' ? x : y; + end = sizeKey === 'width' ? x + width : y + height; + } else { + start = sizeKey === 'width' ? x + width : y + height; + end = sizeKey === 'width' ? x : y; + } + + if (preserveEnd) { + // Try to guarantee the tail to be displayed + let tail = ticks[len - 1]; + const tailContent = _.isFunction(tickFormatter) ? tickFormatter(tail.value, len - 1) : tail.value; + const tailSize = getStringSize(tailContent, { fontSize, letterSpacing })[sizeKey] + unitSize; + const tailGap = sign * (tail.coordinate + (sign * tailSize) / 2 - end); + result[len - 1] = tail = { + ...tail, + tickCoord: tailGap > 0 ? tail.coordinate - tailGap * sign : tail.coordinate, + }; + + const isTailShow = + sign * (tail.tickCoord - (sign * tailSize) / 2 - start) >= 0 && + sign * (tail.tickCoord + (sign * tailSize) / 2 - end) <= 0; + + if (isTailShow) { + end = tail.tickCoord - sign * (tailSize / 2 + minTickGap); + result[len - 1] = { ...tail, isShow: true }; + } + } + + const count = preserveEnd ? len - 1 : len; + for (let i = 0; i < count; i++) { + let entry = result[i]; + const content = _.isFunction(tickFormatter) ? tickFormatter(entry.value, i) : entry.value; + const size = getStringSize(content, { fontSize, letterSpacing })[sizeKey] + unitSize; + + if (i === 0) { + const gap = sign * (entry.coordinate - (sign * size) / 2 - start); + result[i] = entry = { + ...entry, + tickCoord: gap < 0 ? entry.coordinate - gap * sign : entry.coordinate, + }; + } else { + result[i] = entry = { ...entry, tickCoord: entry.coordinate }; + } + + const isShow = + sign * (entry.tickCoord - (sign * size) / 2 - start) >= 0 && + sign * (entry.tickCoord + (sign * size) / 2 - end) <= 0; + + if (isShow) { + start = entry.tickCoord + sign * (size / 2 + minTickGap); + result[i] = { ...entry, isShow: true }; + } + } + + return result.filter(entry => entry.isShow); + } + + static getTicksEnd({ + ticks, + tickFormatter, + viewBox, + orientation, + minTickGap, + unit, + fontSize, + letterSpacing, + }: Omit) { + const { x, y, width, height } = viewBox; + const sizeKey = orientation === 'top' || orientation === 'bottom' ? 'width' : 'height'; + // we need add the width of 'unit' only when sizeKey === 'width' + const unitSize = unit && sizeKey === 'width' ? getStringSize(unit, { fontSize, letterSpacing })[sizeKey] : 0; + const result = (ticks || []).slice(); + const len = result.length; + const sign = len >= 2 ? mathSign(result[1].coordinate - result[0].coordinate) : 1; + + let start, end; + + if (sign === 1) { + start = sizeKey === 'width' ? x : y; + end = sizeKey === 'width' ? x + width : y + height; + } else { + start = sizeKey === 'width' ? x + width : y + height; + end = sizeKey === 'width' ? x : y; + } + + for (let i = len - 1; i >= 0; i--) { + let entry = result[i]; + const content = _.isFunction(tickFormatter) ? tickFormatter(entry.value, len - i - 1) : entry.value; + const size = getStringSize(content, { fontSize, letterSpacing })[sizeKey] + unitSize; + + if (i === len - 1) { + const gap = sign * (entry.coordinate + (sign * size) / 2 - end); + result[i] = entry = { + ...entry, + tickCoord: gap > 0 ? entry.coordinate - gap * sign : entry.coordinate, + }; + } else { + result[i] = entry = { ...entry, tickCoord: entry.coordinate }; + } + + const isShow = + sign * (entry.tickCoord - (sign * size) / 2 - start) >= 0 && + sign * (entry.tickCoord + (sign * size) / 2 - end) <= 0; + + if (isShow) { + end = entry.tickCoord - sign * (size / 2 + minTickGap); + result[i] = { ...entry, isShow: true }; + } + } + + return result.filter(entry => entry.isShow); + } + + shouldComponentUpdate({ viewBox, ...restProps }: Props, nextState: IState) { + // props.viewBox is sometimes generated every time - + // check that specially as object equality is likely to fail + const { viewBox: viewBoxOld, ...restPropsOld } = this.props; + return ( + !shallowEqual(viewBox, viewBoxOld) || + !shallowEqual(restProps, restPropsOld) || + !shallowEqual(nextState, this.state) + ); + } + + componentDidMount() { + const tick: Element = document.getElementsByClassName('recharts-cartesian-axis-tick-value')[0]; + if (tick) { + this.setState({ + fontSize: window.getComputedStyle(tick).fontSize, + letterSpacing: window.getComputedStyle(tick).letterSpacing, + }); + } + } + + /** + * Calculate the coordinates of endpoints in ticks + * @param {Object} data The data of a simple tick + * @return {Object} (x1, y1): The coordinate of endpoint close to tick text + * (x2, y2): The coordinate of endpoint close to axis + */ + getTickLineCoord(data: CartesianTickItem) { + const { x, y, width, height, orientation, tickSize, mirror, tickMargin } = this.props; + let x1, x2, y1, y2, tx, ty; + + const sign = mirror ? -1 : 1; + const finalTickSize = data.tickSize || tickSize; + const tickCoord = isNumber(data.tickCoord) ? data.tickCoord : data.coordinate; + + switch (orientation) { + case 'top': + x1 = x2 = data.coordinate; + y2 = y + +!mirror * height; + y1 = y2 - sign * finalTickSize; + ty = y1 - sign * tickMargin; + tx = tickCoord; + break; + case 'left': + y1 = y2 = data.coordinate; + x2 = x + +!mirror * width; + x1 = x2 - sign * finalTickSize; + tx = x1 - sign * tickMargin; + ty = tickCoord; + break; + case 'right': + y1 = y2 = data.coordinate; + x2 = x + +mirror * width; + x1 = x2 + sign * finalTickSize; + tx = x1 + sign * tickMargin; + ty = tickCoord; + break; + default: + x1 = x2 = data.coordinate; + y2 = y + +mirror * height; + y1 = y2 + sign * finalTickSize; + ty = y1 + sign * tickMargin; + tx = tickCoord; + break; + } + + return { line: { x1, y1, x2, y2 }, tick: { x: tx, y: ty } }; + } + + getTickTextAnchor() { + const { orientation, mirror } = this.props; + let textAnchor; + + switch (orientation) { + case 'left': + textAnchor = mirror ? 'start' : 'end'; + break; + case 'right': + textAnchor = mirror ? 'end' : 'start'; + break; + default: + textAnchor = 'middle'; + break; + } + + return textAnchor; + } + + getTickVerticalAnchor() { + const { orientation, mirror } = this.props; + let verticalAnchor = 'end'; + + switch (orientation) { + case 'left': + case 'right': + verticalAnchor = 'middle'; + break; + case 'top': + verticalAnchor = mirror ? 'start' : 'end'; + break; + default: + verticalAnchor = mirror ? 'end' : 'start'; + break; + } + + return verticalAnchor; + } + + renderAxisLine() { + const { x, y, width, height, orientation, mirror, axisLine } = this.props; + let props: SVGProps = { + ...filterProps(this.props), + ...filterProps(axisLine), + fill: 'none', + }; + + if (orientation === 'top' || orientation === 'bottom') { + const needHeight = +((orientation === 'top' && !mirror) || (orientation === 'bottom' && mirror)); + props = { + ...props, + x1: x, + y1: y + needHeight * height, + x2: x + width, + y2: y + needHeight * height, + }; + } else { + const needWidth = +((orientation === 'left' && !mirror) || (orientation === 'right' && mirror)); + props = { + ...props, + x1: x + needWidth * width, + y1: y, + x2: x + needWidth * width, + y2: y + height, + }; + } + + return ; + } + + static renderTickItem(option: Props['tick'], props: any, value: ReactNode) { + let tickItem; + + if (React.isValidElement(option)) { + tickItem = React.cloneElement(option, props); + } else if (_.isFunction(option)) { + tickItem = option(props); + } else { + tickItem = ( + + {value} + + ); + } + + return tickItem; + } + + /** + * render the ticks + * @param {Array} ticks The ticks to actually render (overrides what was passed in props) + * @return {ReactComponent} renderedTicks + */ + renderTicks(ticks: CartesianTickItem[], fontSize: string, letterSpacing: string) { + const { tickLine, stroke, tick, tickFormatter, unit } = this.props; + const finalTicks = CartesianAxis.getTicks({ ...this.props, ticks }, fontSize, letterSpacing); + const textAnchor = this.getTickTextAnchor(); + const verticalAnchor = this.getTickVerticalAnchor(); + const axisProps = filterProps(this.props); + const customTickProps = filterProps(tick); + const tickLineProps = { + ...axisProps, + fill: 'none', + ...filterProps(tickLine), + }; + const items = finalTicks.map((entry, i) => { + const { line: lineCoord, tick: tickCoord } = this.getTickLineCoord(entry); + const tickProps = { + textAnchor, + verticalAnchor, + ...axisProps, + stroke: 'none', + fill: stroke, + ...customTickProps, + ...tickCoord, + index: i, + payload: entry, + visibleTicksCount: finalTicks.length, + tickFormatter, + }; + + return ( + + {tickLine && ( + + )} + {tick && + CartesianAxis.renderTickItem( + tick, + tickProps, + `${_.isFunction(tickFormatter) ? tickFormatter(entry.value, i) : entry.value}${unit || ''}`, + )} + + ); + }); + + return {items}; + } + + render() { + const { axisLine, width, height, ticksGenerator, className, hide } = this.props; + + if (hide) { + return null; + } + + const { ticks, ...noTicksProps } = this.props; + let finalTicks = ticks; + + if (_.isFunction(ticksGenerator)) { + finalTicks = ticks && ticks.length > 0 ? ticksGenerator(this.props) : ticksGenerator(noTicksProps); + } + + if (width <= 0 || height <= 0 || !finalTicks || !finalTicks.length) { + return null; + } + + return ( + + {axisLine && this.renderAxisLine()} + {this.renderTicks(finalTicks, this.state.fontSize, this.state.letterSpacing)} + {Label.renderCallByParent(this.props)} + + ); + } +} From 6dccda7e766619c6b53639236835ef8cec6d3d48 Mon Sep 17 00:00:00 2001 From: saghan Date: Wed, 29 Jun 2022 08:40:11 -0700 Subject: [PATCH 3/6] no exporting state interface --- src/cartesian/CartesianAxis.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cartesian/CartesianAxis.tsx b/src/cartesian/CartesianAxis.tsx index 2a4ae711da..244cccd400 100644 --- a/src/cartesian/CartesianAxis.tsx +++ b/src/cartesian/CartesianAxis.tsx @@ -52,7 +52,7 @@ export interface CartesianAxisProps { interval?: number | 'preserveStart' | 'preserveEnd' | 'preserveStartEnd'; } -export interface IState { +interface IState { fontSize: string; letterSpacing: string; } From 9e124b1ca411979d1bee0af4f5c28af1176cb321 Mon Sep 17 00:00:00 2001 From: saghan Date: Sun, 10 Jul 2022 20:54:07 -0700 Subject: [PATCH 4/6] fixed scope of getElement --- src/cartesian/CartesianAxis.tsx | 17 ++++-- src/container/Layer.tsx | 6 +- test/specs/cartesian/CartesianAxisSpec.js | 68 +++++++++++++---------- 3 files changed, 56 insertions(+), 35 deletions(-) diff --git a/src/cartesian/CartesianAxis.tsx b/src/cartesian/CartesianAxis.tsx index 244cccd400..caca6cf115 100644 --- a/src/cartesian/CartesianAxis.tsx +++ b/src/cartesian/CartesianAxis.tsx @@ -85,13 +85,15 @@ export class CartesianAxis extends Component { interval: 'preserveEnd', }; + private layerReference: any; + constructor(props: Props) { super(props); - this.state = { fontSize: '14px', letterSpacing: '0px' }; + this.state = { fontSize: '', letterSpacing: '' }; } // todo Array - static getTicks(props: Props, fontSize: string, letterSpacing: string): any[] { + static getTicks(props: Props, fontSize?: string, letterSpacing?: string): any[] { const { tick, ticks, viewBox, minTickGap, orientation, interval, tickFormatter, unit } = props; if (!ticks || !ticks.length || !tick) { @@ -298,7 +300,9 @@ export class CartesianAxis extends Component { } componentDidMount() { - const tick: Element = document.getElementsByClassName('recharts-cartesian-axis-tick-value')[0]; + const htmlLayer: SVGElement = this.layerReference; + if (!htmlLayer) return; + const tick: Element = htmlLayer.getElementsByClassName('recharts-cartesian-axis-tick-value')[0]; if (tick) { this.setState({ fontSize: window.getComputedStyle(tick).fontSize, @@ -521,7 +525,12 @@ export class CartesianAxis extends Component { } return ( - + { + this.layerReference = ref; + }} + > {axisLine && this.renderAxisLine()} {this.renderTicks(finalTicks, this.state.fontSize, this.state.letterSpacing)} {Label.renderCallByParent(this.props)} diff --git a/src/container/Layer.tsx b/src/container/Layer.tsx index ebed65d613..6a3228a9dc 100644 --- a/src/container/Layer.tsx +++ b/src/container/Layer.tsx @@ -12,13 +12,13 @@ interface LayerProps { export type Props = SVGProps & LayerProps; -export function Layer(props: Props) { +export const Layer = React.forwardRef((props: Props, ref: any) => { const { children, className, ...others } = props; const layerClass = classNames('recharts-layer', className); return ( - + {children} ); -} +}); diff --git a/test/specs/cartesian/CartesianAxisSpec.js b/test/specs/cartesian/CartesianAxisSpec.js index 2e7219d84c..e86ae10f54 100644 --- a/test/specs/cartesian/CartesianAxisSpec.js +++ b/test/specs/cartesian/CartesianAxisSpec.js @@ -24,7 +24,7 @@ describe('', () => { ticks={ticks} label="test" /> - + , ); expect(wrapper.find('.recharts-cartesian-axis-tick').length).to.equal(5); @@ -43,7 +43,7 @@ describe('', () => { ticks={[]} label="test" /> - + , ); expect(wrapper.find('.recharts-cartesian-axis-tick').length).to.equal(0); @@ -62,7 +62,7 @@ describe('', () => { label="test" interval="preserveStartEnd" /> - + , ); expect(wrapper.find('.recharts-cartesian-axis-tick').length).to.equal(5); @@ -81,7 +81,7 @@ describe('', () => { label="test" interval="preserveStart" /> - + , ); expect(wrapper.find('.recharts-cartesian-axis-tick').length).to.equal(5); @@ -99,7 +99,7 @@ describe('', () => { ticks={ticks} label="top" /> - + , ); expect(wrapper.find('.recharts-cartesian-axis-tick').length).to.equal(5); @@ -118,7 +118,7 @@ describe('', () => { ticks={ticks} label="left" /> - + , ); expect(wrapper.find('.recharts-cartesian-axis-tick').length).to.equal(5); @@ -137,19 +137,22 @@ describe('', () => { ticks={ticks} label="right" /> - + , ); expect(wrapper.find('.recharts-cartesian-axis-tick').length).to.equal(5); expect(wrapper.find('.recharts-label').length).to.equal(1); }); - it('Renders label when label is a function', () => { - const renderLabel = (props) => { + const renderLabel = props => { const { x, y, width, height } = props; - return test; + return ( + + test + + ); }; const wrapper = render( @@ -162,7 +165,7 @@ describe('', () => { ticks={ticks} label={renderLabel} /> - + , ); expect(wrapper.find('.recharts-cartesian-axis-tick').length).to.equal(5); @@ -170,10 +173,14 @@ describe('', () => { }); it('Renders label when label is a react element', () => { - const Label = (props) => { + const Label = props => { const { x, y, width, height } = props; - return test; + return ( + + test + + ); }; const wrapper = render( @@ -186,16 +193,18 @@ describe('', () => { ticks={ticks} label={ + , ); expect(wrapper.find('.recharts-cartesian-axis-tick').length).to.equal(5); }); it('Render customized ticks when tick is set to be a ReactElement', () => { - const CustomizedTick = ({ x, y }) => - test - ; + const CustomizedTick = ({ x, y }) => ( + + test + + ); const wrapper = render( ', () => { tick={} interval={0} /> - + , ); expect(wrapper.find('.customized-tick').length).to.equal(ticks.length); }); it('Render customized ticks when ticks is an array of strings and interval is 0', () => { - const CustomizedTick = ({ x, y }) => - test - ; + const CustomizedTick = ({ x, y }) => ( + + test + + ); const wrapper = render( ', () => { tick={} interval={0} /> - + , ); expect(wrapper.find('.customized-tick').length).to.equal(3); }); it('Render customized ticks when tick is set to be a function', () => { - const renderCustomizedTick = ({ x, y }) => - test - ; + const renderCustomizedTick = ({ x, y }) => ( + + test + + ); const wrapper = render( ', () => { tick={renderCustomizedTick} interval={0} /> - + , ); expect(wrapper.find('.customized-tick').length).to.equal(ticks.length); @@ -269,10 +282,9 @@ describe('', () => { viewBox={{ x: 0, y: 0, width: 500, height: 500 }} tick={false} /> - + , ); expect(wrapper.find('.recharts-cartesian-axis-tick').length).to.equal(0); }); - }); From adbe1f7e3c2bbd63b3443637447fdd2ec77493f5 Mon Sep 17 00:00:00 2001 From: saghan Date: Tue, 12 Jul 2022 19:10:42 -0700 Subject: [PATCH 5/6] rolling back eslint changes --- test/specs/cartesian/CartesianAxisSpec.js | 68 ++++++++++------------- 1 file changed, 28 insertions(+), 40 deletions(-) diff --git a/test/specs/cartesian/CartesianAxisSpec.js b/test/specs/cartesian/CartesianAxisSpec.js index e86ae10f54..2e7219d84c 100644 --- a/test/specs/cartesian/CartesianAxisSpec.js +++ b/test/specs/cartesian/CartesianAxisSpec.js @@ -24,7 +24,7 @@ describe('', () => { ticks={ticks} label="test" /> - , + ); expect(wrapper.find('.recharts-cartesian-axis-tick').length).to.equal(5); @@ -43,7 +43,7 @@ describe('', () => { ticks={[]} label="test" /> - , + ); expect(wrapper.find('.recharts-cartesian-axis-tick').length).to.equal(0); @@ -62,7 +62,7 @@ describe('', () => { label="test" interval="preserveStartEnd" /> - , + ); expect(wrapper.find('.recharts-cartesian-axis-tick').length).to.equal(5); @@ -81,7 +81,7 @@ describe('', () => { label="test" interval="preserveStart" /> - , + ); expect(wrapper.find('.recharts-cartesian-axis-tick').length).to.equal(5); @@ -99,7 +99,7 @@ describe('', () => { ticks={ticks} label="top" /> - , + ); expect(wrapper.find('.recharts-cartesian-axis-tick').length).to.equal(5); @@ -118,7 +118,7 @@ describe('', () => { ticks={ticks} label="left" /> - , + ); expect(wrapper.find('.recharts-cartesian-axis-tick').length).to.equal(5); @@ -137,22 +137,19 @@ describe('', () => { ticks={ticks} label="right" /> - , + ); expect(wrapper.find('.recharts-cartesian-axis-tick').length).to.equal(5); expect(wrapper.find('.recharts-label').length).to.equal(1); }); + it('Renders label when label is a function', () => { - const renderLabel = props => { + const renderLabel = (props) => { const { x, y, width, height } = props; - return ( - - test - - ); + return test; }; const wrapper = render( @@ -165,7 +162,7 @@ describe('', () => { ticks={ticks} label={renderLabel} /> - , + ); expect(wrapper.find('.recharts-cartesian-axis-tick').length).to.equal(5); @@ -173,14 +170,10 @@ describe('', () => { }); it('Renders label when label is a react element', () => { - const Label = props => { + const Label = (props) => { const { x, y, width, height } = props; - return ( - - test - - ); + return test; }; const wrapper = render( @@ -193,18 +186,16 @@ describe('', () => { ticks={ticks} label={, + ); expect(wrapper.find('.recharts-cartesian-axis-tick').length).to.equal(5); }); it('Render customized ticks when tick is set to be a ReactElement', () => { - const CustomizedTick = ({ x, y }) => ( - - test - - ); + const CustomizedTick = ({ x, y }) => + test + ; const wrapper = render( ', () => { tick={} interval={0} /> - , + ); expect(wrapper.find('.customized-tick').length).to.equal(ticks.length); }); it('Render customized ticks when ticks is an array of strings and interval is 0', () => { - const CustomizedTick = ({ x, y }) => ( - - test - - ); + const CustomizedTick = ({ x, y }) => + test + ; const wrapper = render( ', () => { tick={} interval={0} /> - , + ); expect(wrapper.find('.customized-tick').length).to.equal(3); }); it('Render customized ticks when tick is set to be a function', () => { - const renderCustomizedTick = ({ x, y }) => ( - - test - - ); + const renderCustomizedTick = ({ x, y }) => + test + ; const wrapper = render( ', () => { tick={renderCustomizedTick} interval={0} /> - , + ); expect(wrapper.find('.customized-tick').length).to.equal(ticks.length); @@ -282,9 +269,10 @@ describe('', () => { viewBox={{ x: 0, y: 0, width: 500, height: 500 }} tick={false} /> - , + ); expect(wrapper.find('.recharts-cartesian-axis-tick').length).to.equal(0); }); + }); From e270cc7f2e48db8baba16c3620add331d26dfe46 Mon Sep 17 00:00:00 2001 From: saghan Date: Sun, 17 Jul 2022 11:30:04 -0700 Subject: [PATCH 6/6] test added --- test/specs/cartesian/CartesianAxisSpec.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/specs/cartesian/CartesianAxisSpec.js b/test/specs/cartesian/CartesianAxisSpec.js index e86ae10f54..6028143405 100644 --- a/test/specs/cartesian/CartesianAxisSpec.js +++ b/test/specs/cartesian/CartesianAxisSpec.js @@ -2,6 +2,7 @@ import React from 'react'; import { expect } from 'chai'; import { Surface, CartesianAxis } from 'recharts'; import { mount, render } from 'enzyme'; +import sinon from 'sinon'; describe('', () => { const ticks = [ @@ -68,6 +69,24 @@ describe('', () => { expect(wrapper.find('.recharts-cartesian-axis-tick').length).to.equal(5); }); + it('gets font states from its ComputedStyle', () => { + const stub = sinon.stub(window, 'getComputedStyle').returns({ fontSize: '14px', letterSpacing: '0.5em' }); + const wrapper = mount( + , + ); + + expect(wrapper.state().fontSize).to.equal('14px'); + expect(wrapper.state().letterSpacing).to.equal('0.5em'); + + stub.restore(); + }); + it('Renders ticks when interval="preserveStart"', () => { const wrapper = render(