Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Take letter-spacing and font-size into consideration while rendering ticks #2898

Merged
merged 7 commits into from Aug 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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