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

feat(Bar): add bar totals #2525

Merged
merged 28 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2c7d970
feat: add bar totals layer
joaopedromatias Mar 2, 2024
b3a81c7
fix: insert unique key prop to each total
joaopedromatias Mar 2, 2024
485c661
test: insert tests to totals bar layer
joaopedromatias Mar 2, 2024
418f871
docs: insert recipe on website bar page
joaopedromatias Mar 2, 2024
fe284c1
fix: update scale functions types
joaopedromatias Mar 4, 2024
d632462
refactor: enable totals by prop instead of directly on layers
joaopedromatias Mar 5, 2024
a40f8cf
style: apply theme configuration on totals
joaopedromatias Mar 5, 2024
0b97fcb
feat: make totals offsets configurable
joaopedromatias Mar 5, 2024
15beb01
refactor: centralize compute of totals bar
joaopedromatias Mar 5, 2024
7bea4a1
chore: re-format website docs yml
joaopedromatias Mar 5, 2024
eeefd62
refactor: use props along with layers to enable totals
joaopedromatias Mar 5, 2024
4d12cdc
fix: remove unnused variable
joaopedromatias Mar 5, 2024
e3edd63
style: add transitions to bar totals
joaopedromatias Mar 6, 2024
16bd568
test: update tests to find totals component
joaopedromatias Mar 6, 2024
84e4369
docs: add enable totals docs on website
joaopedromatias Mar 6, 2024
fcf3194
refactor: use totals computed value through hook on canvas
joaopedromatias Mar 6, 2024
b582221
fix: remove unnused var
joaopedromatias Mar 6, 2024
515eb69
fix: add enableTotals prop to default bar props on website
joaopedromatias Mar 6, 2024
ad8806b
docs(website): add default enableTotals prop to canvas and svg flavors
joaopedromatias Mar 6, 2024
b3cd571
style: align total label text based on layout mode
joaopedromatias Mar 6, 2024
49e30e0
feat: add value format to totals labels
joaopedromatias Mar 7, 2024
81b1756
refactor: configure totals transition inside its component
joaopedromatias Mar 8, 2024
66eb0f1
refactor: format value in totals compute function
joaopedromatias Mar 8, 2024
3050350
style: prevent overlap on zero totals offset
joaopedromatias Mar 8, 2024
f3c0876
types: remove optional syntax
joaopedromatias Mar 8, 2024
f621950
feat: animation offset is calculated individually by index
joaopedromatias Mar 8, 2024
ab67405
chore: change order of initializing default layers
joaopedromatias Mar 13, 2024
999ba05
refactor: add numeric value to bar totals data
joaopedromatias Mar 19, 2024
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
32 changes: 7 additions & 25 deletions packages/bar/src/Bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { svgDefaultProps } from './props'
import { useTransition } from '@react-spring/web'
import { useBar } from './hooks'
import { computeBarTotals } from './compute/totals'
import { BarTotals } from './BarTotals'

