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 6da3ea5bf..4d0327874 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, + truncateTickAt, legend, legendPosition = 'end', legendOffset = 0, @@ -46,6 +47,7 @@ export const NonMemoizedAxis = ({ tickSize, tickPadding, tickRotation, + truncateTickAt, }) let legendNode = null @@ -122,11 +124,13 @@ export const NonMemoizedAxis = ({ }) 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: `translate(${tick.x},${tick.y})`, + textTransform: `translate(${tick.textX},${tick.textY}) rotate(${tickRotation})`, + } + }, [tickRotation] ) const getFromAnimatedProps = useCallback( @@ -163,6 +167,7 @@ export const NonMemoizedAxis = ({ rotate: tickRotation, textBaseline, textAnchor: textAlign, + truncateTickAt: truncateTickAt, animatedProps: transitionProps, ...tick, ...(onClick ? { onClick } : {}), diff --git a/packages/axes/src/compute.ts b/packages/axes/src/compute.ts index 04a48b79c..2a01e9693 100644 --- a/packages/axes/src/compute.ts +++ b/packages/axes/src/compute.ts @@ -15,6 +15,7 @@ export const computeCartesianTicks = ({ tickSize, tickPadding, tickRotation, + truncateTickAt, engine = 'svg', }: { axis: 'x' | 'y' @@ -24,6 +25,7 @@ export const computeCartesianTicks = ({ tickSize: number tickPadding: number tickRotation: number + truncateTickAt?: number engine?: 'svg' | 'canvas' }) => { const values = getScaleTicks(scale, tickValues) @@ -79,13 +81,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/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..e9301c0f5 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[] @@ -28,6 +28,7 @@ export interface AxisProps { tickPadding?: number tickRotation?: number format?: string | ValueFormatter + truncateTickAt?: number renderTick?: (props: AxisTickProps) => JSX.Element legend?: React.ReactNode legendPosition?: AxisLegendPosition @@ -59,6 +60,7 @@ export interface AxisTickProps { textTransform: string transform: string }> + truncateTickAt: number | undefined onClick?: (event: React.MouseEvent, value: Value | string) => void } 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( diff --git a/website/src/lib/chart-properties/axes.ts b/website/src/lib/chart-properties/axes.ts index 41335f510..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', 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,