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

Feature/bar chart truncate axes ticks #2405

Merged
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/axes/src/components/Axes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const Axes = memo(
scale={isXAxis ? xScale : yScale}
length={isXAxis ? width : height}
ticksPosition={ticksPosition}
truncateTickAt={axis.truncateTickAt}
/>
)
})}
Expand Down
25 changes: 15 additions & 10 deletions packages/axes/src/components/Axis.tsx
Original file line number Diff line number Diff line change
@@ -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 = <Value extends ScaleValue>({
axis,
Expand All @@ -20,6 +20,7 @@ export const NonMemoizedAxis = <Value extends ScaleValue>({
tickRotation = 0,
format,
renderTick = AxisTick,
truncateTickAt,
legend,
legendPosition = 'end',
legendOffset = 0,
Expand All @@ -46,6 +47,7 @@ export const NonMemoizedAxis = <Value extends ScaleValue>({
tickSize,
tickPadding,
tickRotation,
truncateTickAt,
})

let legendNode = null
Expand Down Expand Up @@ -122,11 +124,13 @@ export const NonMemoizedAxis = <Value extends ScaleValue>({
})

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(
Expand Down Expand Up @@ -163,6 +167,7 @@ export const NonMemoizedAxis = <Value extends ScaleValue>({
rotate: tickRotation,
textBaseline,
textAnchor: textAlign,
truncateTickAt: truncateTickAt,
animatedProps: transitionProps,
...tick,
...(onClick ? { onClick } : {}),
Expand Down
29 changes: 22 additions & 7 deletions packages/axes/src/compute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const computeCartesianTicks = <Value extends ScaleValue>({
tickSize,
tickPadding,
tickRotation,
truncateTickAt,
engine = 'svg',
}: {
axis: 'x' | 'y'
Expand All @@ -24,6 +25,7 @@ export const computeCartesianTicks = <Value extends ScaleValue>({
tickSize: number
tickPadding: number
tickRotation: number
truncateTickAt?: number
engine?: 'svg' | 'canvas'
}) => {
const values = getScaleTicks<Value>(scale, tickValues)
Expand Down Expand Up @@ -79,13 +81,26 @@ export const computeCartesianTicks = <Value extends ScaleValue>({
}
}

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,
Expand Down
4 changes: 4 additions & 0 deletions packages/axes/src/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion packages/axes/src/types.ts
Original file line number Diff line number Diff line change
@@ -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> = T extends number
? number[]
Expand Down Expand Up @@ -28,6 +28,7 @@ export interface AxisProps<Value extends ScaleValue = any> {
tickPadding?: number
tickRotation?: number
format?: string | ValueFormatter<Value>
truncateTickAt?: number
renderTick?: (props: AxisTickProps<Value>) => JSX.Element
legend?: React.ReactNode
legendPosition?: AxisLegendPosition
Expand Down Expand Up @@ -59,6 +60,7 @@ export interface AxisTickProps<Value extends ScaleValue> {
textTransform: string
transform: string
}>
truncateTickAt: number | undefined
onClick?: (event: React.MouseEvent<SVGGElement, MouseEvent>, value: Value | string) => void
}

Expand Down
67 changes: 67 additions & 0 deletions packages/axes/tests/compute.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand Down
14 changes: 13 additions & 1 deletion website/src/lib/chart-properties/axes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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',
Expand Down
4 changes: 4 additions & 0 deletions website/src/pages/bar/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const initialProperties = {
tickRotation: 0,
legend: '',
legendOffset: 36,
truncateTickAt: 0,
},
axisRight: {
enable: false,
Expand All @@ -61,6 +62,7 @@ const initialProperties = {
tickRotation: 0,
legend: '',
legendOffset: 0,
truncateTickAt: 0,
},
axisBottom: {
enable: true,
Expand All @@ -70,6 +72,7 @@ const initialProperties = {
legend: 'country',
legendPosition: 'middle',
legendOffset: 36,
truncateTickAt: 0,
},
axisLeft: {
enable: true,
Expand All @@ -79,6 +82,7 @@ const initialProperties = {
legend: 'food',
legendPosition: 'middle',
legendOffset: -40,
truncateTickAt: 0,
},

enableGridX: true,
Expand Down
4 changes: 4 additions & 0 deletions website/src/pages/bar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const initialProperties = {
tickRotation: 0,
legend: '',
legendOffset: 36,
truncateTickAt: 0,
},
axisRight: {
enable: false,
Expand All @@ -79,6 +80,7 @@ const initialProperties = {
tickRotation: 0,
legend: '',
legendOffset: 0,
truncateTickAt: 0,
},
axisBottom: {
enable: true,
Expand All @@ -88,6 +90,7 @@ const initialProperties = {
legend: 'country',
legendPosition: 'middle',
legendOffset: 32,
truncateTickAt: 0,
},
axisLeft: {
enable: true,
Expand All @@ -97,6 +100,7 @@ const initialProperties = {
legend: 'food',
legendPosition: 'middle',
legendOffset: -40,
truncateTickAt: 0,
},

enableGridX: false,
Expand Down