type InnerBarProps<RawDatum extends BarDatum> = Omit<
BarSvgProps<RawDatum>,
Expand Down Expand Up @@ -289,6 +290,7 @@ const InnerBar = <RawDatum extends BarDatum>({
grid: null,
legends: null,
markers: null,
totals: null,
}

if (layers.includes('annotations')) {
Expand Down Expand Up @@ -368,6 +370,11 @@ const InnerBar = <RawDatum extends BarDatum>({
)
}

if (layers.includes('totals') && enableTotals) {
const barTotals = computeBarTotals(bars, xScale, yScale, layout, groupMode, totalsOffset)
plouc marked this conversation as resolved.
Show resolved Hide resolved
layerById.totals = <BarTotals barTotals={barTotals} />
}

const layerContext: BarCustomLayerProps<RawDatum> = useMemo(
() => ({
...commonProps,
Expand Down Expand Up @@ -407,16 +414,6 @@ const InnerBar = <RawDatum extends BarDatum>({
]
)

const barTotals = computeBarTotals(
enableTotals,
bars,
xScale,
yScale,
layout,
groupMode,
totalsOffset
)

return (
<SvgWrapper
width={outerWidth}
Expand All @@ -436,21 +433,6 @@ const InnerBar = <RawDatum extends BarDatum>({

return layerById?.[layer] ?? null
})}
{barTotals.map(barTotal => (
<text
key={barTotal.key}
x={barTotal.x}
y={barTotal.y}
fill={theme.text.fill}
fontSize={theme.text.fontSize}
fontWeight="bold"
textAnchor="middle"
alignmentBaseline="middle"
data-test="bar-total"
>
{barTotal.value}
</text>
))}
</SvgWrapper>
)
}
Expand Down
47 changes: 27 additions & 20 deletions packages/bar/src/BarCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ComputedBarDatum,
} from './types'
import {
CompleteTheme,
Container,
Margin,
getRelativeCursor,
Expand All @@ -32,7 +33,7 @@ import { renderAxesToCanvas, renderGridLinesToCanvas } from '@nivo/axes'
import { renderLegendToCanvas } from '@nivo/legends'
import { useTooltip } from '@nivo/tooltip'
import { useBar } from './hooks'
import { computeBarTotals } from './compute/totals'
import { BarTotalsData, computeBarTotals } from './compute/totals'

type InnerBarCanvasProps<RawDatum extends BarDatum> = Omit<
BarCanvasProps<RawDatum>,
Expand All @@ -53,6 +54,21 @@ const findBarUnderCursor = <RawDatum,>(

const isNumber = (value: unknown): value is number => typeof value === 'number'

function renderTotalsToCanvas(
ctx: CanvasRenderingContext2D,
barTotals: BarTotalsData[],
theme: CompleteTheme
) {
ctx.fillStyle = theme.text.fill
ctx.font = `bold ${theme.labels.text.fontSize}px ${theme.labels.text.fontFamily}`
ctx.textBaseline = 'middle'
ctx.textAlign = 'center'

barTotals.forEach(barTotal => {
ctx.fillText(String(barTotal.value), barTotal.x, barTotal.y)
})
}

const InnerBarCanvas = <RawDatum extends BarDatum>({
data,
indexBy,
Expand Down Expand Up @@ -366,30 +382,21 @@ const InnerBarCanvas = <RawDatum extends BarDatum>({
})
} else if (layer === 'annotations') {
renderAnnotationsToCanvas(ctx, { annotations: boundAnnotations, theme })
} else if (layer === 'totals' && enableTotals) {
const barTotals = computeBarTotals(
bars,
xScale,
yScale,
layout,
groupMode,
totalsOffset
)
renderTotalsToCanvas(ctx, barTotals, theme)
} else if (typeof layer === 'function') {
layer(ctx, layerContext)
}
})

const barTotals = computeBarTotals(
enableTotals,
bars,
xScale,
yScale,
layout,
groupMode,
totalsOffset
)

ctx.fillStyle = theme.text.fill
ctx.font = `bold ${theme.text.fontSize}px sans-serif`
ctx.textBaseline = 'middle'
ctx.textAlign = 'center'

barTotals.forEach(barTotal => {
ctx.fillText(String(barTotal.value), barTotal.x, barTotal.y)
})

ctx.save()
}, [
axisBottom,
Expand Down
29 changes: 29 additions & 0 deletions packages/bar/src/BarTotals.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useTheme } from '@nivo/core'
import { BarTotalsData } from './compute/totals'

interface Props {
barTotals: BarTotalsData[]
}

export const BarTotals = ({ barTotals }: Props) => {
plouc marked this conversation as resolved.
Show resolved Hide resolved
const theme = useTheme()
return (
<>
{barTotals.map(barTotal => (
<text
key={barTotal.key}
x={barTotal.x}
y={barTotal.y}
fill={theme.text.fill}
fontSize={theme.labels.text.fontSize}
fontFamily={theme.labels.text.fontFamily}
fontWeight="bold"
textAnchor="middle"
alignmentBaseline="middle"
>
{barTotal.value}
</text>
))}
</>
)
}
5 changes: 2 additions & 3 deletions packages/bar/src/compute/totals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import { AnyScale, ScaleBand } from '@nivo/scales'
import { defaultProps } from '../props'
import { BarCommonProps, BarDatum, ComputedBarDatum } from '../types'

interface BarTotalsData {
export interface BarTotalsData {
key: string
x: number
y: number
value: number
}

export const computeBarTotals = <RawDatum extends BarDatum>(
enableTotals: boolean,
bars: ComputedBarDatum<RawDatum>[],
xScale: ScaleBand<string> | AnyScale,
yScale: ScaleBand<string> | AnyScale,
Expand All @@ -20,7 +19,7 @@ export const computeBarTotals = <RawDatum extends BarDatum>(
) => {
const totals = [] as BarTotalsData[]

if (!enableTotals || bars.length === 0) return totals
if (bars.length === 0) return totals

const totalsByIndex = new Map<string | number, number>()

Expand Down
1 change: 1 addition & 0 deletions packages/bar/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './Bar'
export * from './BarItem'
export * from './BarTooltip'
export * from './BarCanvas'
export * from './BarTotals'
export * from './ResponsiveBar'
export * from './ResponsiveBarCanvas'
export * from './props'
Expand Down
4 changes: 2 additions & 2 deletions packages/bar/src/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const defaultProps = {

export const svgDefaultProps = {
...defaultProps,
layers: ['grid', 'axes', 'bars', 'markers', 'legends', 'annotations'] as BarLayerId[],
layers: ['grid', 'axes', 'bars', 'markers', 'legends', 'annotations', 'totals'] as BarLayerId[],
plouc marked this conversation as resolved.
Show resolved Hide resolved
barComponent: BarItem,

defs: [],
Expand All @@ -69,7 +69,7 @@ export const svgDefaultProps = {

export const canvasDefaultProps = {
...defaultProps,
layers: ['grid', 'axes', 'bars', 'legends', 'annotations'] as BarCanvasLayerId[],
layers: ['grid', 'axes', 'bars', 'legends', 'annotations', 'totals'] as BarCanvasLayerId[],
plouc marked this conversation as resolved.
Show resolved Hide resolved

pixelRatio: typeof window !== 'undefined' ? window.devicePixelRatio ?? 1 : 1,
}
2 changes: 1 addition & 1 deletion packages/bar/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export interface BarLegendProps extends LegendProps {
export type LabelFormatter = (label: string | number) => string | number
export type ValueFormatter = (value: number) => string | number

export type BarLayerId = 'grid' | 'axes' | 'bars' | 'markers' | 'legends' | 'annotations'
export type BarLayerId = 'grid' | 'axes' | 'bars' | 'markers' | 'legends' | 'annotations' | 'totals'
export type BarCanvasLayerId = Exclude<BarLayerId, 'markers'>

interface BarCustomLayerBaseProps<RawDatum>
Expand Down
50 changes: 18 additions & 32 deletions packages/bar/tests/Bar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { mount } from 'enzyme'
import { create, act, ReactTestRenderer, type ReactTestInstance } from 'react-test-renderer'
import { LegendSvg, LegendSvgItem } from '@nivo/legends'
import { Bar, BarDatum, BarItemProps, ComputedDatum, BarItem, BarTooltip } from '../'
import { Bar, BarDatum, BarItemProps, ComputedDatum, BarItem, BarTooltip, BarTotals } from '../'

type IdValue = {
id: string
Expand Down Expand Up @@ -631,12 +631,9 @@ describe('totals layer', () => {
/>
).root

const totals = instance.findAllByType('text').filter(text => {
return text.props['data-test'] === 'bar-total'
})

expect(totals).toHaveLength(3)
totals.forEach((total, index) => {
const totals = instance.findByType(BarTotals)
expect(totals.children).toHaveLength(3)
;(totals.children as ReactTestInstance[]).forEach((total, index) => {
if (index === 0) {
expect(total.children[0]).toBe(`2`)
} else if (index === 1) {
Expand All @@ -663,12 +660,9 @@ describe('totals layer', () => {
/>
).root

const totals = instance.findAllByType('text').filter(text => {
return text.props['data-test'] === 'bar-total'
})

expect(totals).toHaveLength(3)
totals.forEach((total, index) => {
const totals = instance.findByType(BarTotals)
expect(totals.children).toHaveLength(3)
;(totals.children as ReactTestInstance[]).forEach((total, index) => {
if (index === 0) {
expect(total.children[0]).toBe(`2`)
} else if (index === 1) {
Expand All @@ -694,12 +688,9 @@ describe('totals layer', () => {
/>
).root

const totals = instance.findAllByType('text').filter(text => {
return text.props['data-test'] === 'bar-total'
})

expect(totals).toHaveLength(2)
totals.forEach((total, index) => {
const totals = instance.findByType(BarTotals)
expect(totals.children).toHaveLength(2)
;(totals.children as ReactTestInstance[]).forEach((total, index) => {
if (index === 0) {
expect(total.children[0]).toBe(`-2`)
} else {
Expand All @@ -725,13 +716,9 @@ describe('totals layer', () => {
/>
).root

const totals = instance.findAllByType('text').filter(text => {
return text.props['data-test'] === 'bar-total'
})

expect(totals).toHaveLength(3)

totals.forEach((total, index) => {
const totals = instance.findByType(BarTotals)
expect(totals.children).toHaveLength(3)
;(totals.children as ReactTestInstance[]).forEach((total, index) => {
if (index === 0) {
expect(total.children[0]).toBe(`0`)
} else if (index === 1) {
Expand All @@ -751,6 +738,7 @@ describe('totals layer', () => {
text: {
fontSize: 14,
fill: 'red',
fontFamily: 'serif',
},
}}
keys={['value1', 'value2']}
Expand All @@ -763,15 +751,13 @@ describe('totals layer', () => {
/>
).root

const totals = instance.findAllByType('text').filter(text => {
return text.props['data-test'] === 'bar-total'
})

expect(totals).toHaveLength(3)
totals.forEach((total, index) => {
const totals = instance.findByType(BarTotals)
expect(totals.children).toHaveLength(3)
;(totals.children as ReactTestInstance[]).forEach((total, index) => {
const props = total.props
expect(props.fill).toBe('red')
expect(props.fontSize).toBe(14)
expect(props.fontFamily).toBe('serif')
})
})
})
Expand Down