From 5585ea10bae52c69fd1cd1737d1b25e731d7d5b2 Mon Sep 17 00:00:00 2001 From: Jose C Quintas Jr Date: Tue, 21 May 2024 16:50:52 +0200 Subject: [PATCH] [charts] Add `label` to be displayed inside bars in BarChart (#12988) Signed-off-by: Jose C Quintas Jr Co-authored-by: Lukas Co-authored-by: Alexandre Fauquette <45398769+alexfauquette@users.noreply.github.com> --- docs/data/charts-component-api-pages.ts | 4 + docs/data/charts/bars/BarLabel.js | 14 ++ docs/data/charts/bars/BarLabel.tsx | 14 ++ docs/data/charts/bars/BarLabel.tsx.preview | 7 + docs/data/charts/bars/CustomLabels.js | 22 +++ docs/data/charts/bars/CustomLabels.tsx | 22 +++ .../data/charts/bars/CustomLabels.tsx.preview | 15 ++ docs/data/charts/bars/bars.md | 21 ++- docs/pages/x/api/charts/bar-chart.json | 7 + docs/pages/x/api/charts/bar-label.js | 23 +++ docs/pages/x/api/charts/bar-label.json | 41 +++++ docs/pages/x/api/charts/bar-plot.json | 7 + .../api-docs/charts/bar-chart/bar-chart.json | 4 + .../api-docs/charts/bar-label/bar-label.json | 18 ++ .../api-docs/charts/bar-plot/bar-plot.json | 8 +- packages/x-charts/src/BarChart/BarChart.tsx | 10 ++ .../src/BarChart/BarLabel/BarLabel.tsx | 52 ++++++ .../src/BarChart/BarLabel/BarLabel.types.ts | 46 +++++ .../src/BarChart/BarLabel/BarLabelItem.tsx | 170 ++++++++++++++++++ .../src/BarChart/BarLabel/BarLabelPlot.tsx | 98 ++++++++++ .../src/BarChart/BarLabel/barLabelClasses.tsx | 34 ++++ .../src/BarChart/BarLabel/getBarLabel.ts | 20 +++ .../x-charts/src/BarChart/BarLabel/index.ts | 6 + packages/x-charts/src/BarChart/BarPlot.tsx | 38 +++- packages/x-charts/src/BarChart/index.ts | 1 + .../src/SparkLineChart/SparkLineChart.tsx | 2 +- .../src/themeAugmentation/components.d.ts | 4 + .../x-charts/src/themeAugmentation/index.js | 2 +- .../src/themeAugmentation/overrides.d.ts | 3 + .../x-charts/src/themeAugmentation/props.d.ts | 2 + scripts/x-charts.exports.json | 11 ++ 31 files changed, 717 insertions(+), 9 deletions(-) create mode 100644 docs/data/charts/bars/BarLabel.js create mode 100644 docs/data/charts/bars/BarLabel.tsx create mode 100644 docs/data/charts/bars/BarLabel.tsx.preview create mode 100644 docs/data/charts/bars/CustomLabels.js create mode 100644 docs/data/charts/bars/CustomLabels.tsx create mode 100644 docs/data/charts/bars/CustomLabels.tsx.preview create mode 100644 docs/pages/x/api/charts/bar-label.js create mode 100644 docs/pages/x/api/charts/bar-label.json create mode 100644 docs/translations/api-docs/charts/bar-label/bar-label.json create mode 100644 packages/x-charts/src/BarChart/BarLabel/BarLabel.tsx create mode 100644 packages/x-charts/src/BarChart/BarLabel/BarLabel.types.ts create mode 100644 packages/x-charts/src/BarChart/BarLabel/BarLabelItem.tsx create mode 100644 packages/x-charts/src/BarChart/BarLabel/BarLabelPlot.tsx create mode 100644 packages/x-charts/src/BarChart/BarLabel/barLabelClasses.tsx create mode 100644 packages/x-charts/src/BarChart/BarLabel/getBarLabel.ts create mode 100644 packages/x-charts/src/BarChart/BarLabel/index.ts diff --git a/docs/data/charts-component-api-pages.ts b/docs/data/charts-component-api-pages.ts index aff842dcf2190..3a0d808b2d06b 100644 --- a/docs/data/charts-component-api-pages.ts +++ b/docs/data/charts-component-api-pages.ts @@ -25,6 +25,10 @@ const apiPages: MuiPage[] = [ pathname: '/x/api/charts/bar-element', title: 'BarElement', }, + { + pathname: '/x/api/charts/bar-label', + title: 'BarLabel', + }, { pathname: '/x/api/charts/bar-plot', title: 'BarPlot', diff --git a/docs/data/charts/bars/BarLabel.js b/docs/data/charts/bars/BarLabel.js new file mode 100644 index 0000000000000..c684e33fa3e4c --- /dev/null +++ b/docs/data/charts/bars/BarLabel.js @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { BarChart } from '@mui/x-charts/BarChart'; + +export default function BarLabel() { + return ( + + ); +} diff --git a/docs/data/charts/bars/BarLabel.tsx b/docs/data/charts/bars/BarLabel.tsx new file mode 100644 index 0000000000000..c684e33fa3e4c --- /dev/null +++ b/docs/data/charts/bars/BarLabel.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { BarChart } from '@mui/x-charts/BarChart'; + +export default function BarLabel() { + return ( + + ); +} diff --git a/docs/data/charts/bars/BarLabel.tsx.preview b/docs/data/charts/bars/BarLabel.tsx.preview new file mode 100644 index 0000000000000..12bc56aa174ed --- /dev/null +++ b/docs/data/charts/bars/BarLabel.tsx.preview @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/docs/data/charts/bars/CustomLabels.js b/docs/data/charts/bars/CustomLabels.js new file mode 100644 index 0000000000000..8e22ead904871 --- /dev/null +++ b/docs/data/charts/bars/CustomLabels.js @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { BarChart } from '@mui/x-charts/BarChart'; + +export default function CustomLabels() { + return ( + { + if ((item.value ?? 0) > 10) { + return 'High'; + } + return context.bar.height < 60 ? null : item.value?.toString(); + }} + width={600} + height={350} + /> + ); +} diff --git a/docs/data/charts/bars/CustomLabels.tsx b/docs/data/charts/bars/CustomLabels.tsx new file mode 100644 index 0000000000000..8e22ead904871 --- /dev/null +++ b/docs/data/charts/bars/CustomLabels.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { BarChart } from '@mui/x-charts/BarChart'; + +export default function CustomLabels() { + return ( + { + if ((item.value ?? 0) > 10) { + return 'High'; + } + return context.bar.height < 60 ? null : item.value?.toString(); + }} + width={600} + height={350} + /> + ); +} diff --git a/docs/data/charts/bars/CustomLabels.tsx.preview b/docs/data/charts/bars/CustomLabels.tsx.preview new file mode 100644 index 0000000000000..44fe9443bee57 --- /dev/null +++ b/docs/data/charts/bars/CustomLabels.tsx.preview @@ -0,0 +1,15 @@ + { + if ((item.value ?? 0) > 10) { + return 'High'; + } + return context.bar.height < 60 ? null : item.value?.toString(); + }} + width={600} + height={350} +/> \ No newline at end of file diff --git a/docs/data/charts/bars/bars.md b/docs/data/charts/bars/bars.md index 9ac226e43ddd4..04483c3724f7a 100644 --- a/docs/data/charts/bars/bars.md +++ b/docs/data/charts/bars/bars.md @@ -1,7 +1,7 @@ --- title: React Bar chart productId: x-charts -components: BarChart, BarElement, BarPlot, ChartsGrid, ChartsOnAxisClickHandler +components: BarChart, BarElement, BarPlot, ChartsGrid, ChartsOnAxisClickHandler, BarLabel --- # Charts - Bars @@ -108,6 +108,25 @@ It will work with any positive value and will be properly applied to horizontal {{"demo": "BorderRadius.js"}} +## Labels + +You can display labels on the bars. +To do so, the `BarChart` or `BarPlot` accepts a `barLabel` property. +It can either get a function that gets the bar item and some context. +Or you can pass `'value'` to display the raw value of the bar. + +{{"demo": "BarLabel.js"}} + +### Custom Labels + +You can display, change or hide labels based on conditional logic. +To do so, provide a function to the `barLabel`. +Labels are not displayed if the function returns `null`. + +In the example we display a `'High'` text on values higher than 10, and hide values when the generated bar height is lower than 60px. + +{{"demo": "CustomLabels.js"}} + ## Click event Bar charts provides two click handlers: diff --git a/docs/pages/x/api/charts/bar-chart.json b/docs/pages/x/api/charts/bar-chart.json index 809e11c9a76c6..4f65db4878404 100644 --- a/docs/pages/x/api/charts/bar-chart.json +++ b/docs/pages/x/api/charts/bar-chart.json @@ -14,6 +14,7 @@ "text": "highlight docs" } }, + "barLabel": { "type": { "name": "union", "description": "'value'
| func" } }, "borderRadius": { "type": { "name": "number" } }, "bottomAxis": { "type": { "name": "union", "description": "object
| string" }, @@ -131,6 +132,12 @@ "default": "BarElementPath", "class": null }, + { + "name": "barLabel", + "description": "The component that renders the bar label.", + "default": "BarLabel", + "class": null + }, { "name": "legend", "description": "Custom rendering of the legend.", diff --git a/docs/pages/x/api/charts/bar-label.js b/docs/pages/x/api/charts/bar-label.js new file mode 100644 index 0000000000000..48aeaf5b8d312 --- /dev/null +++ b/docs/pages/x/api/charts/bar-label.js @@ -0,0 +1,23 @@ +import * as React from 'react'; +import ApiPage from 'docs/src/modules/components/ApiPage'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import jsonPageContent from './bar-label.json'; + +export default function Page(props) { + const { descriptions, pageContent } = props; + return ; +} + +Page.getInitialProps = () => { + const req = require.context( + 'docsx/translations/api-docs/charts/bar-label', + false, + /\.\/bar-label.*.json$/, + ); + const descriptions = mapApiPageTranslations(req); + + return { + descriptions, + pageContent: jsonPageContent, + }; +}; diff --git a/docs/pages/x/api/charts/bar-label.json b/docs/pages/x/api/charts/bar-label.json new file mode 100644 index 0000000000000..b5a68df0e457e --- /dev/null +++ b/docs/pages/x/api/charts/bar-label.json @@ -0,0 +1,41 @@ +{ + "props": {}, + "name": "BarLabel", + "imports": [ + "import { BarLabel } from '@mui/x-charts/BarChart';", + "import { BarLabel } from '@mui/x-charts';" + ], + "slots": [ + { + "name": "barLabel", + "description": "The component that renders the bar label.", + "default": "BarLabel", + "class": null + } + ], + "classes": [ + { + "key": "faded", + "className": "MuiBarLabel-faded", + "description": "Styles applied to the root element if it is faded.", + "isGlobal": false + }, + { + "key": "highlighted", + "className": "MuiBarLabel-highlighted", + "description": "Styles applied to the root element if it is highlighted.", + "isGlobal": false + }, + { + "key": "root", + "className": "MuiBarLabel-root", + "description": "Styles applied to the root element.", + "isGlobal": false + } + ], + "muiName": "MuiBarLabel", + "filename": "/packages/x-charts/src/BarChart/BarLabel/BarLabel.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/x/api/charts/bar-plot.json b/docs/pages/x/api/charts/bar-plot.json index 51afe2f2a2d19..cb04173a43115 100644 --- a/docs/pages/x/api/charts/bar-plot.json +++ b/docs/pages/x/api/charts/bar-plot.json @@ -1,5 +1,6 @@ { "props": { + "barLabel": { "type": { "name": "union", "description": "'value'
| func" } }, "borderRadius": { "type": { "name": "number" } }, "onItemClick": { "type": { "name": "func" }, @@ -27,6 +28,12 @@ "description": "The component that renders the bar.", "default": "BarElementPath", "class": null + }, + { + "name": "barLabel", + "description": "The component that renders the bar label.", + "default": "BarLabel", + "class": null } ], "classes": [], diff --git a/docs/translations/api-docs/charts/bar-chart/bar-chart.json b/docs/translations/api-docs/charts/bar-chart/bar-chart.json index 88dc2317541f3..6adc0b412648b 100644 --- a/docs/translations/api-docs/charts/bar-chart/bar-chart.json +++ b/docs/translations/api-docs/charts/bar-chart/bar-chart.json @@ -5,6 +5,9 @@ "description": "The configuration of axes highlight. Default is set to 'band' in the bar direction. Depends on layout prop.", "seeMoreText": "See {{link}} for more details." }, + "barLabel": { + "description": "If provided, the function will be used to format the label of the bar. It can be set to 'value' to display the current value." + }, "borderRadius": { "description": "Defines the border radius of the bar element." }, "bottomAxis": { "description": "Indicate which axis to display the bottom of the charts. Can be a string (the id of the axis) or an object ChartsXAxisProps." @@ -76,6 +79,7 @@ "axisTick": "Custom component for the axis tick.", "axisTickLabel": "Custom component for tick label.", "bar": "The component that renders the bar.", + "barLabel": "The component that renders the bar label.", "itemContent": "Custom component for displaying tooltip content when triggered by item event.", "legend": "Custom rendering of the legend.", "loadingOverlay": "Overlay component rendered when the chart is in a loading state.", diff --git a/docs/translations/api-docs/charts/bar-label/bar-label.json b/docs/translations/api-docs/charts/bar-label/bar-label.json new file mode 100644 index 0000000000000..9d908b3e5797d --- /dev/null +++ b/docs/translations/api-docs/charts/bar-label/bar-label.json @@ -0,0 +1,18 @@ +{ + "componentDescription": "", + "propDescriptions": {}, + "classDescriptions": { + "faded": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the root element", + "conditions": "it is faded" + }, + "highlighted": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the root element", + "conditions": "it is highlighted" + }, + "root": { "description": "Styles applied to the root element." } + }, + "slotDescriptions": { "barLabel": "The component that renders the bar label." } +} diff --git a/docs/translations/api-docs/charts/bar-plot/bar-plot.json b/docs/translations/api-docs/charts/bar-plot/bar-plot.json index b56710ba45b29..a4eed37a0972e 100644 --- a/docs/translations/api-docs/charts/bar-plot/bar-plot.json +++ b/docs/translations/api-docs/charts/bar-plot/bar-plot.json @@ -1,6 +1,9 @@ { "componentDescription": "", "propDescriptions": { + "barLabel": { + "description": "If provided, the function will be used to format the label of the bar. It can be set to 'value' to display the current value." + }, "borderRadius": { "description": "Defines the border radius of the bar element." }, "onItemClick": { "description": "Callback fired when a bar item is clicked.", @@ -14,5 +17,8 @@ "slots": { "description": "Overridable component slots." } }, "classDescriptions": {}, - "slotDescriptions": { "bar": "The component that renders the bar." } + "slotDescriptions": { + "bar": "The component that renders the bar.", + "barLabel": "The component that renders the bar label." + } } diff --git a/packages/x-charts/src/BarChart/BarChart.tsx b/packages/x-charts/src/BarChart/BarChart.tsx index 48e017d914e3e..5e315d8e69ae2 100644 --- a/packages/x-charts/src/BarChart/BarChart.tsx +++ b/packages/x-charts/src/BarChart/BarChart.tsx @@ -138,6 +138,7 @@ const BarChart = React.forwardRef(function BarChart(props: BarChartProps, ref) { slots, slotProps, loading, + barLabel, } = props; const id = useId(); @@ -197,6 +198,7 @@ const BarChart = React.forwardRef(function BarChart(props: BarChartProps, ref) { skipAnimation={skipAnimation} onItemClick={onItemClick} borderRadius={borderRadius} + barLabel={barLabel} /> @@ -232,6 +234,14 @@ BarChart.propTypes = { x: PropTypes.oneOf(['band', 'line', 'none']), y: PropTypes.oneOf(['band', 'line', 'none']), }), + /** + * If provided, the function will be used to format the label of the bar. + * It can be set to 'value' to display the current value. + * @param {BarItem} item The item to format. + * @param {BarLabelContext} context data about the bar. + * @returns {string} The formatted label. + */ + barLabel: PropTypes.oneOfType([PropTypes.oneOf(['value']), PropTypes.func]), /** * Defines the border radius of the bar element. */ diff --git a/packages/x-charts/src/BarChart/BarLabel/BarLabel.tsx b/packages/x-charts/src/BarChart/BarLabel/BarLabel.tsx new file mode 100644 index 0000000000000..80aa8ba4dbb15 --- /dev/null +++ b/packages/x-charts/src/BarChart/BarLabel/BarLabel.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import { styled, useThemeProps } from '@mui/material/styles'; +import { animated } from '@react-spring/web'; +import PropTypes from 'prop-types'; +import { barLabelClasses } from './barLabelClasses'; +import { BarLabelOwnerState } from './BarLabel.types'; + +export const BarLabelComponent = styled(animated.text, { + name: 'MuiBarLabel', + slot: 'Root', + overridesResolver: (_, styles) => [ + { [`&.${barLabelClasses.faded}`]: styles.faded }, + { [`&.${barLabelClasses.highlighted}`]: styles.highlighted }, + styles.root, + ], +})(({ theme }) => ({ + ...theme?.typography?.body2, + stroke: 'none', + fill: (theme.vars || theme)?.palette?.text?.primary, + transition: 'opacity 0.2s ease-in, fill 0.2s ease-in', + textAnchor: 'middle', + dominantBaseline: 'central', + pointerEvents: 'none', + opacity: 1, + [`&.${barLabelClasses.faded}`]: { + opacity: 0.3, + }, +})); + +export type BarLabelProps = Omit, 'ref' | 'id'> & BarLabelOwnerState; + +function BarLabel(props: BarLabelProps) { + const themeProps = useThemeProps({ props, name: 'MuiBarLabel' }); + + const { seriesId, dataIndex, color, isFaded, isHighlighted, classes, ...otherProps } = themeProps; + + return ; +} + +BarLabel.propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + classes: PropTypes.object, + dataIndex: PropTypes.number.isRequired, + isFaded: PropTypes.bool.isRequired, + isHighlighted: PropTypes.bool.isRequired, + seriesId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, +} as any; + +export { BarLabel }; diff --git a/packages/x-charts/src/BarChart/BarLabel/BarLabel.types.ts b/packages/x-charts/src/BarChart/BarLabel/BarLabel.types.ts new file mode 100644 index 0000000000000..bc0c73ff96317 --- /dev/null +++ b/packages/x-charts/src/BarChart/BarLabel/BarLabel.types.ts @@ -0,0 +1,46 @@ +import { SeriesId } from '../../models/seriesType/common'; +import type { BarLabelClasses } from './barLabelClasses'; + +export interface BarLabelOwnerState { + seriesId: SeriesId; + dataIndex: number; + color: string; + isFaded: boolean; + isHighlighted: boolean; + classes?: Partial; +} + +export type BarItem = { + /** + * The series id of the bar. + */ + seriesId: SeriesId; + /** + * The index of the data point in the series. + */ + dataIndex: number; + /** + * The value of the data point. + */ + value: number | null; +}; + +export type BarLabelContext = { + bar: { + /** + * The height of the bar. + * It could be used to control the label based on the bar size. + */ + height: number; + /** + * The width of the bar. + * It could be used to control the label based on the bar size. + */ + width: number; + }; +}; + +export type BarLabelFunction = ( + item: BarItem, + context: BarLabelContext, +) => string | null | undefined; diff --git a/packages/x-charts/src/BarChart/BarLabel/BarLabelItem.tsx b/packages/x-charts/src/BarChart/BarLabel/BarLabelItem.tsx new file mode 100644 index 0000000000000..6c8c2bcd945b0 --- /dev/null +++ b/packages/x-charts/src/BarChart/BarLabel/BarLabelItem.tsx @@ -0,0 +1,170 @@ +import * as React from 'react'; +import { useSlotProps } from '@mui/base/utils'; +import PropTypes from 'prop-types'; +import { InteractionContext } from '../../context/InteractionProvider'; +import { getIsFaded, getIsHighlighted } from '../../hooks/useInteractionItemProps'; +import { useUtilityClasses } from './barLabelClasses'; +import { HighlighContext } from '../../context/HighlightProvider'; +import { BarLabelOwnerState, BarItem, BarLabelContext } from './BarLabel.types'; +import { getBarLabel } from './getBarLabel'; +import { BarLabel, BarLabelProps } from './BarLabel'; + +export interface BarLabelSlots { + /** + * The component that renders the bar label. + * @default BarLabel + */ + barLabel?: React.JSXElementConstructor; +} + +export interface BarLabelSlotProps { + barLabel?: Partial; +} + +export type BarLabelItemProps = Omit & + Pick & { + /** + * The props used for each component slot. + * @default {} + */ + slotProps?: BarLabelSlotProps; + /** + * Overridable component slots. + * @default {} + */ + slots?: BarLabelSlots; + /** + * The height of the bar. + */ + height: number; + /** + * The width of the bar. + */ + width: number; + /** + * The value of the data point. + */ + value: number | null; + /** + * If provided, the function will be used to format the label of the bar. + * It can be set to 'value' to display the current value. + * @param {BarItem} item The item to format. + * @param {BarLabelContext} context data about the bar. + * @returns {string} The formatted label. + */ + barLabel?: 'value' | ((item: BarItem, context: BarLabelContext) => string | null | undefined); + }; + +/** + * @ignore - internal component. + */ +function BarLabelItem(props: BarLabelItemProps) { + const { + seriesId, + classes: innerClasses, + color, + style, + dataIndex, + barLabel, + slots, + slotProps, + height, + width, + value, + ...other + } = props; + const { item } = React.useContext(InteractionContext); + const { scope } = React.useContext(HighlighContext); + + const isHighlighted = getIsHighlighted(item, { type: 'bar', seriesId, dataIndex }, scope); + const isFaded = !isHighlighted && getIsFaded(item, { type: 'bar', seriesId, dataIndex }, scope); + + const ownerState = { + seriesId, + classes: innerClasses, + color, + isFaded, + isHighlighted, + dataIndex, + }; + const classes = useUtilityClasses(ownerState); + + const Component = slots?.barLabel ?? BarLabel; + + const { ownerState: barLabelOwnerState, ...barLabelProps } = useSlotProps({ + elementType: Component, + externalSlotProps: slotProps?.barLabel, + additionalProps: { + ...other, + style, + className: classes.root, + }, + ownerState, + }); + + if (!barLabel) { + return null; + } + + const formattedLabelText = getBarLabel({ + barLabel, + value, + dataIndex, + seriesId, + height, + width, + }); + + if (!formattedLabelText) { + return null; + } + + return ( + + {formattedLabelText} + + ); +} + +BarLabelItem.propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * If provided, the function will be used to format the label of the bar. + * It can be set to 'value' to display the current value. + * @param {BarItem} item The item to format. + * @param {BarLabelContext} context data about the bar. + * @returns {string} The formatted label. + */ + barLabel: PropTypes.oneOfType([PropTypes.oneOf(['value']), PropTypes.func]), + classes: PropTypes.object, + color: PropTypes.string.isRequired, + dataIndex: PropTypes.number.isRequired, + /** + * The height of the bar. + */ + height: PropTypes.number.isRequired, + seriesId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + /** + * The props used for each component slot. + * @default {} + */ + slotProps: PropTypes.object, + /** + * Overridable component slots. + * @default {} + */ + slots: PropTypes.object, + /** + * The value of the data point. + */ + value: PropTypes.number, + /** + * The width of the bar. + */ + width: PropTypes.number.isRequired, +} as any; + +export { BarLabelItem }; diff --git a/packages/x-charts/src/BarChart/BarLabel/BarLabelPlot.tsx b/packages/x-charts/src/BarChart/BarLabel/BarLabelPlot.tsx new file mode 100644 index 0000000000000..21799a6da2279 --- /dev/null +++ b/packages/x-charts/src/BarChart/BarLabel/BarLabelPlot.tsx @@ -0,0 +1,98 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { useTransition } from '@react-spring/web'; +import type { AnimationData, CompletedBarData } from '../types'; +import { BarLabelItem, BarLabelItemProps } from './BarLabelItem'; + +const leaveStyle = ({ layout, yOrigin, x, width, y, xOrigin, height }: AnimationData) => ({ + ...(layout === 'vertical' + ? { + y: yOrigin, + x: x + width / 2, + height: 0, + width, + } + : { + y: y + height / 2, + x: xOrigin, + height, + width: 0, + }), +}); + +const enterStyle = ({ x, width, y, height }: AnimationData) => ({ + x: x + width / 2, + y: y + height / 2, + height, + width, +}); + +type BarLabelPlotProps = { + bars: CompletedBarData[]; + skipAnimation?: boolean; + barLabel?: BarLabelItemProps['barLabel']; +}; + +/** + * @ignore - internal component. + */ +function BarLabelPlot(props: BarLabelPlotProps) { + const { bars, skipAnimation, ...other } = props; + + const barLabelTransition = useTransition(bars, { + keys: (bar) => `${bar.seriesId}-${bar.dataIndex}`, + from: leaveStyle, + leave: null, + enter: enterStyle, + update: enterStyle, + immediate: skipAnimation, + }); + + return ( + + {barLabelTransition((style, { seriesId, dataIndex, color, value, width, height }) => ( + + ))} + + ); +} + +BarLabelPlot.propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + barLabel: PropTypes.oneOfType([PropTypes.oneOf(['value']), PropTypes.func]), + bars: PropTypes.arrayOf( + PropTypes.shape({ + color: PropTypes.string.isRequired, + dataIndex: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + highlightScope: PropTypes.shape({ + faded: PropTypes.oneOf(['global', 'none', 'series']), + highlighted: PropTypes.oneOf(['item', 'none', 'series']), + }), + layout: PropTypes.oneOf(['horizontal', 'vertical']), + maskId: PropTypes.string.isRequired, + seriesId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + value: PropTypes.number, + width: PropTypes.number.isRequired, + x: PropTypes.number.isRequired, + xOrigin: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + yOrigin: PropTypes.number.isRequired, + }), + ).isRequired, + skipAnimation: PropTypes.bool, +} as any; + +export { BarLabelPlot }; diff --git a/packages/x-charts/src/BarChart/BarLabel/barLabelClasses.tsx b/packages/x-charts/src/BarChart/BarLabel/barLabelClasses.tsx new file mode 100644 index 0000000000000..b892abd628835 --- /dev/null +++ b/packages/x-charts/src/BarChart/BarLabel/barLabelClasses.tsx @@ -0,0 +1,34 @@ +import generateUtilityClass from '@mui/utils/generateUtilityClass'; +import generateUtilityClasses from '@mui/utils/generateUtilityClasses'; +import composeClasses from '@mui/utils/composeClasses'; +import type { BarLabelOwnerState } from './BarLabel.types'; + +export interface BarLabelClasses { + /** Styles applied to the root element. */ + root: string; + /** Styles applied to the root element if it is highlighted. */ + highlighted: string; + /** Styles applied to the root element if it is faded. */ + faded: string; +} + +export type BarLabelClassKey = keyof BarLabelClasses; + +export function getBarLabelUtilityClass(slot: string) { + return generateUtilityClass('MuiBarLabel', slot); +} + +export const barLabelClasses = generateUtilityClasses('MuiBarLabel', [ + 'root', + 'highlighted', + 'faded', +]); + +export const useUtilityClasses = (ownerState: BarLabelOwnerState) => { + const { classes, seriesId, isFaded, isHighlighted } = ownerState; + const slots = { + root: ['root', `series-${seriesId}`, isHighlighted && 'highlighted', isFaded && 'faded'], + }; + + return composeClasses(slots, getBarLabelUtilityClass, classes); +}; diff --git a/packages/x-charts/src/BarChart/BarLabel/getBarLabel.ts b/packages/x-charts/src/BarChart/BarLabel/getBarLabel.ts new file mode 100644 index 0000000000000..2459c764d956e --- /dev/null +++ b/packages/x-charts/src/BarChart/BarLabel/getBarLabel.ts @@ -0,0 +1,20 @@ +import { SeriesId } from '../../models/seriesType/common'; +import { BarLabelFunction } from './BarLabel.types'; + +export const getBarLabel = (options: { + barLabel: 'value' | BarLabelFunction; + value: number | null; + dataIndex: number; + seriesId: SeriesId; + height: number; + width: number; +}): string | null | undefined => { + const { barLabel, value, dataIndex, seriesId, height, width } = options; + + if (barLabel === 'value') { + // We don't want to show the label if the value is 0 + return value ? value?.toString() : null; + } + + return barLabel({ seriesId, dataIndex, value }, { bar: { height, width } }); +}; diff --git a/packages/x-charts/src/BarChart/BarLabel/index.ts b/packages/x-charts/src/BarChart/BarLabel/index.ts new file mode 100644 index 0000000000000..8778a7e4605b4 --- /dev/null +++ b/packages/x-charts/src/BarChart/BarLabel/index.ts @@ -0,0 +1,6 @@ +export { BarLabel } from './BarLabel'; +export type { BarLabelProps } from './BarLabel'; +export { barLabelClasses, getBarLabelUtilityClass } from './barLabelClasses'; +export type { BarLabelSlotProps, BarLabelSlots } from './BarLabelItem'; +export type { BarLabelOwnerState, BarItem, BarLabelContext } from './BarLabel.types'; +export type { BarLabelClasses, BarLabelClassKey } from './barLabelClasses'; diff --git a/packages/x-charts/src/BarChart/BarPlot.tsx b/packages/x-charts/src/BarChart/BarPlot.tsx index 5d09ff6008abd..4451bddff5868 100644 --- a/packages/x-charts/src/BarChart/BarPlot.tsx +++ b/packages/x-charts/src/BarChart/BarPlot.tsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { useTransition } from '@react-spring/web'; import { SeriesContext } from '../context/SeriesContextProvider'; import { CartesianContext } from '../context/CartesianContextProvider'; -import { BarElement, BarElementProps, BarElementSlotProps, BarElementSlots } from './BarElement'; +import { BarElement, BarElementSlotProps, BarElementSlots } from './BarElement'; import { AxisDefaultized, isBandScaleConfig, isPointScaleConfig } from '../models/axis'; import { FormatterResult } from '../models/seriesType/config'; import { BarItemIdentifier } from '../models'; @@ -12,6 +12,8 @@ import getColor from './getColor'; import { useChartId } from '../hooks'; import { AnimationData, CompletedBarData, MaskData } from './types'; import { BarClipPath } from './BarClipPath'; +import { BarLabelItemProps, BarLabelSlotProps, BarLabelSlots } from './BarLabel/BarLabelItem'; +import { BarLabelPlot } from './BarLabel/BarLabelPlot'; /** * Solution of the equations @@ -45,11 +47,11 @@ function getBandSize({ }; } -export interface BarPlotSlots extends BarElementSlots {} +export interface BarPlotSlots extends BarElementSlots, BarLabelSlots {} -export interface BarPlotSlotProps extends BarElementSlotProps {} +export interface BarPlotSlotProps extends BarElementSlotProps, BarLabelSlotProps {} -export interface BarPlotProps extends Pick { +export interface BarPlotProps extends Pick { /** * If `true`, animations are skipped. * @default false @@ -68,6 +70,16 @@ export interface BarPlotProps extends Pick ({ */ function BarPlot(props: BarPlotProps) { const { completedData, masksData } = useAggregatedData(); - const { skipAnimation, onItemClick, borderRadius, ...other } = props; + const { skipAnimation, onItemClick, borderRadius, barLabel, ...other } = props; const transition = useTransition(completedData, { keys: (bar) => `${bar.seriesId}-${bar.dataIndex}`, from: leaveStyle, @@ -329,6 +341,14 @@ function BarPlot(props: BarPlotProps) { return {barElement}; })} + {barLabel && ( + + )} ); } @@ -338,6 +358,14 @@ BarPlot.propTypes = { // | These PropTypes are generated from the TypeScript type definitions | // | To update them edit the TypeScript types and run "pnpm proptypes" | // ---------------------------------------------------------------------- + /** + * If provided, the function will be used to format the label of the bar. + * It can be set to 'value' to display the current value. + * @param {BarItem} item The item to format. + * @param {BarLabelContext} context data about the bar. + * @returns {string} The formatted label. + */ + barLabel: PropTypes.oneOfType([PropTypes.oneOf(['value']), PropTypes.func]), /** * Defines the border radius of the bar element. */ diff --git a/packages/x-charts/src/BarChart/index.ts b/packages/x-charts/src/BarChart/index.ts index b84ff348687d3..2c0b9ffa33ab9 100644 --- a/packages/x-charts/src/BarChart/index.ts +++ b/packages/x-charts/src/BarChart/index.ts @@ -1,3 +1,4 @@ export * from './BarChart'; export * from './BarPlot'; export * from './BarElement'; +export * from './BarLabel'; diff --git a/packages/x-charts/src/SparkLineChart/SparkLineChart.tsx b/packages/x-charts/src/SparkLineChart/SparkLineChart.tsx index 0f232fd0f0a69..437d7c763b4da 100644 --- a/packages/x-charts/src/SparkLineChart/SparkLineChart.tsx +++ b/packages/x-charts/src/SparkLineChart/SparkLineChart.tsx @@ -29,7 +29,7 @@ export interface SparkLineChartSlots LinePlotSlots, MarkPlotSlots, LineHighlightPlotSlots, - BarPlotSlots, + Omit, ChartsTooltipSlots {} export interface SparkLineChartSlotProps extends AreaPlotSlotProps, diff --git a/packages/x-charts/src/themeAugmentation/components.d.ts b/packages/x-charts/src/themeAugmentation/components.d.ts index 60862604ce60d..c9cc3dd4f2faf 100644 --- a/packages/x-charts/src/themeAugmentation/components.d.ts +++ b/packages/x-charts/src/themeAugmentation/components.d.ts @@ -40,6 +40,10 @@ export interface ChartsComponents { defaultProps?: ComponentsProps['MuiBarElement']; styleOverrides?: ComponentsOverrides['MuiBarElement']; }; + MuiBarLabel?: { + defaultProps?: ComponentsProps['MuiBarLabel']; + styleOverrides?: ComponentsOverrides['MuiBarLabel']; + }; MuiLineChart?: { defaultProps?: ComponentsProps['MuiLineChart']; }; diff --git a/packages/x-charts/src/themeAugmentation/index.js b/packages/x-charts/src/themeAugmentation/index.js index 9eb356e20e39d..cf3f797ea6e6a 100644 --- a/packages/x-charts/src/themeAugmentation/index.js +++ b/packages/x-charts/src/themeAugmentation/index.js @@ -1 +1 @@ -// Prefer to use `import type {} from '@mui/x-date-pickers/themeAugmentation';` instead to avoid importing an empty file. +// Prefer to use `import type {} from '@mui/x-charts/themeAugmentation';` instead to avoid importing an empty file. diff --git a/packages/x-charts/src/themeAugmentation/overrides.d.ts b/packages/x-charts/src/themeAugmentation/overrides.d.ts index 3a1dc5e2891ae..c2699e9f67d0a 100644 --- a/packages/x-charts/src/themeAugmentation/overrides.d.ts +++ b/packages/x-charts/src/themeAugmentation/overrides.d.ts @@ -1,3 +1,4 @@ +import { BarLabelClassKey } from '../BarChart'; import { BarElementClassKey } from '../BarChart/BarElement'; import { ChartsAxisClassKey } from '../ChartsAxis'; import { ChartsAxisHighlightClassKey } from '../ChartsAxisHighlight'; @@ -16,6 +17,8 @@ export interface PickersComponentNameToClassKey { // BarChart components MuiBarElement: BarElementClassKey; + MuiBarLabel: BarLabelClassKey; + // LineChart components MuiAreaElement: AreaElementClassKey; diff --git a/packages/x-charts/src/themeAugmentation/props.d.ts b/packages/x-charts/src/themeAugmentation/props.d.ts index 207d4f9d69ca7..0189a5b19e679 100644 --- a/packages/x-charts/src/themeAugmentation/props.d.ts +++ b/packages/x-charts/src/themeAugmentation/props.d.ts @@ -1,3 +1,4 @@ +import { BarLabelProps } from '../BarChart/BarLabel'; import { BarChartProps } from '../BarChart/BarChart'; import { BarElementProps } from '../BarChart/BarElement'; import { ChartsAxisProps } from '../ChartsAxis'; @@ -27,6 +28,7 @@ export interface ChartsComponentsPropsList { // BarChart components MuiBarChart: BarChartProps; MuiBarElement: BarElementProps; + MuiBarLabel: BarLabelProps; // LineChart components MuiLineChart: LineChartProps; MuiAreaElement: AreaElementProps; diff --git a/scripts/x-charts.exports.json b/scripts/x-charts.exports.json index 8e34fbddc6b84..f0848fb3231b1 100644 --- a/scripts/x-charts.exports.json +++ b/scripts/x-charts.exports.json @@ -35,7 +35,17 @@ { "name": "BarElementProps", "kind": "TypeAlias" }, { "name": "BarElementSlotProps", "kind": "Interface" }, { "name": "BarElementSlots", "kind": "Interface" }, + { "name": "BarItem", "kind": "TypeAlias" }, { "name": "BarItemIdentifier", "kind": "TypeAlias" }, + { "name": "BarLabel", "kind": "Function" }, + { "name": "barLabelClasses", "kind": "Variable" }, + { "name": "BarLabelClasses", "kind": "Interface" }, + { "name": "BarLabelClassKey", "kind": "TypeAlias" }, + { "name": "BarLabelContext", "kind": "TypeAlias" }, + { "name": "BarLabelOwnerState", "kind": "Interface" }, + { "name": "BarLabelProps", "kind": "TypeAlias" }, + { "name": "BarLabelSlotProps", "kind": "Interface" }, + { "name": "BarLabelSlots", "kind": "Interface" }, { "name": "BarPlot", "kind": "Function" }, { "name": "BarPlotProps", "kind": "Interface" }, { "name": "BarPlotSlotProps", "kind": "Interface" }, @@ -140,6 +150,7 @@ { "name": "getAxisHighlightUtilityClass", "kind": "Function" }, { "name": "getAxisUtilityClass", "kind": "Function" }, { "name": "getBarElementUtilityClass", "kind": "Function" }, + { "name": "getBarLabelUtilityClass", "kind": "Function" }, { "name": "getChartsGridUtilityClass", "kind": "Function" }, { "name": "getChartsTooltipUtilityClass", "kind": "Function" }, { "name": "getGaugeUtilityClass", "kind": "Function" },