From 0df5778a3ed5f1a3f9e23b30afb740d79b45286b Mon Sep 17 00:00:00 2001 From: Sebastian Quiroga Date: Tue, 25 Jul 2023 09:33:38 -0500 Subject: [PATCH 1/7] New AxisProps rotateTickOnLength with its corresponding Prop-types --- packages/axes/src/props.ts | 4 ++++ packages/axes/src/types.ts | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/axes/src/props.ts b/packages/axes/src/props.ts index 9c7646faa..4487871f4 100644 --- a/packages/axes/src/props.ts +++ b/packages/axes/src/props.ts @@ -9,6 +9,10 @@ export const axisPropTypes = { ), PropTypes.string, ]), + rotateOnTickLength: PropTypes.shape({ + angle: PropTypes.number, + length: PropTypes.number, + }), tickSize: PropTypes.number, tickPadding: PropTypes.number, tickRotation: PropTypes.number, diff --git a/packages/axes/src/types.ts b/packages/axes/src/types.ts index 9c3532783..3f6eb6f3c 100644 --- a/packages/axes/src/types.ts +++ b/packages/axes/src/types.ts @@ -1,6 +1,6 @@ -import * as React from 'react' import { ScaleValue, TicksSpec } from '@nivo/scales' import { SpringValues } from '@react-spring/web' +import * as React from 'react' export type GridValuesBuilder = T extends number ? number[] @@ -21,6 +21,8 @@ export type AxisLegendPosition = 'start' | 'middle' | 'end' export type ValueFormatter = (value: Value) => Value | string +export type TickRotationOnLength = { angle: number | undefined; length: number | undefined } + export interface AxisProps { ticksPosition?: 'before' | 'after' tickValues?: TicksSpec @@ -28,6 +30,7 @@ export interface AxisProps { tickPadding?: number tickRotation?: number format?: string | ValueFormatter + rotateOnTickLength: TickRotationOnLength renderTick?: (props: AxisTickProps) => JSX.Element legend?: React.ReactNode legendPosition?: AxisLegendPosition From e324d77e8cc5154a6add9e56384e9103c95fd46e Mon Sep 17 00:00:00 2001 From: Sebastian Quiroga Date: Tue, 25 Jul 2023 09:42:34 -0500 Subject: [PATCH 2/7] rotateOnTickLength as optional --- packages/axes/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/axes/src/types.ts b/packages/axes/src/types.ts index 3f6eb6f3c..ddb8e114a 100644 --- a/packages/axes/src/types.ts +++ b/packages/axes/src/types.ts @@ -30,7 +30,7 @@ export interface AxisProps { tickPadding?: number tickRotation?: number format?: string | ValueFormatter - rotateOnTickLength: TickRotationOnLength + rotateOnTickLength?: TickRotationOnLength renderTick?: (props: AxisTickProps) => JSX.Element legend?: React.ReactNode legendPosition?: AxisLegendPosition From f54b8ed996d3060ba0a970fcc983fa7041bacc46 Mon Sep 17 00:00:00 2001 From: Sebastian Quiroga Date: Tue, 25 Jul 2023 13:18:11 -0500 Subject: [PATCH 3/7] Website new control for Axes on BarChart --- website/src/lib/chart-properties/axes.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/website/src/lib/chart-properties/axes.ts b/website/src/lib/chart-properties/axes.ts index 41335f510..56f2dfd4e 100644 --- a/website/src/lib/chart-properties/axes.ts +++ b/website/src/lib/chart-properties/axes.ts @@ -106,6 +106,18 @@ export const axes = ({ max: 60, }, }, + { + key: `rotateTickOnLength`, + flavors, + help: `${axisKey} prevent the tick to overlap rotating when length overpassing certain length`, + type: '{angle: number, length: number }', + control: { + type: 'angle', + start: 90, + min: -90, + max: 90, + }, + }, ], }, }, From 19d79047c2b3cb70130ceff6741820e5fde92f14 Mon Sep 17 00:00:00 2001 From: Sebastian Quiroga Date: Fri, 28 Jul 2023 18:48:32 -0500 Subject: [PATCH 4/7] Adding rotation in animatedProps --- packages/axes/src/components/Axis.tsx | 45 +++++++++++++++++++++------ 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/packages/axes/src/components/Axis.tsx b/packages/axes/src/components/Axis.tsx index 6da3ea5bf..06c1ec00c 100644 --- a/packages/axes/src/components/Axis.tsx +++ b/packages/axes/src/components/Axis.tsx @@ -1,11 +1,11 @@ -import { useMemo, memo, useCallback } from 'react' +import { useMotionConfig, useTheme } from '@nivo/core' +import { AnyScale, ScaleValue } from '@nivo/scales' +import { animated, useSpring, useTransition } from '@react-spring/web' import * as React from 'react' -import { useSpring, useTransition, animated } from '@react-spring/web' -import { useTheme, useMotionConfig } from '@nivo/core' -import { ScaleValue, AnyScale } from '@nivo/scales' +import { memo, useCallback, useMemo } from 'react' import { computeCartesianTicks, getFormatter } from '../compute' -import { AxisTick } from './AxisTick' import { AxisProps } from '../types' +import { AxisTick } from './AxisTick' export const NonMemoizedAxis = ({ axis, @@ -20,6 +20,7 @@ export const NonMemoizedAxis = ({ tickRotation = 0, format, renderTick = AxisTick, + rotateTickOnLength, legend, legendPosition = 'end', legendOffset = 0, @@ -121,12 +122,35 @@ export const NonMemoizedAxis = ({ immediate: !animate, }) + const rotateTick = useCallback( + (thresholdLength: number, rotation: number, tick: (typeof ticks)[0]): string => { + if (String(tick.value).length >= thresholdLength) { + return `translate(${tick.x},${tick.y}) rotate(${rotation})` + } + return `translate(${tick.x},${tick.y})` + }, + [] + ) + + const autoRotationOnLength = useCallback( + (tick: (typeof ticks)[0]): string => { + if (!rotateTickOnLength || !rotateTickOnLength.angle || rotateTickOnLength.angle === 0) + return `translate(${tick.x},${tick.y})` + + return rotateTick(rotateTickOnLength?.length ?? 5, rotateTickOnLength.angle, tick) + }, + [rotateTick, rotateTickOnLength] + ) + const getAnimatedProps = useCallback( - (tick: (typeof ticks)[0]) => ({ - opacity: 1, - transform: `translate(${tick.x},${tick.y})`, - textTransform: `translate(${tick.textX},${tick.textY}) rotate(${tickRotation})`, - }), + (tick: (typeof ticks)[0]) => { + return { + opacity: 1, + transform: autoRotationOnLength(tick), + // transform: `translate(${tick.x},${tick.y})`, + textTransform: `translate(${tick.textX},${tick.textY}) rotate(${tickRotation})`, + } + }, [tickRotation] ) const getFromAnimatedProps = useCallback( @@ -163,6 +187,7 @@ export const NonMemoizedAxis = ({ rotate: tickRotation, textBaseline, textAnchor: textAlign, + rotateTickOnLength: rotateTickOnLength, animatedProps: transitionProps, ...tick, ...(onClick ? { onClick } : {}), From ac6d77a263fd23de8ba692ba801e29ffa0877dd7 Mon Sep 17 00:00:00 2001 From: Sebastian Quiroga Date: Fri, 28 Jul 2023 19:55:07 -0500 Subject: [PATCH 5/7] truncateTickAt initial approach finished --- packages/axes/src/components/Axes.tsx | 1 + packages/axes/src/components/Axis.tsx | 27 +++-------------------- packages/axes/src/components/AxisTick.tsx | 13 +++++++++-- packages/axes/src/types.ts | 5 ++--- website/src/lib/chart-properties/axes.ts | 26 +++++++++++----------- website/src/pages/bar/canvas.js | 4 ++++ website/src/pages/bar/index.js | 4 ++++ 7 files changed, 38 insertions(+), 42 deletions(-) diff --git a/packages/axes/src/components/Axes.tsx b/packages/axes/src/components/Axes.tsx index 0e8169a57..5399980bd 100644 --- a/packages/axes/src/components/Axes.tsx +++ b/packages/axes/src/components/Axes.tsx @@ -49,6 +49,7 @@ export const Axes = memo( scale={isXAxis ? xScale : yScale} length={isXAxis ? width : height} ticksPosition={ticksPosition} + truncateTickAt={axis.truncateTickAt} /> ) })} diff --git a/packages/axes/src/components/Axis.tsx b/packages/axes/src/components/Axis.tsx index 06c1ec00c..69119e6fc 100644 --- a/packages/axes/src/components/Axis.tsx +++ b/packages/axes/src/components/Axis.tsx @@ -20,7 +20,7 @@ export const NonMemoizedAxis = ({ tickRotation = 0, format, renderTick = AxisTick, - rotateTickOnLength, + truncateTickAt, legend, legendPosition = 'end', legendOffset = 0, @@ -122,32 +122,11 @@ export const NonMemoizedAxis = ({ immediate: !animate, }) - const rotateTick = useCallback( - (thresholdLength: number, rotation: number, tick: (typeof ticks)[0]): string => { - if (String(tick.value).length >= thresholdLength) { - return `translate(${tick.x},${tick.y}) rotate(${rotation})` - } - return `translate(${tick.x},${tick.y})` - }, - [] - ) - - const autoRotationOnLength = useCallback( - (tick: (typeof ticks)[0]): string => { - if (!rotateTickOnLength || !rotateTickOnLength.angle || rotateTickOnLength.angle === 0) - return `translate(${tick.x},${tick.y})` - - return rotateTick(rotateTickOnLength?.length ?? 5, rotateTickOnLength.angle, tick) - }, - [rotateTick, rotateTickOnLength] - ) - const getAnimatedProps = useCallback( (tick: (typeof ticks)[0]) => { return { opacity: 1, - transform: autoRotationOnLength(tick), - // transform: `translate(${tick.x},${tick.y})`, + transform: `translate(${tick.x},${tick.y})`, textTransform: `translate(${tick.textX},${tick.textY}) rotate(${tickRotation})`, } }, @@ -187,7 +166,7 @@ export const NonMemoizedAxis = ({ rotate: tickRotation, textBaseline, textAnchor: textAlign, - rotateTickOnLength: rotateTickOnLength, + truncateTickAt: truncateTickAt, animatedProps: transitionProps, ...tick, ...(onClick ? { onClick } : {}), diff --git a/packages/axes/src/components/AxisTick.tsx b/packages/axes/src/components/AxisTick.tsx index bbd887d24..2e726aa40 100644 --- a/packages/axes/src/components/AxisTick.tsx +++ b/packages/axes/src/components/AxisTick.tsx @@ -13,6 +13,7 @@ const AxisTick = ({ onClick, textBaseline, textAnchor, + truncateTickAt, animatedProps, }: AxisTickProps) => { const theme = useTheme() @@ -34,6 +35,14 @@ const AxisTick = ({ } }, [animatedProps.opacity, onClick, value]) + const truncateTick = () => { + const valueLength = String(value).length + if (truncateTickAt && truncateTickAt > 0 && valueLength > truncateTickAt) { + return `${String(value).slice(0, truncateTickAt).concat('...')}` + } + return `${value}` + } + return ( @@ -47,7 +56,7 @@ const AxisTick = ({ stroke={textStyle.outlineColor} strokeLinejoin="round" > - {`${value}`} + {truncateTick()} )} ({ transform={animatedProps.textTransform} style={textStyle} > - {`${value}`} + {truncateTick()} ) diff --git a/packages/axes/src/types.ts b/packages/axes/src/types.ts index ddb8e114a..e9301c0f5 100644 --- a/packages/axes/src/types.ts +++ b/packages/axes/src/types.ts @@ -21,8 +21,6 @@ export type AxisLegendPosition = 'start' | 'middle' | 'end' export type ValueFormatter = (value: Value) => Value | string -export type TickRotationOnLength = { angle: number | undefined; length: number | undefined } - export interface AxisProps { ticksPosition?: 'before' | 'after' tickValues?: TicksSpec @@ -30,7 +28,7 @@ export interface AxisProps { tickPadding?: number tickRotation?: number format?: string | ValueFormatter - rotateOnTickLength?: TickRotationOnLength + truncateTickAt?: number renderTick?: (props: AxisTickProps) => JSX.Element legend?: React.ReactNode legendPosition?: AxisLegendPosition @@ -62,6 +60,7 @@ export interface AxisTickProps { textTransform: string transform: string }> + truncateTickAt: number | undefined onClick?: (event: React.MouseEvent, value: Value | string) => void } diff --git a/website/src/lib/chart-properties/axes.ts b/website/src/lib/chart-properties/axes.ts index 56f2dfd4e..abf45fc3a 100644 --- a/website/src/lib/chart-properties/axes.ts +++ b/website/src/lib/chart-properties/axes.ts @@ -87,6 +87,18 @@ export const axes = ({ max: 90, }, }, + { + key: `truncateTickAt`, + flavors, + help: `${axisKey} prevent the tick from overlapping truncating it`, + type: 'number', + required: false, + control: { + type: 'range', + min: 0, + max: 100, + }, + }, { key: `legend`, flavors, @@ -98,7 +110,7 @@ export const axes = ({ key: `legendOffset`, flavors, help: `${axisKey} axis legend offset from axis.`, - type: 'number', + type: 'object', control: { type: 'range', unit: 'px', @@ -106,18 +118,6 @@ export const axes = ({ max: 60, }, }, - { - key: `rotateTickOnLength`, - flavors, - help: `${axisKey} prevent the tick to overlap rotating when length overpassing certain length`, - type: '{angle: number, length: number }', - control: { - type: 'angle', - start: 90, - min: -90, - max: 90, - }, - }, ], }, }, diff --git a/website/src/pages/bar/canvas.js b/website/src/pages/bar/canvas.js index c0a543bd1..398521c0d 100644 --- a/website/src/pages/bar/canvas.js +++ b/website/src/pages/bar/canvas.js @@ -53,6 +53,7 @@ const initialProperties = { tickRotation: 0, legend: '', legendOffset: 36, + truncateTickAt: 0, }, axisRight: { enable: false, @@ -61,6 +62,7 @@ const initialProperties = { tickRotation: 0, legend: '', legendOffset: 0, + truncateTickAt: 0, }, axisBottom: { enable: true, @@ -70,6 +72,7 @@ const initialProperties = { legend: 'country', legendPosition: 'middle', legendOffset: 36, + truncateTickAt: 0, }, axisLeft: { enable: true, @@ -79,6 +82,7 @@ const initialProperties = { legend: 'food', legendPosition: 'middle', legendOffset: -40, + truncateTickAt: 0, }, enableGridX: true, diff --git a/website/src/pages/bar/index.js b/website/src/pages/bar/index.js index 1bc9ad0d5..74254db8e 100644 --- a/website/src/pages/bar/index.js +++ b/website/src/pages/bar/index.js @@ -71,6 +71,7 @@ const initialProperties = { tickRotation: 0, legend: '', legendOffset: 36, + truncateTickAt: 0, }, axisRight: { enable: false, @@ -79,6 +80,7 @@ const initialProperties = { tickRotation: 0, legend: '', legendOffset: 0, + truncateTickAt: 0, }, axisBottom: { enable: true, @@ -88,6 +90,7 @@ const initialProperties = { legend: 'country', legendPosition: 'middle', legendOffset: 32, + truncateTickAt: 0, }, axisLeft: { enable: true, @@ -97,6 +100,7 @@ const initialProperties = { legend: 'food', legendPosition: 'middle', legendOffset: -40, + truncateTickAt: 0, }, enableGridX: false, From e9478ab898a51aa90d8157655962c6c377e740f6 Mon Sep 17 00:00:00 2001 From: Sebastian Quiroga Date: Fri, 28 Jul 2023 20:46:49 -0500 Subject: [PATCH 6/7] Correct approach to truncate tick value + Unit tests --- packages/axes/src/components/Axis.tsx | 1 + packages/axes/src/components/AxisTick.tsx | 12 +--- packages/axes/src/compute.ts | 38 ++++++++++--- packages/axes/tests/compute.test.tsx | 67 +++++++++++++++++++++++ 4 files changed, 100 insertions(+), 18 deletions(-) diff --git a/packages/axes/src/components/Axis.tsx b/packages/axes/src/components/Axis.tsx index 69119e6fc..4d0327874 100644 --- a/packages/axes/src/components/Axis.tsx +++ b/packages/axes/src/components/Axis.tsx @@ -47,6 +47,7 @@ export const NonMemoizedAxis = ({ tickSize, tickPadding, tickRotation, + truncateTickAt, }) let legendNode = null diff --git a/packages/axes/src/components/AxisTick.tsx b/packages/axes/src/components/AxisTick.tsx index 2e726aa40..e6bd6ce11 100644 --- a/packages/axes/src/components/AxisTick.tsx +++ b/packages/axes/src/components/AxisTick.tsx @@ -35,14 +35,6 @@ const AxisTick = ({ } }, [animatedProps.opacity, onClick, value]) - const truncateTick = () => { - const valueLength = String(value).length - if (truncateTickAt && truncateTickAt > 0 && valueLength > truncateTickAt) { - return `${String(value).slice(0, truncateTickAt).concat('...')}` - } - return `${value}` - } - return ( @@ -56,7 +48,7 @@ const AxisTick = ({ stroke={textStyle.outlineColor} strokeLinejoin="round" > - {truncateTick()} + {`${value}`} )} ({ transform={animatedProps.textTransform} style={textStyle} > - {truncateTick()} + {`${value}`} ) diff --git a/packages/axes/src/compute.ts b/packages/axes/src/compute.ts index 04a48b79c..44c6e9caa 100644 --- a/packages/axes/src/compute.ts +++ b/packages/axes/src/compute.ts @@ -2,7 +2,14 @@ import { timeFormat } from 'd3-time-format' import { format as d3Format } from 'd3-format' // @ts-ignore import { textPropsByEngine } from '@nivo/core' -import { ScaleValue, AnyScale, TicksSpec, getScaleTicks, centerScale } from '@nivo/scales' +import { + ScaleValue, + AnyScale, + TicksSpec, + getScaleTicks, + centerScale, + StringValue, +} from '@nivo/scales' import { Point, ValueFormatter, Line } from './types' const isArray = (value: unknown): value is T[] => Array.isArray(value) @@ -15,6 +22,7 @@ export const computeCartesianTicks = ({ tickSize, tickPadding, tickRotation, + truncateTickAt, engine = 'svg', }: { axis: 'x' | 'y' @@ -24,6 +32,7 @@ export const computeCartesianTicks = ({ tickSize: number tickPadding: number tickRotation: number + truncateTickAt?: number engine?: 'svg' | 'canvas' }) => { const values = getScaleTicks(scale, tickValues) @@ -79,13 +88,26 @@ export const computeCartesianTicks = ({ } } - const ticks = values.map((value: Value) => ({ - key: value instanceof Date ? `${value.valueOf()}` : `${value}`, - value, - ...translate(value), - ...line, - ...text, - })) + const truncateTick = (value: string) => { + const valueLength = String(value).length + + if (truncateTickAt && truncateTickAt > 0 && valueLength > truncateTickAt) { + return `${String(value).slice(0, truncateTickAt).concat('...')}` + } + return `${value}` + } + + const ticks = values.map((value: Value) => { + const processedValue = + typeof value === 'string' ? (truncateTick(value) as unknown as Value) : value + return { + key: value instanceof Date ? `${value.valueOf()}` : `${value}`, + value: processedValue, + ...translate(value), + ...line, + ...text, + } + }) return { ticks, diff --git a/packages/axes/tests/compute.test.tsx b/packages/axes/tests/compute.test.tsx index e1f3bae25..baa80f6b6 100644 --- a/packages/axes/tests/compute.test.tsx +++ b/packages/axes/tests/compute.test.tsx @@ -5,6 +5,18 @@ import { computeCartesianTicks } from '../src/compute' describe('computeCartesianTicks()', () => { const ordinalScale = scaleOrdinal([0, 10, 20, 30]).domain(['A', 'B', 'C', 'D']) + const ordinalScaleForTruncationText = scaleOrdinal([0, 10, 20, 30]).domain([ + 'Colombia', + 'England', + 'Australia', + 'France', + ]) + const ordinalScaleForTruncationWithNoText = scaleOrdinal([0, 10, 20, 30]).domain([ + new Date(), + 1234, + false, + {}, + ]) const pointScale = castPointScale(scalePoint().domain(['E', 'F', 'G', 'H']).range([0, 300])) const bandScale = castBandScale(scaleBand().domain(['I', 'J', 'K', 'L']).rangeRound([0, 400])) const linearScale = castLinearScale(scaleLinear().domain([0, 500]).range([0, 100]), false) @@ -92,6 +104,61 @@ describe('computeCartesianTicks()', () => { }) }) + describe('from ordinal scale with text for value truncation', () => { + it('should truncate tick value for x axis', () => { + const axis = computeCartesianTicks({ + scale: ordinalScaleForTruncationText, + tickValues: 1, + axis: 'x', + ticksPosition: 'after', + tickSize: 10, + tickPadding: 5, + tickRotation: 0, + truncateTickAt: 2, + }) + expect(axis.ticks[0].value).toBe('Co...') + expect(axis.ticks[1].value).toBe('En...') + expect(axis.ticks[2].value).toBe('Au...') + expect(axis.ticks[3].value).toBe('Fr...') + }) + it('should truncate tick value for y axis', () => { + const axis = computeCartesianTicks({ + scale: ordinalScaleForTruncationText, + tickValues: 1, + axis: 'y', + ticksPosition: 'after', + tickSize: 10, + tickPadding: 5, + tickRotation: 0, + truncateTickAt: 3, + }) + expect(axis.ticks[0].value).toBe('Col...') + expect(axis.ticks[1].value).toBe('Eng...') + expect(axis.ticks[2].value).toBe('Aus...') + expect(axis.ticks[3].value).toBe('Fra...') + }) + }) + + describe('from ordinal scale with NO text for value truncation', () => { + it('should NOT truncate tick', () => { + const axis = computeCartesianTicks({ + scale: ordinalScaleForTruncationWithNoText, + tickValues: 1, + axis: 'y', + ticksPosition: 'after', + tickSize: 10, + tickPadding: 5, + tickRotation: 0, + truncateTickAt: 3, + }) + console.info(axis) + expect(axis.ticks[0].value).not.toContain('...') + expect(axis.ticks[1].value).not.toContain('...') + expect(axis.ticks[2].value).not.toContain('...') + expect(axis.ticks[3].value).not.toContain('...') + }) + }) + describe('from point scale', () => { it('should compute ticks for x axis', () => { expect( From 4c996bf08da6733363cca26560fc6331518f2bc8 Mon Sep 17 00:00:00 2001 From: Sebastian Quiroga Date: Tue, 1 Aug 2023 11:06:21 -0500 Subject: [PATCH 7/7] Removed unnecessary imports --- packages/axes/src/components/AxisTick.tsx | 1 - packages/axes/src/compute.ts | 9 +-------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/axes/src/components/AxisTick.tsx b/packages/axes/src/components/AxisTick.tsx index e6bd6ce11..bbd887d24 100644 --- a/packages/axes/src/components/AxisTick.tsx +++ b/packages/axes/src/components/AxisTick.tsx @@ -13,7 +13,6 @@ const AxisTick = ({ onClick, textBaseline, textAnchor, - truncateTickAt, animatedProps, }: AxisTickProps) => { const theme = useTheme() diff --git a/packages/axes/src/compute.ts b/packages/axes/src/compute.ts index 44c6e9caa..2a01e9693 100644 --- a/packages/axes/src/compute.ts +++ b/packages/axes/src/compute.ts @@ -2,14 +2,7 @@ import { timeFormat } from 'd3-time-format' import { format as d3Format } from 'd3-format' // @ts-ignore import { textPropsByEngine } from '@nivo/core' -import { - ScaleValue, - AnyScale, - TicksSpec, - getScaleTicks, - centerScale, - StringValue, -} from '@nivo/scales' +import { ScaleValue, AnyScale, TicksSpec, getScaleTicks, centerScale } from '@nivo/scales' import { Point, ValueFormatter, Line } from './types' const isArray = (value: unknown): value is T[] => Array.isArray(value)