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 27 commits
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
42 changes: 30 additions & 12 deletions packages/bar/src/Bar.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,4 @@
import { Axes, Grid } from '@nivo/axes'
import { BarAnnotations } from './BarAnnotations'
import {
BarCustomLayerProps,
BarDatum,
BarLayer,
BarLayerId,
BarSvgProps,
ComputedBarDatumWithValue,
} from './types'
import { BarLegends } from './BarLegends'
import {
CartesianMarkers,
Container,
Expand All @@ -18,10 +8,21 @@ import {
useDimensions,
useMotionConfig,
} from '@nivo/core'
import { Fragment, ReactNode, createElement, useMemo } from 'react'
import { svgDefaultProps } from './props'
import { useTransition } from '@react-spring/web'
import { Fragment, ReactNode, createElement, useMemo } from 'react'
import { BarAnnotations } from './BarAnnotations'
import { BarLegends } from './BarLegends'
import { useBar } from './hooks'
import { svgDefaultProps } from './props'
import {
BarCustomLayerProps,
BarDatum,
BarLayer,
BarLayerId,
BarSvgProps,
ComputedBarDatumWithValue,
} from './types'
import { BarTotals } from './BarTotals'

type InnerBarProps<RawDatum extends BarDatum> = Omit<
BarSvgProps<RawDatum>,
Expand Down Expand Up @@ -102,6 +103,9 @@ const InnerBar = <RawDatum extends BarDatum>({
barAriaDescribedBy,

initialHiddenIds,

enableTotals = svgDefaultProps.enableTotals,
totalsOffset = svgDefaultProps.totalsOffset,
}: InnerBarProps<RawDatum>) => {
const { animate, config: springConfig } = useMotionConfig()
const { outerWidth, outerHeight, margin, innerWidth, innerHeight } = useDimensions(
Expand All @@ -122,6 +126,7 @@ const InnerBar = <RawDatum extends BarDatum>({
shouldRenderBarLabel,
toggleSerie,
legendsWithData,
barTotals,
} = useBar<RawDatum>({
indexBy,
label,
Expand Down Expand Up @@ -151,6 +156,7 @@ const InnerBar = <RawDatum extends BarDatum>({
legends,
legendLabel,
initialHiddenIds,
totalsOffset,
})

const transition = useTransition<
Expand Down Expand Up @@ -283,6 +289,7 @@ const InnerBar = <RawDatum extends BarDatum>({
grid: null,
legends: null,
markers: null,
totals: null,
}

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

if (layers.includes('totals') && enableTotals) {
layerById.totals = (
<BarTotals
data={barTotals}
springConfig={springConfig}
animate={animate}
layout={layout}
/>
)
}

const layerContext: BarCustomLayerProps<RawDatum> = useMemo(
() => ({
...commonProps,
Expand Down
32 changes: 32 additions & 0 deletions packages/bar/src/BarCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@ import {
BarCanvasCustomLayerProps,
BarCanvasLayer,
BarCanvasProps,
BarCommonProps,
BarDatum,
ComputedBarDatum,
} from './types'
import {
CompleteTheme,
Container,
Margin,
getRelativeCursor,
isCursorInRect,
useDimensions,
useTheme,
useValueFormatter,
} from '@nivo/core'
import {
ForwardedRef,
Expand All @@ -32,6 +35,7 @@ import { renderAxesToCanvas, renderGridLinesToCanvas } from '@nivo/axes'
import { renderLegendToCanvas } from '@nivo/legends'
import { useTooltip } from '@nivo/tooltip'
import { useBar } from './hooks'
import { BarTotalsData } from './compute/totals'

type InnerBarCanvasProps<RawDatum extends BarDatum> = Omit<
BarCanvasProps<RawDatum>,
Expand All @@ -52,6 +56,22 @@ const findBarUnderCursor = <RawDatum,>(

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

function renderTotalsToCanvas<RawDatum extends BarDatum>(
ctx: CanvasRenderingContext2D,
barTotals: BarTotalsData[],
theme: CompleteTheme,
layout: BarCommonProps<RawDatum>['layout'] = canvasDefaultProps.layout
) {
ctx.fillStyle = theme.text.fill
ctx.font = `bold ${theme.labels.text.fontSize}px ${theme.labels.text.fontFamily}`
ctx.textBaseline = layout === 'vertical' ? 'alphabetic' : 'middle'
ctx.textAlign = layout === 'vertical' ? 'center' : 'start'

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

const InnerBarCanvas = <RawDatum extends BarDatum>({
data,
indexBy,
Expand Down Expand Up @@ -166,6 +186,9 @@ const InnerBarCanvas = <RawDatum extends BarDatum>({
pixelRatio = canvasDefaultProps.pixelRatio,

canvasRef,

enableTotals = canvasDefaultProps.enableTotals,
totalsOffset = canvasDefaultProps.totalsOffset,
}: InnerBarCanvasProps<RawDatum>) => {
const canvasEl = useRef<HTMLCanvasElement | null>(null)

Expand All @@ -187,6 +210,7 @@ const InnerBarCanvas = <RawDatum extends BarDatum>({
getLabelColor,
shouldRenderBarLabel,
legendsWithData,
barTotals,
} = useBar<RawDatum>({
indexBy,
label,
Expand Down Expand Up @@ -215,6 +239,7 @@ const InnerBarCanvas = <RawDatum extends BarDatum>({
labelSkipHeight,
legends,
legendLabel,
totalsOffset,
})

const { showTooltipFromEvent, hideTooltip } = useTooltip()
Expand Down Expand Up @@ -285,6 +310,8 @@ const InnerBarCanvas = <RawDatum extends BarDatum>({
]
)

const formatValue = useValueFormatter(valueFormat)
plouc marked this conversation as resolved.
Show resolved Hide resolved

useEffect(() => {
const ctx = canvasEl.current?.getContext('2d')

Expand Down Expand Up @@ -362,6 +389,8 @@ const InnerBarCanvas = <RawDatum extends BarDatum>({
})
} else if (layer === 'annotations') {
renderAnnotationsToCanvas(ctx, { annotations: boundAnnotations, theme })
} else if (layer === 'totals' && enableTotals) {
renderTotalsToCanvas(ctx, barTotals, theme, layout)
} else if (typeof layer === 'function') {
layer(ctx, layerContext)
}
Expand Down Expand Up @@ -404,6 +433,9 @@ const InnerBarCanvas = <RawDatum extends BarDatum>({
shouldRenderBarLabel,
theme,
width,
barTotals,
enableTotals,
formatValue,
])

const handleMouseHover = useCallback(
Expand Down
75 changes: 75 additions & 0 deletions packages/bar/src/BarTotals.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useTheme } from '@nivo/core'
import { AnimationConfig, animated, useTransition } from '@react-spring/web'
import { BarCommonProps, BarDatum } from './types'
import { svgDefaultProps } from './props'
import { BarTotalsData } from './compute/totals'

interface Props<RawDatum extends BarDatum> {
data: BarTotalsData[]
springConfig: Partial<AnimationConfig>
animate: boolean
layout?: BarCommonProps<RawDatum>['layout']
}

export const BarTotals = <RawDatum extends BarDatum>({
data,
springConfig,
animate,
layout = svgDefaultProps.layout,
}: Props<RawDatum>) => {
const theme = useTheme()
const totalsTransition = useTransition<
BarTotalsData,
{
x: number
y: number
labelOpacity: number
}
>(data, {
keys: barTotal => barTotal.key,
from: barTotal => ({
x: layout === 'vertical' ? barTotal.x : barTotal.animationOffset,
y: layout === 'vertical' ? barTotal.animationOffset : barTotal.y,
labelOpacity: 0,
}),
enter: barTotal => ({
x: barTotal.x,
y: barTotal.y,
labelOpacity: 1,
}),
update: barTotal => ({
x: barTotal.x,
y: barTotal.y,
labelOpacity: 1,
}),
leave: barTotal => ({
x: layout === 'vertical' ? barTotal.x : barTotal.animationOffset,
y: layout === 'vertical' ? barTotal.animationOffset : barTotal.y,
labelOpacity: 0,
}),
config: springConfig,
immediate: !animate,
initial: animate ? undefined : null,
})

return totalsTransition((style, barTotal) => (
<animated.text
key={barTotal.key}
x={style.x}
y={style.y}
fillOpacity={style.labelOpacity}
style={{
...theme.labels.text,
pointerEvents: 'none',
fill: theme.text.fill,
}}
fontWeight="bold"
fontSize={theme.labels.text.fontSize}
fontFamily={theme.labels.text.fontFamily}
textAnchor={layout === 'vertical' ? 'middle' : 'start'}
alignmentBaseline={layout === 'vertical' ? 'alphabetic' : 'middle'}
>
{barTotal.value}
</animated.text>
))
}
Loading
Loading