Skip to content

Commit

Permalink
Take letter-spacing and font-size into consideration while rendering …
Browse files Browse the repository at this point in the history
…ticks (#2898)

* accessibility fix for letterSpacing

* fixed line ending

* no exporting state interface

* fixed scope of getElement

* rolling back eslint changes

* test added
  • Loading branch information
saghan committed Aug 18, 2022
1 parent 6b3cce4 commit 6be367c
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 18 deletions.
86 changes: 71 additions & 15 deletions src/cartesian/CartesianAxis.tsx
Expand Up @@ -52,9 +52,13 @@ export interface CartesianAxisProps {
interval?: number | 'preserveStart' | 'preserveEnd' | 'preserveStartEnd';
}

interface IState {
fontSize: string;
letterSpacing: string;
}
export type Props = Omit<PresentationAttributesAdaptChildEvent<any, SVGElement>, 'viewBox'> & CartesianAxisProps;

export class CartesianAxis extends Component<Props> {
export class CartesianAxis extends Component<Props, IState> {
static displayName = 'CartesianAxis';

static defaultProps = {
Expand All @@ -81,8 +85,15 @@ export class CartesianAxis extends Component<Props> {
interval: 'preserveEnd',
};

private layerReference: any;

constructor(props: Props) {
super(props);
this.state = { fontSize: '', letterSpacing: '' };
}

// todo Array<Tick>
static getTicks(props: Props): 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) {
Expand All @@ -105,6 +116,8 @@ export class CartesianAxis extends Component<Props> {
orientation,
minTickGap,
unit,
fontSize,
letterSpacing,
},
true,
);
Expand All @@ -117,6 +130,8 @@ export class CartesianAxis extends Component<Props> {
orientation,
minTickGap,
unit,
fontSize,
letterSpacing,
});
}

Expand All @@ -127,6 +142,8 @@ export class CartesianAxis extends Component<Props> {
orientation,
minTickGap,
unit,
fontSize,
letterSpacing,
});
}

Expand All @@ -135,14 +152,23 @@ export class CartesianAxis extends Component<Props> {
}

static getTicksStart(
{ ticks, tickFormatter, viewBox, orientation, minTickGap, unit }: Omit<Props, 'tickMargin'>,
{
ticks,
tickFormatter,
viewBox,
orientation,
minTickGap,
unit,
fontSize,
letterSpacing,
}: Omit<Props, 'tickMargin'>,
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 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;

Expand All @@ -160,7 +186,7 @@ export class CartesianAxis extends Component<Props> {
// 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 tailSize = getStringSize(tailContent, { fontSize, letterSpacing })[sizeKey] + unitSize;
const tailGap = sign * (tail.coordinate + (sign * tailSize) / 2 - end);
result[len - 1] = tail = {
...tail,
Expand All @@ -181,7 +207,7 @@ export class CartesianAxis extends Component<Props> {
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;
const size = getStringSize(content, { fontSize, letterSpacing })[sizeKey] + unitSize;

if (i === 0) {
const gap = sign * (entry.coordinate - (sign * size) / 2 - start);
Expand All @@ -206,11 +232,20 @@ export class CartesianAxis extends Component<Props> {
return result.filter(entry => entry.isShow);
}

static getTicksEnd({ ticks, tickFormatter, viewBox, orientation, minTickGap, unit }: Omit<Props, 'tickMargin'>) {
static getTicksEnd({
ticks,
tickFormatter,
viewBox,
orientation,
minTickGap,
unit,
fontSize,
letterSpacing,
}: Omit<Props, 'tickMargin'>) {
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 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;
Expand All @@ -228,7 +263,7 @@ export class CartesianAxis extends Component<Props> {
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;
const size = getStringSize(content, { fontSize, letterSpacing })[sizeKey] + unitSize;

if (i === len - 1) {
const gap = sign * (entry.coordinate + (sign * size) / 2 - end);
Expand All @@ -253,11 +288,27 @@ export class CartesianAxis extends Component<Props> {
return result.filter(entry => entry.isShow);
}

shouldComponentUpdate({ viewBox, ...restProps }: Props) {
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);
return (
!shallowEqual(viewBox, viewBoxOld) ||
!shallowEqual(restProps, restPropsOld) ||
!shallowEqual(nextState, this.state)
);
}

componentDidMount() {
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,
letterSpacing: window.getComputedStyle(tick).letterSpacing,
});
}
}

/**
Expand Down Expand Up @@ -401,9 +452,9 @@ export class CartesianAxis extends Component<Props> {
* @param {Array} ticks The ticks to actually render (overrides what was passed in props)
* @return {ReactComponent} renderedTicks
*/
renderTicks(ticks: CartesianTickItem[]) {
renderTicks(ticks: CartesianTickItem[], fontSize: string, letterSpacing: string) {
const { tickLine, stroke, tick, tickFormatter, unit } = this.props;
const finalTicks = CartesianAxis.getTicks({ ...this.props, ticks });
const finalTicks = CartesianAxis.getTicks({ ...this.props, ticks }, fontSize, letterSpacing);
const textAnchor = this.getTickTextAnchor();
const verticalAnchor = this.getTickVerticalAnchor();
const axisProps = filterProps(this.props);
Expand Down Expand Up @@ -474,9 +525,14 @@ export class CartesianAxis extends Component<Props> {
}

return (
<Layer className={classNames('recharts-cartesian-axis', className)}>
<Layer
className={classNames('recharts-cartesian-axis', className)}
ref={ref => {
this.layerReference = ref;
}}
>
{axisLine && this.renderAxisLine()}
{this.renderTicks(finalTicks)}
{this.renderTicks(finalTicks, this.state.fontSize, this.state.letterSpacing)}
{Label.renderCallByParent(this.props)}
</Layer>
);
Expand Down
6 changes: 3 additions & 3 deletions src/container/Layer.tsx
Expand Up @@ -12,13 +12,13 @@ interface LayerProps {

export type Props = SVGProps<SVGGElement> & 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 (
<g className={layerClass} {...filterProps(others, true)}>
<g className={layerClass} {...filterProps(others, true)} ref={ref}>
{children}
</g>
);
}
});
19 changes: 19 additions & 0 deletions test/specs/cartesian/CartesianAxisSpec.js
Expand Up @@ -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('<CartesianAxis />', () => {
const ticks = [
Expand Down Expand Up @@ -68,6 +69,24 @@ describe('<CartesianAxis />', () => {
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(
<CartesianAxis
orientation="bottom"
width={400}
height={50}
viewBox={{ x: 0, y: 0, width: 500, height: 500 }}
ticks={ticks}
/>,
);

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(
<Surface width={500} height={500}>
Expand Down

0 comments on commit 6be367c

Please sign in to comment.