diff --git a/packages/bar/src/Bar.tsx b/packages/bar/src/Bar.tsx index 11ec1d2a1c..d0dd26ec15 100644 --- a/packages/bar/src/Bar.tsx +++ b/packages/bar/src/Bar.tsx @@ -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, @@ -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 = Omit< BarSvgProps, @@ -102,6 +103,9 @@ const InnerBar = ({ barAriaDescribedBy, initialHiddenIds, + + enableTotals = svgDefaultProps.enableTotals, + totalsOffset = svgDefaultProps.totalsOffset, }: InnerBarProps) => { const { animate, config: springConfig } = useMotionConfig() const { outerWidth, outerHeight, margin, innerWidth, innerHeight } = useDimensions( @@ -122,6 +126,7 @@ const InnerBar = ({ shouldRenderBarLabel, toggleSerie, legendsWithData, + barTotals, } = useBar({ indexBy, label, @@ -151,6 +156,7 @@ const InnerBar = ({ legends, legendLabel, initialHiddenIds, + totalsOffset, }) const transition = useTransition< @@ -283,6 +289,7 @@ const InnerBar = ({ grid: null, legends: null, markers: null, + totals: null, } if (layers.includes('annotations')) { @@ -362,6 +369,17 @@ const InnerBar = ({ ) } + if (layers.includes('totals') && enableTotals) { + layerById.totals = ( + + ) + } + const layerContext: BarCustomLayerProps = useMemo( () => ({ ...commonProps, diff --git a/packages/bar/src/BarCanvas.tsx b/packages/bar/src/BarCanvas.tsx index 7f69196a6c..fefd2a04f2 100644 --- a/packages/bar/src/BarCanvas.tsx +++ b/packages/bar/src/BarCanvas.tsx @@ -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, @@ -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 = Omit< BarCanvasProps, @@ -52,6 +56,22 @@ const findBarUnderCursor = ( const isNumber = (value: unknown): value is number => typeof value === 'number' +function renderTotalsToCanvas( + ctx: CanvasRenderingContext2D, + barTotals: BarTotalsData[], + theme: CompleteTheme, + layout: BarCommonProps['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.formattedValue, barTotal.x, barTotal.y) + }) +} + const InnerBarCanvas = ({ data, indexBy, @@ -166,6 +186,9 @@ const InnerBarCanvas = ({ pixelRatio = canvasDefaultProps.pixelRatio, canvasRef, + + enableTotals = canvasDefaultProps.enableTotals, + totalsOffset = canvasDefaultProps.totalsOffset, }: InnerBarCanvasProps) => { const canvasEl = useRef(null) @@ -187,6 +210,7 @@ const InnerBarCanvas = ({ getLabelColor, shouldRenderBarLabel, legendsWithData, + barTotals, } = useBar({ indexBy, label, @@ -215,6 +239,7 @@ const InnerBarCanvas = ({ labelSkipHeight, legends, legendLabel, + totalsOffset, }) const { showTooltipFromEvent, hideTooltip } = useTooltip() @@ -285,6 +310,8 @@ const InnerBarCanvas = ({ ] ) + const formatValue = useValueFormatter(valueFormat) + useEffect(() => { const ctx = canvasEl.current?.getContext('2d') @@ -362,6 +389,8 @@ const InnerBarCanvas = ({ }) } 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) } @@ -404,6 +433,9 @@ const InnerBarCanvas = ({ shouldRenderBarLabel, theme, width, + barTotals, + enableTotals, + formatValue, ]) const handleMouseHover = useCallback( diff --git a/packages/bar/src/BarTotals.tsx b/packages/bar/src/BarTotals.tsx new file mode 100644 index 0000000000..dc6c0ea5cb --- /dev/null +++ b/packages/bar/src/BarTotals.tsx @@ -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 { + data: BarTotalsData[] + springConfig: Partial + animate: boolean + layout?: BarCommonProps['layout'] +} + +export const BarTotals = ({ + data, + springConfig, + animate, + layout = svgDefaultProps.layout, +}: Props) => { + 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) => ( + + {barTotal.formattedValue} + + )) +} diff --git a/packages/bar/src/compute/totals.ts b/packages/bar/src/compute/totals.ts new file mode 100644 index 0000000000..f9b29ceaab --- /dev/null +++ b/packages/bar/src/compute/totals.ts @@ -0,0 +1,155 @@ +import { AnyScale, ScaleBand } from '@nivo/scales' +import { defaultProps } from '../props' +import { BarCommonProps, BarDatum, ComputedBarDatum } from '../types' + +export interface BarTotalsData { + key: string + x: number + y: number + value: number + formattedValue: string + animationOffset: number +} + +export const computeBarTotals = ( + bars: ComputedBarDatum[], + xScale: ScaleBand | AnyScale, + yScale: ScaleBand | AnyScale, + layout: BarCommonProps['layout'] = defaultProps.layout, + groupMode: BarCommonProps['groupMode'] = defaultProps.groupMode, + totalsOffset: number, + formatValue: (value: number) => string +) => { + const totals = [] as BarTotalsData[] + + if (bars.length === 0) return totals + + const totalsByIndex = new Map() + + const barWidth = bars[0].width + const barHeight = bars[0].height + + if (groupMode === 'stacked') { + const totalsPositivesByIndex = new Map() + + bars.forEach(bar => { + const { indexValue, value } = bar.data + updateTotalsByIndex(totalsByIndex, indexValue, Number(value)) + updateTotalsPositivesByIndex(totalsPositivesByIndex, indexValue, Number(value)) + }) + + totalsPositivesByIndex.forEach((totalsPositive, indexValue) => { + const indexTotal = totalsByIndex.get(indexValue) || 0 + + let xPosition: number + let yPosition: number + let animationOffset: number + + if (layout === 'vertical') { + xPosition = xScale(indexValue) + yPosition = yScale(totalsPositive) + animationOffset = yScale(totalsPositive / 2) + } else { + xPosition = xScale(totalsPositive) + yPosition = yScale(indexValue) + animationOffset = xScale(totalsPositive / 2) + } + + xPosition += layout === 'vertical' ? barWidth / 2 : totalsOffset + yPosition += layout === 'vertical' ? -totalsOffset : barHeight / 2 + + totals.push({ + key: 'total_' + indexValue, + x: xPosition, + y: yPosition, + value: indexTotal, + formattedValue: formatValue(indexTotal), + animationOffset, + }) + }) + } else if (groupMode === 'grouped') { + const greatestValueByIndex = new Map() + const numberOfBarsByIndex = new Map() + + bars.forEach(bar => { + const { indexValue, value } = bar.data + updateTotalsByIndex(totalsByIndex, indexValue, Number(value)) + updateGreatestValueByIndex(greatestValueByIndex, indexValue, Number(value)) + updateNumberOfBarsByIndex(numberOfBarsByIndex, indexValue) + }) + + greatestValueByIndex.forEach((greatestValue, indexValue) => { + const indexTotal = totalsByIndex.get(indexValue) || 0 + const numberOfBars = numberOfBarsByIndex.get(indexValue) + + let xPosition: number + let yPosition: number + let animationOffset: number + + if (layout === 'vertical') { + xPosition = xScale(indexValue) + yPosition = yScale(greatestValue) + animationOffset = yScale(greatestValue / 2) + } else { + xPosition = xScale(greatestValue) + yPosition = yScale(indexValue) + animationOffset = xScale(greatestValue / 2) + } + + const indexBarsWidth = numberOfBars * barWidth + const indexBarsHeight = numberOfBars * barHeight + + xPosition += layout === 'vertical' ? indexBarsWidth / 2 : totalsOffset + yPosition += layout === 'vertical' ? -totalsOffset : indexBarsHeight / 2 + + totals.push({ + key: 'total_' + indexValue, + x: xPosition, + y: yPosition, + value: indexTotal, + formattedValue: formatValue(indexTotal), + animationOffset, + }) + }) + } + return totals +} + +// this function is used to compute the total value for the indexes. The total value is later rendered on the chart +export const updateTotalsByIndex = ( + totalsByIndex: Map, + indexValue: string | number, + value: number +) => { + const currentIndexValue = totalsByIndex.get(indexValue) || 0 + totalsByIndex.set(indexValue, currentIndexValue + value) +} + +// this function is used to compute only the positive values of the indexes. Useful to position the text right above the last stacked bar. It prevents overlapping in case of negative values +export const updateTotalsPositivesByIndex = ( + totalsPositivesByIndex: Map, + indexValue: string | number, + value: number +) => { + const currentIndexValue = totalsPositivesByIndex.get(indexValue) || 0 + totalsPositivesByIndex.set(indexValue, currentIndexValue + (value > 0 ? value : 0)) +} + +// this function is used to keep track of the highest value for the indexes. Useful to position the text above the longest grouped bar +export const updateGreatestValueByIndex = ( + greatestValueByIndex: Map, + indexValue: string | number, + value: number +) => { + const currentGreatestValue = greatestValueByIndex.get(indexValue) || 0 + greatestValueByIndex.set(indexValue, Math.max(currentGreatestValue, Number(value))) +} + +// this function is used to save the number of bars for the indexes. Useful to position the text in the middle of the grouped bars +export const updateNumberOfBarsByIndex = ( + numberOfBarsByIndex: Map, + indexValue: string | number +) => { + const currentNumberOfBars = numberOfBarsByIndex.get(indexValue) || 0 + numberOfBarsByIndex.set(indexValue, currentNumberOfBars + 1) +} diff --git a/packages/bar/src/hooks.ts b/packages/bar/src/hooks.ts index 880b5983b7..635b711c4c 100644 --- a/packages/bar/src/hooks.ts +++ b/packages/bar/src/hooks.ts @@ -11,6 +11,7 @@ import { } from './types' import { defaultProps } from './props' import { generateGroupedBars, generateStackedBars, getLegendData } from './compute' +import { computeBarTotals } from './compute/totals' export const useBar = ({ indexBy = defaultProps.indexBy, @@ -41,6 +42,7 @@ export const useBar = ({ labelSkipHeight = defaultProps.labelSkipHeight, legends = defaultProps.legends, legendLabel, + totalsOffset = defaultProps.totalsOffset, }: { indexBy?: BarCommonProps['indexBy'] label?: BarCommonProps['label'] @@ -70,6 +72,7 @@ export const useBar = ({ labelSkipHeight?: BarCommonProps['labelSkipHeight'] legends?: BarCommonProps['legends'] legendLabel?: BarCommonProps['legendLabel'] + totalsOffset?: BarCommonProps['totalsOffset'] }) => { const [hiddenIds, setHiddenIds] = useState(initialHiddenIds ?? []) const toggleSerie = useCallback((id: string | number) => { @@ -167,6 +170,11 @@ export const useBar = ({ [legends, legendData, bars, groupMode, layout, legendLabel, reverse] ) + const barTotals = useMemo( + () => computeBarTotals(bars, xScale, yScale, layout, groupMode, totalsOffset, formatValue), + [bars, xScale, yScale, layout, groupMode, totalsOffset, formatValue] + ) + return { bars, barsWithValue, @@ -183,5 +191,6 @@ export const useBar = ({ hiddenIds, toggleSerie, legendsWithData, + barTotals, } } diff --git a/packages/bar/src/index.ts b/packages/bar/src/index.ts index 179dd8da6b..45855e52bd 100644 --- a/packages/bar/src/index.ts +++ b/packages/bar/src/index.ts @@ -1,7 +1,8 @@ export * from './Bar' +export * from './BarCanvas' export * from './BarItem' export * from './BarTooltip' -export * from './BarCanvas' +export * from './BarTotals' export * from './ResponsiveBar' export * from './ResponsiveBarCanvas' export * from './props' diff --git a/packages/bar/src/props.ts b/packages/bar/src/props.ts index e069933c7b..98c53fcf72 100644 --- a/packages/bar/src/props.ts +++ b/packages/bar/src/props.ts @@ -1,6 +1,6 @@ import { BarItem } from './BarItem' import { BarTooltip } from './BarTooltip' -import { ComputedDatum } from './types' +import { BarCanvasLayerId, BarLayerId, ComputedDatum } from './types' import { InheritedColorConfig, OrdinalColorScaleConfig } from '@nivo/colors' import { ScaleBandSpec, ScaleSpec } from '@nivo/scales' @@ -47,11 +47,14 @@ export const defaultProps = { initialHiddenIds: [], annotations: [], markers: [], + + enableTotals: false, + totalsOffset: 10, } export const svgDefaultProps = { ...defaultProps, - layers: ['grid', 'axes', 'bars', 'markers', 'legends', 'annotations'], + layers: ['grid', 'axes', 'bars', 'totals', 'markers', 'legends', 'annotations'] as BarLayerId[], barComponent: BarItem, defs: [], @@ -66,7 +69,7 @@ export const svgDefaultProps = { export const canvasDefaultProps = { ...defaultProps, - layers: ['grid', 'axes', 'bars', 'legends', 'annotations'], + layers: ['grid', 'axes', 'bars', 'totals', 'legends', 'annotations'] as BarCanvasLayerId[], pixelRatio: typeof window !== 'undefined' ? window.devicePixelRatio ?? 1 : 1, } diff --git a/packages/bar/src/types.ts b/packages/bar/src/types.ts index 24e8ea52ea..ec545851ed 100644 --- a/packages/bar/src/types.ts +++ b/packages/bar/src/types.ts @@ -95,7 +95,8 @@ 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 interface BarCustomLayerBaseProps extends Pick< @@ -138,9 +139,7 @@ export type BarCanvasCustomLayer = ( ) => void export type BarCustomLayer = React.FC> -export type BarCanvasLayer = - | Exclude - | BarCanvasCustomLayer +export type BarCanvasLayer = BarCanvasLayerId | BarCanvasCustomLayer export type BarLayer = BarLayerId | BarCustomLayer export interface BarItemProps @@ -259,6 +258,9 @@ export type BarCommonProps = { renderWrapper?: boolean initialHiddenIds: readonly (string | number)[] + + enableTotals: boolean + totalsOffset: number } export type BarSvgProps = Partial> & diff --git a/packages/bar/tests/Bar.test.tsx b/packages/bar/tests/Bar.test.tsx index 37cbc1ae50..1fe68574c7 100644 --- a/packages/bar/tests/Bar.test.tsx +++ b/packages/bar/tests/Bar.test.tsx @@ -1,7 +1,7 @@ import { mount } from 'enzyme' -import { create, act, ReactTestRenderer } from 'react-test-renderer' +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 @@ -614,6 +614,163 @@ it('should render bars in grouped mode after updating starting values from 0', ( }) }) +describe('totals layer', () => { + it('should have the total text for each index with vertical layout', () => { + const instance = create( + + ).root + + const totals = instance.findByType(BarTotals).findAllByType('text') + + totals.forEach((total, index) => { + const value = total.findByType('text').children[0] + if (index === 0) { + expect(value).toBe(`2`) + } else if (index === 1) { + expect(value).toBe(`3`) + } else if (index === 2) { + expect(value).toBe(`4`) + } + }) + }) + it('should have the total text for each index with horizontal layout', () => { + const instance = create( + + ).root + + const totals = instance.findByType(BarTotals).findAllByType('text') + + totals.forEach((total, index) => { + const value = total.findByType('text').children[0] + if (index === 0) { + expect(value).toBe(`$2`) + } else if (index === 1) { + expect(value).toBe(`$4`) + } else if (index === 2) { + expect(value).toBe(`$6`) + } + }) + }) + it('should have the total text for each index with grouped group mode and vertical layout', () => { + const instance = create( + + ).root + + const totals = instance.findByType(BarTotals).findAllByType('text') + + totals.forEach((total, index) => { + const value = total.findByType('text').children[0] + if (index === 0) { + expect(value).toBe(`-2`) + } else { + expect(value).toBe(`-4`) + } + }) + }) + it('should have the total text for each index with grouped group mode and horizontal layout', () => { + const instance = create( + + ).root + + const totals = instance.findByType(BarTotals).findAllByType('text') + + totals.forEach((total, index) => { + const value = total.findByType('text').children[0] + if (index === 0) { + expect(value).toBe(`0`) + } else if (index === 1) { + expect(value).toBe(`1`) + } else if (index === 2) { + expect(value).toBe(`3`) + } + }) + }) + it('should follow the theme configurations', () => { + const instance = create( + + ).root + + const totals = instance.findByType(BarTotals).findAllByType('text') + + totals.forEach((total, index) => { + const props = total.findByType('text').props + expect(props.style.fill).toBe('red') + expect(props.fontSize).toBe(14) + expect(props.fontFamily).toBe('serif') + }) + }) +}) + describe('tooltip', () => { it('should render a tooltip when hovering a slice', () => { let component: ReactTestRenderer diff --git a/storybook/stories/bar/Bar.stories.tsx b/storybook/stories/bar/Bar.stories.tsx index aa2a3b8326..2c9ddb5f6f 100644 --- a/storybook/stories/bar/Bar.stories.tsx +++ b/storybook/stories/bar/Bar.stories.tsx @@ -294,6 +294,10 @@ export const WithSymlogScale: Story = { ), } +export const WithTotals: Story = { + render: () => , +} + const DataGenerator = (initialIndex, initialState) => { let index = initialIndex let state = initialState diff --git a/website/src/data/components/bar/meta.yml b/website/src/data/components/bar/meta.yml index 6af1929ba8..989f3706c5 100644 --- a/website/src/data/components/bar/meta.yml +++ b/website/src/data/components/bar/meta.yml @@ -34,21 +34,19 @@ Bar: link: bar--custom-legend-labels - label: Using annotations link: bar--with-annotations + - label: Using totals + link: bar--with-totals description: | Bar chart which can display multiple data series, stacked or side by side. Also supports both vertical and horizontal layout, with negative values descending below the x axis (or y axis if using horizontal layout). - The bar item component can be customized to render any valid SVG element, it will receive current bar style, data and event handlers, the storybook offers an [example](storybook:bar--custom-bar-item). - The responsive alternative of this component is `ResponsiveBar`. - This component is available in the `@nivo/api`, see [sample](api:/samples/bar.svg) or [try it using the API client](self:/bar/api). - See the [dedicated guide](self:/guides/legends) on how to setup legends for this component. However it requires an extra property for each legend configuration you pass to @@ -56,7 +54,6 @@ Bar: legend's data and accept `indexes` or `keys`. `indexes` is suitable for simple bar chart with a single data serie while `keys` may be used if you have several ones (groups). - BarCanvas: package: '@nivo/bar' tags: @@ -72,5 +69,4 @@ BarCanvas: A variation around the [Bar](self:/bar) component. Well suited for large data sets as it does not impact DOM tree depth, however you'll lose the isomorphic ability and transitions. - - The responsive alternative of this component is `ResponsiveBarCanvas`. + The responsive alternative of this component is `ResponsiveBarCanvas`. \ No newline at end of file diff --git a/website/src/data/components/bar/props.ts b/website/src/data/components/bar/props.ts index ba5111e69e..2e5cd406b4 100644 --- a/website/src/data/components/bar/props.ts +++ b/website/src/data/components/bar/props.ts @@ -416,6 +416,31 @@ const props: ChartProperty[] = [ control: { type: 'inheritedColor' }, group: 'Labels', }, + { + key: 'enableTotals', + help: 'Enable/disable totals labels.', + type: 'boolean', + flavors: ['svg', 'canvas', 'api'], + required: false, + defaultValue: svgDefaultProps.enableTotals, + group: 'Labels', + control: { type: 'switch' }, + }, + { + key: 'totalsOffset', + help: 'Offset from the bar edge for the total label.', + type: 'number', + flavors: ['svg', 'canvas', 'api'], + required: false, + defaultValue: svgDefaultProps.totalsOffset, + group: 'Labels', + control: { + type: 'range', + unit: 'px', + min: 0, + max: 40, + }, + }, ...chartGrid({ flavors: allFlavors, xDefault: svgDefaultProps.enableGridX, diff --git a/website/src/pages/bar/api.tsx b/website/src/pages/bar/api.tsx index 5c30e25714..198fe61915 100644 --- a/website/src/pages/bar/api.tsx +++ b/website/src/pages/bar/api.tsx @@ -109,6 +109,8 @@ const BarApi = () => { enableGridY: true, enableLabel: true, + enableTotals: false, + totalsOffset: 10, labelSkipWidth: 12, labelSkipHeight: 12, labelTextColor: { diff --git a/website/src/pages/bar/canvas.js b/website/src/pages/bar/canvas.js index 398521c0d4..f053bd1d53 100644 --- a/website/src/pages/bar/canvas.js +++ b/website/src/pages/bar/canvas.js @@ -89,6 +89,8 @@ const initialProperties = { enableGridY: false, enableLabel: true, + enableTotals: false, + totalsOffset: 10, labelSkipWidth: 12, labelSkipHeight: 12, labelTextColor: { diff --git a/website/src/pages/bar/index.js b/website/src/pages/bar/index.js index 74254db8eb..e7c91f2d13 100644 --- a/website/src/pages/bar/index.js +++ b/website/src/pages/bar/index.js @@ -107,6 +107,8 @@ const initialProperties = { enableGridY: true, enableLabel: true, + enableTotals: false, + totalsOffset: 10, labelSkipWidth: 12, labelSkipHeight: 12, labelTextColor: {