From d246a1fba5135a624b60cdab64776199b03eff1a Mon Sep 17 00:00:00 2001 From: Alexandre Fauquette <45398769+alexfauquette@users.noreply.github.com> Date: Thu, 15 Feb 2024 16:13:40 +0100 Subject: [PATCH] [charts] Add Gauge component (#11996) Signed-off-by: Alexandre Fauquette <45398769+alexfauquette@users.noreply.github.com> Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Co-authored-by: Lukas --- docs/.link-check-errors.txt | 1 + docs/data/charts-component-api-pages.ts | 8 + docs/data/charts/gauge/ArcDesign.js | 28 +++ docs/data/charts/gauge/ArcDesign.tsx | 28 +++ docs/data/charts/gauge/ArcDesign.tsx.preview | 15 ++ docs/data/charts/gauge/ArcPlaygroundNoSnap.js | 90 +++++++ docs/data/charts/gauge/BasicGauges.js | 12 + docs/data/charts/gauge/BasicGauges.tsx | 12 + .../data/charts/gauge/BasicGauges.tsx.preview | 2 + docs/data/charts/gauge/CompositionExample.js | 47 ++++ docs/data/charts/gauge/CompositionExample.tsx | 47 ++++ .../gauge/CompositionExample.tsx.preview | 11 + .../charts/gauge/GaugeValueRangeNoSnap.js | 12 + .../charts/gauge/GaugeValueRangeNoSnap.tsx | 12 + .../gauge/GaugeValueRangeNoSnap.tsx.preview | 2 + .../data/charts/gauge/TextPlaygroundNoSnap.js | 90 +++++++ docs/data/charts/gauge/gauge.md | 135 ++++++++++- docs/data/pages.ts | 2 +- docs/pages/x/api/charts/gauge-container.js | 23 ++ docs/pages/x/api/charts/gauge-container.json | 44 ++++ docs/pages/x/api/charts/gauge.js | 23 ++ docs/pages/x/api/charts/gauge.json | 69 ++++++ .../modules/components/ChartComponentsGrid.js | 1 - docs/src/modules/components/DemoPropsForm.tsx | 13 +- .../gauge-container/gauge-container.json | 40 +++ .../api-docs/charts/gauge/gauge.json | 51 ++++ packages/x-charts/src/ChartsSurface.tsx | 6 +- packages/x-charts/src/Gauge/Gauge.tsx | 150 ++++++++++++ .../x-charts/src/Gauge/GaugeContainer.tsx | 228 ++++++++++++++++++ packages/x-charts/src/Gauge/GaugeProvider.tsx | 213 ++++++++++++++++ .../x-charts/src/Gauge/GaugeReferenceArc.tsx | 31 +++ packages/x-charts/src/Gauge/GaugeValueArc.tsx | 48 ++++ .../x-charts/src/Gauge/GaugeValueText.tsx | 65 +++++ packages/x-charts/src/Gauge/gaugeClasses.ts | 28 +++ packages/x-charts/src/Gauge/index.ts | 7 + packages/x-charts/src/Gauge/utils.ts | 88 +++++++ .../ResponsiveChartContainer.tsx | 90 +------ .../useChartContainerDimensions.ts | 88 +++++++ packages/x-charts/src/index.ts | 1 + scripts/buildApiDocs/chartsSettings/index.ts | 9 +- scripts/x-charts.exports.json | 14 ++ 41 files changed, 1780 insertions(+), 104 deletions(-) create mode 100644 docs/data/charts/gauge/ArcDesign.js create mode 100644 docs/data/charts/gauge/ArcDesign.tsx create mode 100644 docs/data/charts/gauge/ArcDesign.tsx.preview create mode 100644 docs/data/charts/gauge/ArcPlaygroundNoSnap.js create mode 100644 docs/data/charts/gauge/BasicGauges.js create mode 100644 docs/data/charts/gauge/BasicGauges.tsx create mode 100644 docs/data/charts/gauge/BasicGauges.tsx.preview create mode 100644 docs/data/charts/gauge/CompositionExample.js create mode 100644 docs/data/charts/gauge/CompositionExample.tsx create mode 100644 docs/data/charts/gauge/CompositionExample.tsx.preview create mode 100644 docs/data/charts/gauge/GaugeValueRangeNoSnap.js create mode 100644 docs/data/charts/gauge/GaugeValueRangeNoSnap.tsx create mode 100644 docs/data/charts/gauge/GaugeValueRangeNoSnap.tsx.preview create mode 100644 docs/data/charts/gauge/TextPlaygroundNoSnap.js create mode 100644 docs/pages/x/api/charts/gauge-container.js create mode 100644 docs/pages/x/api/charts/gauge-container.json create mode 100644 docs/pages/x/api/charts/gauge.js create mode 100644 docs/pages/x/api/charts/gauge.json create mode 100644 docs/translations/api-docs/charts/gauge-container/gauge-container.json create mode 100644 docs/translations/api-docs/charts/gauge/gauge.json create mode 100644 packages/x-charts/src/Gauge/Gauge.tsx create mode 100644 packages/x-charts/src/Gauge/GaugeContainer.tsx create mode 100644 packages/x-charts/src/Gauge/GaugeProvider.tsx create mode 100644 packages/x-charts/src/Gauge/GaugeReferenceArc.tsx create mode 100644 packages/x-charts/src/Gauge/GaugeValueArc.tsx create mode 100644 packages/x-charts/src/Gauge/GaugeValueText.tsx create mode 100644 packages/x-charts/src/Gauge/gaugeClasses.ts create mode 100644 packages/x-charts/src/Gauge/index.ts create mode 100644 packages/x-charts/src/Gauge/utils.ts create mode 100644 packages/x-charts/src/ResponsiveChartContainer/useChartContainerDimensions.ts diff --git a/docs/.link-check-errors.txt b/docs/.link-check-errors.txt index b753b2a7b704..87eaaff0f8d8 100644 --- a/docs/.link-check-errors.txt +++ b/docs/.link-check-errors.txt @@ -11,6 +11,7 @@ Broken links found by `yarn docs:link-check` that exist: - https://mui.com/system/styles/api/#serverstylesheets - https://mui.com/system/styles/api/#stylesprovider - https://mui.com/system/styles/api/#themeprovider +- https://mui.com/x/api/charts/gauge/#classes - https://mui.com/x/api/data-grid/data-grid/#DataGrid-prop-filterDebounceMs - https://mui.com/x/api/data-grid/data-grid/#props - https://mui.com/x/api/data-grid/data-grid/#slots diff --git a/docs/data/charts-component-api-pages.ts b/docs/data/charts-component-api-pages.ts index 6d8a0f19e4a1..0b2f1d2b55ac 100644 --- a/docs/data/charts-component-api-pages.ts +++ b/docs/data/charts-component-api-pages.ts @@ -97,6 +97,14 @@ const apiPages: MuiPage[] = [ pathname: '/x/api/charts/default-charts-legend', title: 'DefaultChartsLegend', }, + { + pathname: '/x/api/charts/gauge', + title: 'Gauge', + }, + { + pathname: '/x/api/charts/gauge-container', + title: 'GaugeContainer', + }, { pathname: '/x/api/charts/line-chart', title: 'LineChart', diff --git a/docs/data/charts/gauge/ArcDesign.js b/docs/data/charts/gauge/ArcDesign.js new file mode 100644 index 000000000000..48a4633df62e --- /dev/null +++ b/docs/data/charts/gauge/ArcDesign.js @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { Gauge, gaugeClasses } from '@mui/x-charts/Gauge'; + +const settings = { + width: 200, + height: 200, + value: 60, +}; + +export default function ArcDesign() { + return ( + ({ + [`& .${gaugeClasses.valueText}`]: { + fontSize: 40, + }, + [`& .${gaugeClasses.valueArc}`]: { + fill: '#52b202', + }, + [`& .${gaugeClasses.referenceArc}`]: { + fill: theme.palette.text.disabled, + }, + })} + /> + ); +} diff --git a/docs/data/charts/gauge/ArcDesign.tsx b/docs/data/charts/gauge/ArcDesign.tsx new file mode 100644 index 000000000000..48a4633df62e --- /dev/null +++ b/docs/data/charts/gauge/ArcDesign.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { Gauge, gaugeClasses } from '@mui/x-charts/Gauge'; + +const settings = { + width: 200, + height: 200, + value: 60, +}; + +export default function ArcDesign() { + return ( + ({ + [`& .${gaugeClasses.valueText}`]: { + fontSize: 40, + }, + [`& .${gaugeClasses.valueArc}`]: { + fill: '#52b202', + }, + [`& .${gaugeClasses.referenceArc}`]: { + fill: theme.palette.text.disabled, + }, + })} + /> + ); +} diff --git a/docs/data/charts/gauge/ArcDesign.tsx.preview b/docs/data/charts/gauge/ArcDesign.tsx.preview new file mode 100644 index 000000000000..281808d62fc7 --- /dev/null +++ b/docs/data/charts/gauge/ArcDesign.tsx.preview @@ -0,0 +1,15 @@ + ({ + [`& .${gaugeClasses.valueText}`]: { + fontSize: 40, + }, + [`& .${gaugeClasses.valueArc}`]: { + fill: '#52b202', + }, + [`& .${gaugeClasses.referenceArc}`]: { + fill: theme.palette.text.disabled, + }, + })} +/> \ No newline at end of file diff --git a/docs/data/charts/gauge/ArcPlaygroundNoSnap.js b/docs/data/charts/gauge/ArcPlaygroundNoSnap.js new file mode 100644 index 000000000000..5ab80a759206 --- /dev/null +++ b/docs/data/charts/gauge/ArcPlaygroundNoSnap.js @@ -0,0 +1,90 @@ +import * as React from 'react'; +import ChartsUsageDemo from 'docsx/src/modules/components/ChartsUsageDemo'; +import Paper from '@mui/material/Paper'; +import { Gauge, gaugeClasses } from '@mui/x-charts/Gauge'; + +export default function ArcPlaygroundNoSnap() { + return ( + ( + + + + )} + getCode={({ props }) => { + const { innerRadius, outerRadius, ...numberProps } = props; + return [ + `import { Gauge } from '@mui/x-charts/Gauge';`, + '', + ` ` ${name}={${value}}`, + ), + ...Object.entries({ innerRadius, outerRadius }).map( + ([name, value]) => ` ${name}="${value}%"`, + ), + '/>', + ].join('\n'); + }} + /> + ); +} diff --git a/docs/data/charts/gauge/BasicGauges.js b/docs/data/charts/gauge/BasicGauges.js new file mode 100644 index 000000000000..ce8503b6ca11 --- /dev/null +++ b/docs/data/charts/gauge/BasicGauges.js @@ -0,0 +1,12 @@ +import * as React from 'react'; +import Stack from '@mui/material/Stack'; +import { Gauge } from '@mui/x-charts/Gauge'; + +export default function BasicGauges() { + return ( + + + + + ); +} diff --git a/docs/data/charts/gauge/BasicGauges.tsx b/docs/data/charts/gauge/BasicGauges.tsx new file mode 100644 index 000000000000..ce8503b6ca11 --- /dev/null +++ b/docs/data/charts/gauge/BasicGauges.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; +import Stack from '@mui/material/Stack'; +import { Gauge } from '@mui/x-charts/Gauge'; + +export default function BasicGauges() { + return ( + + + + + ); +} diff --git a/docs/data/charts/gauge/BasicGauges.tsx.preview b/docs/data/charts/gauge/BasicGauges.tsx.preview new file mode 100644 index 000000000000..7edb64600ff6 --- /dev/null +++ b/docs/data/charts/gauge/BasicGauges.tsx.preview @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/data/charts/gauge/CompositionExample.js b/docs/data/charts/gauge/CompositionExample.js new file mode 100644 index 000000000000..764a6cf60b46 --- /dev/null +++ b/docs/data/charts/gauge/CompositionExample.js @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { + GaugeContainer, + GaugeValueArc, + GaugeReferenceArc, + useGaugeState, +} from '@mui/x-charts/Gauge'; + +function GaugePointer() { + const { valueAngle, outerRadius, cx, cy } = useGaugeState(); + + if (valueAngle === null) { + // No value to display + return null; + } + + const target = { + x: cx + outerRadius * Math.sin(valueAngle), + y: cy - outerRadius * Math.cos(valueAngle), + }; + return ( + + + + + ); +} + +export default function CompositionExample() { + return ( + + + + + + ); +} diff --git a/docs/data/charts/gauge/CompositionExample.tsx b/docs/data/charts/gauge/CompositionExample.tsx new file mode 100644 index 000000000000..764a6cf60b46 --- /dev/null +++ b/docs/data/charts/gauge/CompositionExample.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { + GaugeContainer, + GaugeValueArc, + GaugeReferenceArc, + useGaugeState, +} from '@mui/x-charts/Gauge'; + +function GaugePointer() { + const { valueAngle, outerRadius, cx, cy } = useGaugeState(); + + if (valueAngle === null) { + // No value to display + return null; + } + + const target = { + x: cx + outerRadius * Math.sin(valueAngle), + y: cy - outerRadius * Math.cos(valueAngle), + }; + return ( + + + + + ); +} + +export default function CompositionExample() { + return ( + + + + + + ); +} diff --git a/docs/data/charts/gauge/CompositionExample.tsx.preview b/docs/data/charts/gauge/CompositionExample.tsx.preview new file mode 100644 index 000000000000..2675af81dc02 --- /dev/null +++ b/docs/data/charts/gauge/CompositionExample.tsx.preview @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/docs/data/charts/gauge/GaugeValueRangeNoSnap.js b/docs/data/charts/gauge/GaugeValueRangeNoSnap.js new file mode 100644 index 000000000000..d6be879022e9 --- /dev/null +++ b/docs/data/charts/gauge/GaugeValueRangeNoSnap.js @@ -0,0 +1,12 @@ +import * as React from 'react'; +import Stack from '@mui/material/Stack'; +import { Gauge } from '@mui/x-charts/Gauge'; + +export default function GaugeValueRangeNoSnap() { + return ( + + + + + ); +} diff --git a/docs/data/charts/gauge/GaugeValueRangeNoSnap.tsx b/docs/data/charts/gauge/GaugeValueRangeNoSnap.tsx new file mode 100644 index 000000000000..d6be879022e9 --- /dev/null +++ b/docs/data/charts/gauge/GaugeValueRangeNoSnap.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; +import Stack from '@mui/material/Stack'; +import { Gauge } from '@mui/x-charts/Gauge'; + +export default function GaugeValueRangeNoSnap() { + return ( + + + + + ); +} diff --git a/docs/data/charts/gauge/GaugeValueRangeNoSnap.tsx.preview b/docs/data/charts/gauge/GaugeValueRangeNoSnap.tsx.preview new file mode 100644 index 000000000000..bd671b68b19b --- /dev/null +++ b/docs/data/charts/gauge/GaugeValueRangeNoSnap.tsx.preview @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/data/charts/gauge/TextPlaygroundNoSnap.js b/docs/data/charts/gauge/TextPlaygroundNoSnap.js new file mode 100644 index 000000000000..aa30b24d6df3 --- /dev/null +++ b/docs/data/charts/gauge/TextPlaygroundNoSnap.js @@ -0,0 +1,90 @@ +import * as React from 'react'; +import ChartsUsageDemo from 'docsx/src/modules/components/ChartsUsageDemo'; +import Paper from '@mui/material/Paper'; +import { Gauge, gaugeClasses } from '@mui/x-charts/Gauge'; + +export default function TextPlaygroundNoSnap() { + return ( + ( + + `${value} / ${valueMax}`} + /> + + )} + getCode={({ props }) => { + return [ + `import { Gauge, gaugeClasses } from '@mui/x-charts/Gauge';`, + '', + ` `${value} / ${valueMax}`', + ' }', + '/>', + ].join('\n'); + }} + /> + ); +} diff --git a/docs/data/charts/gauge/gauge.md b/docs/data/charts/gauge/gauge.md index ffa248998caf..8e64e50022d0 100644 --- a/docs/data/charts/gauge/gauge.md +++ b/docs/data/charts/gauge/gauge.md @@ -1,15 +1,140 @@ --- title: React Gauge chart productId: x-charts +components: Gauge, GaugeContainer +packageName: '@mui/x-charts' +githubLabel: 'component: charts' +waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/meter/ --- -# Charts - Gauge 🚧 +# Charts - Gauge

Gauge charts let the user evaluate metrics.

-:::warning -The Gauge Chart component isn't available yet, but you can upvote [**this GitHub issue**](https://github.com/mui/mui-x/issues/2903) to see it arrive sooner. +{{"component": "modules/components/ComponentLinkHeader.js", "design": false}} -Don't hesitate to leave a comment there to influence what gets built. -Especially if you already have a use case for this component, or if you're facing a pain point with your current solution. +## Basic gauge + +The Gauge displays a numeric value that varies within a defined range. + +{{"demo": "BasicGauges.js"}} + +## Value range + +The Gauge chart's value is provided through the `value` props, which accept a value range between 0 and 100. +To modify it, use the `valueMin` and `valueMax` props. + +{{"demo": "GaugeValueRangeNoSnap.js"}} + +## Arcs configuration + +You can modify the arc shape with the following props: + +- `startAngle` and `endAngle`: The angle range provided in degrees +- `innerRadius` and `outerRadius`: The arc's radii. It can be a fixed number of pixels or a percentage string, which will be a percent of the maximal available radius. +- `cornerRadius`: It can be a fixed number of pixels or a percentage string, which will be a percent of the maximal available radius. + +{{"demo": "ArcPlaygroundNoSnap.js"}} + +:::info +Notice that the arc position is computed to let the Gauge chart take as much space as possible in the drawing area. + +Use the `cx` and/or `cy` props to fix the coordinate of the arc center. ::: + +## Text configuration + +By default, the Gauge displays the value in the center of the arc. +To modify it, use the `text` prop. + +This prop can be a string, or a formatter. +In the second case, the formatter argument contains the `value`, `valueMin` and `valueMax`. + +To modify the text's layout, use the `gaugeClasses.valueText` class name. + +{{"demo": "TextPlaygroundNoSnap.js"}} + +## Arc design + +To customize the Gauge styles, use the `chartsGaugeClasses` export to pull class names from different parts of the component, such as `valueText`, `valueArc`, and `referenceArc`. + +For a full reference list, visit the [API page](/x/api/charts/gauge/#classes). + +{{"demo": "ArcDesign.js"}} + +## Adding elements + +### Using the default Gauge + +To insert more elements into the Gauge chart, the first option would be to add them as children, which means they will be stacked on top of the default rendering. + +```tsx +import { Gauge } from '@mui/x-charts/Gauge'; + + + +; +``` + +### Using the Gauge container + +The second option is to make use of the following elements that are available within the Gauge module: + +- GaugeReferenceArc +- GaugeValueArc +- GaugeValueText + +```tsx +import { + GaugeContainer, + Gauge, + GaugeReferenceArc, + GaugeValueArc, +} from '@mui/x-charts/Gauge'; + + + + + +; +``` + +### Creating your components + +To create your own components, use the `useGaugeState` hook which provides all you need about the gauge configuration: + +- information about the value: `value`, `valueMin`, `valueMax`; +- information to plot the arc: `startAngle`, `endAngle`, `outerRadius`, `innerRadius`, `cornerRadius`, `cx`, and `cy`; +- computed values: + - `maxRadius` the maximal radius that can fit the drawing area; + - `valueAngle` the angle associated with the current value. + +{{"demo": "CompositionExample.js"}} + +## Accessibility + +The MUI X Gauge chart is compliant with the [Meter ARIA pattern](https://www.w3.org/WAI/ARIA/apg/patterns/meter/), which includes the addition of the `meter` role to the parent container and correct usage of the `aria-valuenow`, `aria-valuemin`, and `aria-valuemax` attributes. + +### Label + +If a visible label is available, reference it by adding `aria-labelledby` attribute. +Otherwise, the label can be provided by `aria-label`. + +### Presentation + +Assistive technologies often present the value as a percentage. +This can be modified by providing `aria-valuetext` attribute. + +For example, a battery level indicator is better with an hour-long duration. + +```jsx +

+ Battery level +

+ +``` diff --git a/docs/data/pages.ts b/docs/data/pages.ts index c4c6e8367f43..62529e1d67f8 100644 --- a/docs/data/pages.ts +++ b/docs/data/pages.ts @@ -407,6 +407,7 @@ const pages: MuiPage[] = [ { pathname: '/x/react-charts/bar-demo', title: 'Demos' }, ], }, + { pathname: '/x/react-charts/gauge', title: 'Gauge' }, { pathname: '/x/react-charts-lines', title: 'Lines', @@ -458,7 +459,6 @@ const pages: MuiPage[] = [ pathname: '/x/react-charts-future', subheader: 'Future components', children: [ - { pathname: '/x/react-charts/gauge', title: 'Gauge', planned: true }, { pathname: '/x/react-charts/heat-map', title: 'Heatmap', planned: true }, { pathname: '/x/react-charts/radar', title: 'Radar', planned: true }, { pathname: '/x/react-charts/tree-map', title: 'Tree map', planned: true }, diff --git a/docs/pages/x/api/charts/gauge-container.js b/docs/pages/x/api/charts/gauge-container.js new file mode 100644 index 000000000000..7b2fc51b7afc --- /dev/null +++ b/docs/pages/x/api/charts/gauge-container.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 './gauge-container.json'; + +export default function Page(props) { + const { descriptions, pageContent } = props; + return ; +} + +Page.getInitialProps = () => { + const req = require.context( + 'docsx/translations/api-docs/charts/gauge-container', + false, + /\.\/gauge-container.*.json$/, + ); + const descriptions = mapApiPageTranslations(req); + + return { + descriptions, + pageContent: jsonPageContent, + }; +}; diff --git a/docs/pages/x/api/charts/gauge-container.json b/docs/pages/x/api/charts/gauge-container.json new file mode 100644 index 000000000000..e23ab70ab0da --- /dev/null +++ b/docs/pages/x/api/charts/gauge-container.json @@ -0,0 +1,44 @@ +{ + "props": { + "cornerRadius": { + "type": { "name": "union", "description": "number
| string" }, + "default": "0" + }, + "cx": { "type": { "name": "union", "description": "number
| string" } }, + "cy": { "type": { "name": "union", "description": "number
| string" } }, + "disableAxisListener": { "type": { "name": "bool" }, "default": "false" }, + "endAngle": { "type": { "name": "number" }, "default": "360" }, + "height": { "type": { "name": "number" }, "default": "undefined" }, + "innerRadius": { + "type": { "name": "union", "description": "number
| string" }, + "default": "'80%'" + }, + "margin": { + "type": { + "name": "shape", + "description": "{ bottom?: number, left?: number, right?: number, top?: number }" + }, + "default": "object Depends on the charts type." + }, + "outerRadius": { + "type": { "name": "union", "description": "number
| string" }, + "default": "'100%'" + }, + "startAngle": { "type": { "name": "number" }, "default": "0" }, + "value": { "type": { "name": "number" } }, + "valueMax": { "type": { "name": "number" }, "default": "100" }, + "valueMin": { "type": { "name": "number" }, "default": "0" }, + "width": { "type": { "name": "number" }, "default": "undefined" } + }, + "name": "GaugeContainer", + "imports": [ + "import { GaugeContainer } from '@mui/x-charts/Gauge';", + "import { GaugeContainer } from '@mui/x-charts';" + ], + "classes": [], + "muiName": "MuiGaugeContainer", + "filename": "/packages/x-charts/src/Gauge/GaugeContainer.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/x/api/charts/gauge.js b/docs/pages/x/api/charts/gauge.js new file mode 100644 index 000000000000..4b4dfa202507 --- /dev/null +++ b/docs/pages/x/api/charts/gauge.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 './gauge.json'; + +export default function Page(props) { + const { descriptions, pageContent } = props; + return ; +} + +Page.getInitialProps = () => { + const req = require.context( + 'docsx/translations/api-docs/charts/gauge', + false, + /\.\/gauge.*.json$/, + ); + const descriptions = mapApiPageTranslations(req); + + return { + descriptions, + pageContent: jsonPageContent, + }; +}; diff --git a/docs/pages/x/api/charts/gauge.json b/docs/pages/x/api/charts/gauge.json new file mode 100644 index 000000000000..48c9dfb790cc --- /dev/null +++ b/docs/pages/x/api/charts/gauge.json @@ -0,0 +1,69 @@ +{ + "props": { + "cornerRadius": { + "type": { "name": "union", "description": "number
| string" }, + "default": "0" + }, + "cx": { "type": { "name": "union", "description": "number
| string" } }, + "cy": { "type": { "name": "union", "description": "number
| string" } }, + "disableAxisListener": { "type": { "name": "bool" }, "default": "false" }, + "endAngle": { "type": { "name": "number" }, "default": "360" }, + "height": { "type": { "name": "number" }, "default": "undefined" }, + "innerRadius": { + "type": { "name": "union", "description": "number
| string" }, + "default": "'80%'" + }, + "margin": { + "type": { + "name": "shape", + "description": "{ bottom?: number, left?: number, right?: number, top?: number }" + }, + "default": "object Depends on the charts type." + }, + "outerRadius": { + "type": { "name": "union", "description": "number
| string" }, + "default": "'100%'" + }, + "startAngle": { "type": { "name": "number" }, "default": "0" }, + "value": { "type": { "name": "number" } }, + "valueMax": { "type": { "name": "number" }, "default": "100" }, + "valueMin": { "type": { "name": "number" }, "default": "0" }, + "width": { "type": { "name": "number" }, "default": "undefined" } + }, + "name": "Gauge", + "imports": [ + "import { Gauge } from '@mui/x-charts/Gauge';", + "import { Gauge } from '@mui/x-charts';" + ], + "classes": [ + { + "key": "referenceArc", + "className": "MuiGauge-referenceArc", + "description": "Styles applied to the arc diplaying the range of available values.", + "isGlobal": false + }, + { + "key": "root", + "className": "MuiGauge-root", + "description": "Styles applied to the root element.", + "isGlobal": false + }, + { + "key": "valueArc", + "className": "MuiGauge-valueArc", + "description": "Styles applied to the arc diplaying the value.", + "isGlobal": false + }, + { + "key": "valueText", + "className": "MuiGauge-valueText", + "description": "Styles applied to the value text.", + "isGlobal": false + } + ], + "muiName": "MuiGauge", + "filename": "/packages/x-charts/src/Gauge/Gauge.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/src/modules/components/ChartComponentsGrid.js b/docs/src/modules/components/ChartComponentsGrid.js index d121425eb6c9..0e00d7ad8078 100644 --- a/docs/src/modules/components/ChartComponentsGrid.js +++ b/docs/src/modules/components/ChartComponentsGrid.js @@ -45,7 +45,6 @@ function components() { srcLight: '/static/x/component-illustrations/gauge-light.png', srcDark: '/static/x/component-illustrations/gauge-dark.png', href: '/x/react-charts/gauge/', - planned: true, }, { title: 'Heatmap', diff --git a/docs/src/modules/components/DemoPropsForm.tsx b/docs/src/modules/components/DemoPropsForm.tsx index 8f0d4a5eaefb..87cc5e0666b5 100644 --- a/docs/src/modules/components/DemoPropsForm.tsx +++ b/docs/src/modules/components/DemoPropsForm.tsx @@ -367,14 +367,15 @@ export default function ChartDemoPropsForm( ? (props[propName] as number) : (defaultValue as string) } - onChange={(event) => + onChange={(event) => { + if (Number.isNaN(Number.parseFloat(event.target.value))) { + return; + } setProps((latestProps) => ({ ...latestProps, - [propName]: Number.isNaN(event.target.value) - ? undefined - : Number.parseFloat(event.target.value), - })) - } + [propName]: Number.parseFloat(event.target.value), + })); + }} sx={{ textTransform: 'capitalize', [`& .${inputClasses.root}`]: { diff --git a/docs/translations/api-docs/charts/gauge-container/gauge-container.json b/docs/translations/api-docs/charts/gauge-container/gauge-container.json new file mode 100644 index 000000000000..06c25cdf282c --- /dev/null +++ b/docs/translations/api-docs/charts/gauge-container/gauge-container.json @@ -0,0 +1,40 @@ +{ + "componentDescription": "", + "propDescriptions": { + "cornerRadius": { + "description": "The radius applied to arc corners (similar to border radius). Set it to '50%' to get rounded arc." + }, + "cx": { + "description": "The x coordinate of the arc center. Can be a number (in px) or a string with a percentage such as '50%'. The '100%' is the width the drawing area." + }, + "cy": { + "description": "The y coordinate of the arc center. Can be a number (in px) or a string with a percentage such as '50%'. The '100%' is the height the drawing area." + }, + "disableAxisListener": { + "description": "If true, the charts will not listen to the mouse move event. It might break interactive features, but will improve performance." + }, + "endAngle": { "description": "The end angle (deg)." }, + "height": { + "description": "The height of the chart in px. If not defined, it takes the height of the parent element." + }, + "innerRadius": { + "description": "The radius between circle center and the begining of the arc. Can be a number (in px) or a string with a percentage such as '50%'. The '100%' is the maximal radius that fit into the drawing area." + }, + "margin": { + "description": "The margin between the SVG and the drawing area. It's used for leaving some space for extra information such as the x- and y-axis or legend. Accepts an object with the optional properties: top, bottom, left, and right." + }, + "outerRadius": { + "description": "The radius between circle center and the end of the arc. Can be a number (in px) or a string with a percentage such as '50%'. The '100%' is the maximal radius that fit into the drawing area." + }, + "startAngle": { "description": "The start angle (deg)." }, + "value": { + "description": "The value of the gauge. Set to null to not display a value." + }, + "valueMax": { "description": "The maximal value of the gauge." }, + "valueMin": { "description": "The minimal value of the gauge." }, + "width": { + "description": "The width of the chart in px. If not defined, it takes the width of the parent element." + } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/charts/gauge/gauge.json b/docs/translations/api-docs/charts/gauge/gauge.json new file mode 100644 index 000000000000..9e20b3914942 --- /dev/null +++ b/docs/translations/api-docs/charts/gauge/gauge.json @@ -0,0 +1,51 @@ +{ + "componentDescription": "", + "propDescriptions": { + "cornerRadius": { + "description": "The radius applied to arc corners (similar to border radius). Set it to '50%' to get rounded arc." + }, + "cx": { + "description": "The x coordinate of the arc center. Can be a number (in px) or a string with a percentage such as '50%'. The '100%' is the width the drawing area." + }, + "cy": { + "description": "The y coordinate of the arc center. Can be a number (in px) or a string with a percentage such as '50%'. The '100%' is the height the drawing area." + }, + "disableAxisListener": { + "description": "If true, the charts will not listen to the mouse move event. It might break interactive features, but will improve performance." + }, + "endAngle": { "description": "The end angle (deg)." }, + "height": { + "description": "The height of the chart in px. If not defined, it takes the height of the parent element." + }, + "innerRadius": { + "description": "The radius between circle center and the begining of the arc. Can be a number (in px) or a string with a percentage such as '50%'. The '100%' is the maximal radius that fit into the drawing area." + }, + "margin": { + "description": "The margin between the SVG and the drawing area. It's used for leaving some space for extra information such as the x- and y-axis or legend. Accepts an object with the optional properties: top, bottom, left, and right." + }, + "outerRadius": { + "description": "The radius between circle center and the end of the arc. Can be a number (in px) or a string with a percentage such as '50%'. The '100%' is the maximal radius that fit into the drawing area." + }, + "startAngle": { "description": "The start angle (deg)." }, + "value": { + "description": "The value of the gauge. Set to null to not display a value." + }, + "valueMax": { "description": "The maximal value of the gauge." }, + "valueMin": { "description": "The minimal value of the gauge." }, + "width": { + "description": "The width of the chart in px. If not defined, it takes the width of the parent element." + } + }, + "classDescriptions": { + "referenceArc": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the arc diplaying the range of available values" + }, + "root": { "description": "Styles applied to the root element." }, + "valueArc": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the arc diplaying the value" + }, + "valueText": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the value text" } + } +} diff --git a/packages/x-charts/src/ChartsSurface.tsx b/packages/x-charts/src/ChartsSurface.tsx index eb25424fed83..3cbcac24ebf0 100644 --- a/packages/x-charts/src/ChartsSurface.tsx +++ b/packages/x-charts/src/ChartsSurface.tsx @@ -48,6 +48,8 @@ const ChartsSurface = React.forwardRef(functi viewBox, disableAxisListener = false, className, + title, + desc, ...other } = props; const svgView = { width, height, x: 0, y: 0, ...viewBox }; @@ -62,8 +64,8 @@ const ChartsSurface = React.forwardRef(functi ref={ref} {...other} > - {props.title} - {props.desc} + {title} + {desc} {children} ); diff --git a/packages/x-charts/src/Gauge/Gauge.tsx b/packages/x-charts/src/Gauge/Gauge.tsx new file mode 100644 index 000000000000..1884344bec29 --- /dev/null +++ b/packages/x-charts/src/Gauge/Gauge.tsx @@ -0,0 +1,150 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import composeClasses from '@mui/utils/composeClasses'; +import { GaugeContainer, GaugeContainerProps } from './GaugeContainer'; +import { GaugeValueArc } from './GaugeValueArc'; +import { GaugeReferenceArc } from './GaugeReferenceArc'; +import { GaugeClasses, getGaugeUtilityClass } from './gaugeClasses'; +import { GaugeValueText, GaugeValueTextProps } from './GaugeValueText'; + +export interface GaugeProps extends GaugeContainerProps, Pick { + classes?: Partial; + children?: React.ReactNode; +} + +const useUtilityClasses = (props: GaugeProps) => { + const { classes } = props; + + const slots = { + root: ['root'], + valueArc: ['valueArc'], + referenceArc: ['referenceArc'], + valueText: ['valueText'], + }; + + return composeClasses(slots, getGaugeUtilityClass, classes); +}; + +function Gauge(props: GaugeProps) { + const { text, children, classes: propsClasses, ...other } = props; + const classes = useUtilityClasses(props); + return ( + + + + + {children} + + ); +} + +Gauge.propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + children: PropTypes.node, + classes: PropTypes.object, + className: PropTypes.string, + /** + * The radius applied to arc corners (similar to border radius). + * Set it to '50%' to get rounded arc. + * @default 0 + */ + cornerRadius: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * The x coordinate of the arc center. + * Can be a number (in px) or a string with a percentage such as '50%'. + * The '100%' is the width the drawing area. + */ + cx: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * The y coordinate of the arc center. + * Can be a number (in px) or a string with a percentage such as '50%'. + * The '100%' is the height the drawing area. + */ + cy: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + desc: PropTypes.string, + /** + * If `true`, the charts will not listen to the mouse move event. + * It might break interactive features, but will improve performance. + * @default false + */ + disableAxisListener: PropTypes.bool, + /** + * The end angle (deg). + * @default 360 + */ + endAngle: PropTypes.number, + /** + * The height of the chart in px. If not defined, it takes the height of the parent element. + * @default undefined + */ + height: PropTypes.number, + /** + * The radius between circle center and the begining of the arc. + * Can be a number (in px) or a string with a percentage such as '50%'. + * The '100%' is the maximal radius that fit into the drawing area. + * @default '80%' + */ + innerRadius: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * The margin between the SVG and the drawing area. + * It's used for leaving some space for extra information such as the x- and y-axis or legend. + * Accepts an object with the optional properties: `top`, `bottom`, `left`, and `right`. + * @default object Depends on the charts type. + */ + margin: PropTypes.shape({ + bottom: PropTypes.number, + left: PropTypes.number, + right: PropTypes.number, + top: PropTypes.number, + }), + /** + * The radius between circle center and the end of the arc. + * Can be a number (in px) or a string with a percentage such as '50%'. + * The '100%' is the maximal radius that fit into the drawing area. + * @default '100%' + */ + outerRadius: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * The start angle (deg). + * @default 0 + */ + startAngle: PropTypes.number, + sx: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), + PropTypes.func, + PropTypes.object, + ]), + text: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + title: PropTypes.string, + /** + * The value of the gauge. + * Set to `null` to not display a value. + */ + value: PropTypes.number, + /** + * The maximal value of the gauge. + * @default 100 + */ + valueMax: PropTypes.number, + /** + * The minimal value of the gauge. + * @default 0 + */ + valueMin: PropTypes.number, + viewBox: PropTypes.shape({ + height: PropTypes.number, + width: PropTypes.number, + x: PropTypes.number, + y: PropTypes.number, + }), + /** + * The width of the chart in px. If not defined, it takes the width of the parent element. + * @default undefined + */ + width: PropTypes.number, +} as any; + +export { Gauge }; diff --git a/packages/x-charts/src/Gauge/GaugeContainer.tsx b/packages/x-charts/src/Gauge/GaugeContainer.tsx new file mode 100644 index 000000000000..8b00d695c502 --- /dev/null +++ b/packages/x-charts/src/Gauge/GaugeContainer.tsx @@ -0,0 +1,228 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import useForkRef from '@mui/utils/useForkRef'; +import { styled } from '@mui/material/styles'; +import { useChartContainerDimensions } from '../ResponsiveChartContainer/useChartContainerDimensions'; +import { ChartsSurface, ChartsSurfaceProps } from '../ChartsSurface'; +import { DrawingProvider, DrawingProviderProps } from '../context/DrawingProvider'; +import { GaugeProvider, GaugeProviderProps } from './GaugeProvider'; + +export interface GaugeContainerProps + extends Omit, + Omit, + Omit { + /** + * The width of the chart in px. If not defined, it takes the width of the parent element. + * @default undefined + */ + width?: number; + /** + * The height of the chart in px. If not defined, it takes the height of the parent element. + * @default undefined + */ + height?: number; + children?: React.ReactNode; +} + +const ResizableContainer = styled('div', { + name: 'MuiGauge', + slot: 'Container', +})<{ ownerState: Pick }>(({ ownerState, theme }) => ({ + width: ownerState.width ?? '100%', + height: ownerState.height ?? '100%', + display: 'flex', + position: 'relative', + flexGrow: 1, + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', + '&>svg': { + width: '100%', + height: '100%', + }, + '& text': { + fill: (theme.vars || theme).palette.text.primary, + }, +})); + +const GaugeContainer = React.forwardRef(function GaugeContainer(props: GaugeContainerProps, ref) { + const { + width: inWidth, + height: inHeight, + margin, + title, + desc, + value, + valueMin = 0, + valueMax = 100, + startAngle, + endAngle, + outerRadius, + innerRadius, + cornerRadius, + cx, + cy, + children, + ...other + } = props; + const [containerRef, width, height] = useChartContainerDimensions(inWidth, inHeight); + + const svgRef = React.useRef(null); + const handleRef = useForkRef(ref, svgRef); + + return ( + + {width && height ? ( + + + + + + ) : null} + + ); +}); + +GaugeContainer.propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + children: PropTypes.node, + className: PropTypes.string, + /** + * The radius applied to arc corners (similar to border radius). + * Set it to '50%' to get rounded arc. + * @default 0 + */ + cornerRadius: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * The x coordinate of the arc center. + * Can be a number (in px) or a string with a percentage such as '50%'. + * The '100%' is the width the drawing area. + */ + cx: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * The y coordinate of the arc center. + * Can be a number (in px) or a string with a percentage such as '50%'. + * The '100%' is the height the drawing area. + */ + cy: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + desc: PropTypes.string, + /** + * If `true`, the charts will not listen to the mouse move event. + * It might break interactive features, but will improve performance. + * @default false + */ + disableAxisListener: PropTypes.bool, + /** + * The end angle (deg). + * @default 360 + */ + endAngle: PropTypes.number, + /** + * The height of the chart in px. If not defined, it takes the height of the parent element. + * @default undefined + */ + height: PropTypes.number, + /** + * The radius between circle center and the begining of the arc. + * Can be a number (in px) or a string with a percentage such as '50%'. + * The '100%' is the maximal radius that fit into the drawing area. + * @default '80%' + */ + innerRadius: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * The margin between the SVG and the drawing area. + * It's used for leaving some space for extra information such as the x- and y-axis or legend. + * Accepts an object with the optional properties: `top`, `bottom`, `left`, and `right`. + * @default object Depends on the charts type. + */ + margin: PropTypes.shape({ + bottom: PropTypes.number, + left: PropTypes.number, + right: PropTypes.number, + top: PropTypes.number, + }), + /** + * The radius between circle center and the end of the arc. + * Can be a number (in px) or a string with a percentage such as '50%'. + * The '100%' is the maximal radius that fit into the drawing area. + * @default '100%' + */ + outerRadius: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * The start angle (deg). + * @default 0 + */ + startAngle: PropTypes.number, + sx: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), + PropTypes.func, + PropTypes.object, + ]), + title: PropTypes.string, + /** + * The value of the gauge. + * Set to `null` to not display a value. + */ + value: PropTypes.number, + /** + * The maximal value of the gauge. + * @default 100 + */ + valueMax: PropTypes.number, + /** + * The minimal value of the gauge. + * @default 0 + */ + valueMin: PropTypes.number, + viewBox: PropTypes.shape({ + height: PropTypes.number, + width: PropTypes.number, + x: PropTypes.number, + y: PropTypes.number, + }), + /** + * The width of the chart in px. If not defined, it takes the width of the parent element. + * @default undefined + */ + width: PropTypes.number, +} as any; + +export { GaugeContainer }; diff --git a/packages/x-charts/src/Gauge/GaugeProvider.tsx b/packages/x-charts/src/Gauge/GaugeProvider.tsx new file mode 100644 index 000000000000..ffdb99ecb0df --- /dev/null +++ b/packages/x-charts/src/Gauge/GaugeProvider.tsx @@ -0,0 +1,213 @@ +// @ignore - do not document. +import * as React from 'react'; +import { DrawingContext } from '../context/DrawingProvider'; +import { getPercentageValue } from '../internals/utils'; +import { getArcRatios, getAvailableRadius } from './utils'; + +interface CircularConfig { + /** + * The start angle (deg). + * @default 0 + */ + startAngle?: number; + /** + * The end angle (deg). + * @default 360 + */ + endAngle?: number; + /** + * The radius between circle center and the begining of the arc. + * Can be a number (in px) or a string with a percentage such as '50%'. + * The '100%' is the maximal radius that fit into the drawing area. + * @default '80%' + */ + innerRadius?: number | string; + /** + * The radius between circle center and the end of the arc. + * Can be a number (in px) or a string with a percentage such as '50%'. + * The '100%' is the maximal radius that fit into the drawing area. + * @default '100%' + */ + outerRadius?: number | string; + /** + * The radius applied to arc corners (similar to border radius). + * Set it to '50%' to get rounded arc. + * @default 0 + */ + cornerRadius?: number | string; + /** + * The x coordinate of the arc center. + * Can be a number (in px) or a string with a percentage such as '50%'. + * The '100%' is the width the drawing area. + */ + cx?: number | string; + /** + * The y coordinate of the arc center. + * Can be a number (in px) or a string with a percentage such as '50%'. + * The '100%' is the height the drawing area. + */ + cy?: number | string; +} + +interface ProcessedCircularConfig { + /** + * The start angle (rad). + */ + startAngle: number; + /** + * The end angle (rad). + */ + endAngle: number; + /** + * The radius between circle center and the begining of the arc. + */ + innerRadius: number; + /** + * The radius between circle center and the end of the arc. + */ + outerRadius: number; + /** + * The radius applied to arc corners (similar to border radius). + */ + cornerRadius: number; + /** + * The x coordinate of the pie center. + */ + cx: number; + /** + * The y coordinate of the pie center. + */ + cy: number; +} + +interface GaugeConfig { + /** + * The value of the gauge. + * Set to `null` to not display a value. + */ + value?: number | null; + /** + * The minimal value of the gauge. + * @default 0 + */ + valueMin?: number; + /** + * The maximal value of the gauge. + * @default 100 + */ + valueMax?: number; +} + +export const GaugeContext = React.createContext< + Required & + ProcessedCircularConfig & { + /** + * The maximal radius from (cx, cy) that fits the arc in the drawing area. + */ + maxRadius: number; + /** + * The angle (rad) associated to the current value. + */ + valueAngle: null | number; + } +>({ + value: null, + valueMin: 0, + valueMax: 0, + startAngle: 0, + endAngle: 0, + innerRadius: 0, + outerRadius: 0, + cornerRadius: 0, + cx: 0, + cy: 0, + maxRadius: 0, + valueAngle: null, +}); + +export interface GaugeProviderProps extends GaugeConfig, CircularConfig { + children: React.ReactNode; +} + +export function GaugeProvider(props: GaugeProviderProps) { + const { + value = null, + valueMin = 0, + valueMax = 100, + startAngle = 0, + endAngle = 360, + outerRadius: outerRadiusParam, + innerRadius: innerRadiusParam, + cornerRadius: cornerRadiusParam, + cx: cxParam, + cy: cyParam, + children, + } = props; + + const { width, height, top, left } = React.useContext(DrawingContext); + + const ratios = getArcRatios(startAngle, endAngle); + + const innerCx = cxParam ? getPercentageValue(cxParam, width) : ratios.cx * width; + const innerCy = cyParam ? getPercentageValue(cyParam, height) : ratios.cy * height; + + let cx = left + innerCx; + let cy = top + innerCy; + + const maxRadius = getAvailableRadius(innerCx, innerCy, width, height, ratios); + + // If the center is not defined, after computation of the available radius, udpate the center to use the remaining space. + if (cxParam === undefined) { + const usedWidth = maxRadius * (ratios.maxX - ratios.minX); + cx = left + (width - usedWidth) / 2 + ratios.cx * usedWidth; + } + if (cyParam === undefined) { + const usedHeight = maxRadius * (ratios.maxY - ratios.minY); + cy = top + (height - usedHeight) / 2 + ratios.cy * usedHeight; + } + + const outerRadius = getPercentageValue(outerRadiusParam ?? maxRadius, maxRadius); + const innerRadius = getPercentageValue(innerRadiusParam ?? '80%', maxRadius); + const cornerRadius = getPercentageValue(cornerRadiusParam ?? 0, outerRadius - innerRadius); + + const contextValue = React.useMemo(() => { + const startAngleRad = (Math.PI * startAngle) / 180; + const endAngleRad = (Math.PI * endAngle) / 180; + return { + value, + valueMin, + valueMax, + startAngle: startAngleRad, + endAngle: endAngleRad, + outerRadius, + innerRadius, + cornerRadius, + cx, + cy, + maxRadius, + valueAngle: + value === null + ? null + : startAngleRad + + ((endAngleRad - startAngleRad) * (value - valueMin)) / (valueMax - valueMin), + }; + }, [ + value, + valueMin, + valueMax, + startAngle, + endAngle, + outerRadius, + innerRadius, + cornerRadius, + cx, + cy, + maxRadius, + ]); + + return {children}; +} + +export function useGaugeState() { + return React.useContext(GaugeContext); +} diff --git a/packages/x-charts/src/Gauge/GaugeReferenceArc.tsx b/packages/x-charts/src/Gauge/GaugeReferenceArc.tsx new file mode 100644 index 000000000000..55a0308362cc --- /dev/null +++ b/packages/x-charts/src/Gauge/GaugeReferenceArc.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { arc as d3Arc } from 'd3-shape'; +import { styled } from '@mui/material/styles'; +import { useGaugeState } from './GaugeProvider'; + +const StyledPath = styled('path', { + name: 'MuiGauge', + slot: 'ReferenceArc', + overridesResolver: (props, styles) => styles.referenceArc, +})(({ theme }) => ({ + fill: (theme.vars || theme).palette.divider, +})); + +export function GaugeReferenceArc(props: React.ComponentProps<'path'>) { + const { startAngle, endAngle, outerRadius, innerRadius, cornerRadius, cx, cy } = useGaugeState(); + + return ( + + ); +} diff --git a/packages/x-charts/src/Gauge/GaugeValueArc.tsx b/packages/x-charts/src/Gauge/GaugeValueArc.tsx new file mode 100644 index 000000000000..3c8bb81a5c6f --- /dev/null +++ b/packages/x-charts/src/Gauge/GaugeValueArc.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import { arc as d3Arc } from 'd3-shape'; +import { styled } from '@mui/material/styles'; +import { useGaugeState } from './GaugeProvider'; + +const StyledPath = styled('path', { + name: 'MuiGauge', + slot: 'ReferenceArc', + overridesResolver: (props, styles) => styles.referenceArc, +})(({ theme }) => ({ + fill: (theme.vars || theme).palette.primary.main, +})); + +export function GaugeValueArc(props: React.ComponentProps<'path'>) { + const { + value, + valueMin, + valueMax, + startAngle, + endAngle, + outerRadius, + innerRadius, + cornerRadius, + cx, + cy, + } = useGaugeState(); + + if (value === null) { + return null; + } + const valueAngle = + startAngle + ((value - valueMin) / (valueMax - valueMin)) * (endAngle - startAngle); + + return ( + + ); +} diff --git a/packages/x-charts/src/Gauge/GaugeValueText.tsx b/packages/x-charts/src/Gauge/GaugeValueText.tsx new file mode 100644 index 000000000000..ace6f9607344 --- /dev/null +++ b/packages/x-charts/src/Gauge/GaugeValueText.tsx @@ -0,0 +1,65 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { useGaugeState } from './GaugeProvider'; +import { ChartsText, ChartsTextProps } from '../ChartsText'; + +export interface GaugeFormatterParams { + value: number | null; + valueMin: number; + valueMax: number; +} + +export interface GaugeValueTextProps extends Omit { + text?: string | ((params: GaugeFormatterParams) => string | null); +} + +function defaultFormatter({ value }: GaugeFormatterParams) { + return value === null ? 'NaN' : value.toLocaleString(); +} +function GaugeValueText(props: GaugeValueTextProps) { + const { text = defaultFormatter, className, ...other } = props; + + const { value, valueMin, valueMax, cx, cy } = useGaugeState(); + + const formattedText = typeof text === 'function' ? text({ value, valueMin, valueMax }) : text; + + if (formattedText === null) { + return null; + } + + return ( + + + + ); +} + +GaugeValueText.propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * Height of a text line (in `em`). + */ + lineHeight: PropTypes.number, + /** + * If `true`, the line width is computed. + * @default false + */ + needsComputation: PropTypes.bool, + ownerState: PropTypes.any, + /** + * Style applied to text elements. + */ + style: PropTypes.object, + text: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), +} as any; + +export { GaugeValueText }; diff --git a/packages/x-charts/src/Gauge/gaugeClasses.ts b/packages/x-charts/src/Gauge/gaugeClasses.ts new file mode 100644 index 000000000000..1fdca8354d35 --- /dev/null +++ b/packages/x-charts/src/Gauge/gaugeClasses.ts @@ -0,0 +1,28 @@ +import generateUtilityClasses from '@mui/utils/generateUtilityClasses'; +import generateUtilityClass from '@mui/utils/generateUtilityClass'; + +export interface GaugeClasses { + /** Styles applied to the root element. */ + root: string; + /** Styles applied to the arc diplaying the value. */ + valueArc: string; + /** Styles applied to the arc diplaying the range of available values. */ + referenceArc: string; + /** Styles applied to the value text. */ + valueText: string; +} + +export type GaugeClassKey = keyof GaugeClasses; + +export function getGaugeUtilityClass(slot: string): string { + return generateUtilityClass('MuiGauge', slot); +} + +export const gaugeClasses: GaugeClasses = generateUtilityClasses('MuiGauge', [ + 'root', + 'valueArc', + 'referenceArc', + 'valueText', +]); + +export default gaugeClasses; diff --git a/packages/x-charts/src/Gauge/index.ts b/packages/x-charts/src/Gauge/index.ts new file mode 100644 index 000000000000..8e742bb16073 --- /dev/null +++ b/packages/x-charts/src/Gauge/index.ts @@ -0,0 +1,7 @@ +export * from './Gauge'; +export * from './GaugeContainer'; +export * from './GaugeValueText'; +export * from './GaugeValueArc'; +export * from './GaugeReferenceArc'; +export * from './gaugeClasses'; +export { useGaugeState } from './GaugeProvider'; diff --git a/packages/x-charts/src/Gauge/utils.ts b/packages/x-charts/src/Gauge/utils.ts new file mode 100644 index 000000000000..f8730c788e6c --- /dev/null +++ b/packages/x-charts/src/Gauge/utils.ts @@ -0,0 +1,88 @@ +function deg2rad(angle: number) { + return (Math.PI * angle) / 180; +} +function getPoint(angle: number): [number, number] { + const radAngle = deg2rad(angle); + return [Math.sin(radAngle), -Math.cos(radAngle)]; +} + +/** + * Retruns the ratio of the arc bounding box and its center. + * @param startAngle The start angle (in deg) + * @param endAngle The end angle (in deg) + */ +export function getArcRatios(startAngle: number, endAngle: number) { + // Set the start, end and center point. + const points = [[0, 0], getPoint(startAngle), getPoint(endAngle)]; + + // Add cardinal points included in the arc + const minAngle = Math.min(startAngle, endAngle); + const maxAngle = Math.max(startAngle, endAngle); + + const initialAngle = Math.floor(minAngle / 90) * 90; + + for (let step = 1; step <= 4; step += 1) { + const cartinalAngle = initialAngle + step * 90; + if (cartinalAngle < maxAngle) { + points.push(getPoint(cartinalAngle)); + } + } + + const minX = Math.min(...points.map(([x]) => x)); + const maxX = Math.max(...points.map(([x]) => x)); + const minY = Math.min(...points.map(([, y]) => y)); + const maxY = Math.max(...points.map(([, y]) => y)); + + return { + cx: -minX / (maxX - minX), + cy: -minY / (maxY - minY), + minX, + maxX, + minY, + maxY, + }; +} + +export function getAvailableRadius( + cx: number, + cy: number, + width: number, + height: number, + { + minX, + maxX, + minY, + maxY, + }: { + minX: number; + maxX: number; + minY: number; + maxY: number; + }, +) { + return Math.min( + ...[ + { + ratio: Math.abs(minX), + space: cx, + }, + { + ratio: Math.abs(maxX), + space: width - cx, + }, + { + ratio: Math.abs(minY), + space: cy, + }, + { + ratio: Math.abs(maxY), + space: height - cy, + }, + ].map(({ ratio, space }) => { + if (ratio < 0.00001) { + return Infinity; + } + return space / ratio; + }), + ); +} diff --git a/packages/x-charts/src/ResponsiveChartContainer/ResponsiveChartContainer.tsx b/packages/x-charts/src/ResponsiveChartContainer/ResponsiveChartContainer.tsx index a3928044c04e..3059e7f0ac8b 100644 --- a/packages/x-charts/src/ResponsiveChartContainer/ResponsiveChartContainer.tsx +++ b/packages/x-charts/src/ResponsiveChartContainer/ResponsiveChartContainer.tsx @@ -1,94 +1,8 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; -import ownerWindow from '@mui/utils/ownerWindow'; import { styled } from '@mui/material/styles'; import { ChartContainer, ChartContainerProps } from '../ChartContainer'; - -const useChartDimensions = ( - inWidth?: number, - inHeight?: number, -): [React.RefObject, number, number] => { - const rootRef = React.useRef(null); - const displayError = React.useRef(false); - - const [width, setWidth] = React.useState(0); - const [height, setHeight] = React.useState(0); - - // Adaptation of the `computeSizeAndPublishResizeEvent` from the grid. - const computeSize = React.useCallback(() => { - const mainEl = rootRef?.current; - - if (!mainEl) { - return; - } - - const win = ownerWindow(mainEl); - const computedStyle = win.getComputedStyle(mainEl); - - const newHeight = Math.floor(parseFloat(computedStyle.height)) || 0; - const newWidth = Math.floor(parseFloat(computedStyle.width)) || 0; - - setWidth(newWidth); - setHeight(newHeight); - }, []); - - React.useEffect(() => { - // Ensure the error detection occurs after the first rendering. - displayError.current = true; - }, []); - - useEnhancedEffect(() => { - if (inWidth !== undefined && inHeight !== undefined) { - return () => {}; - } - computeSize(); - - const elementToObserve = rootRef.current; - if (typeof ResizeObserver === 'undefined') { - return () => {}; - } - - let animationFrame: number; - const observer = new ResizeObserver(() => { - // See https://github.com/mui/mui-x/issues/8733 - animationFrame = requestAnimationFrame(() => { - computeSize(); - }); - }); - - if (elementToObserve) { - observer.observe(elementToObserve); - } - - return () => { - if (animationFrame) { - window.cancelAnimationFrame(animationFrame); - } - - if (elementToObserve) { - observer.unobserve(elementToObserve); - } - }; - }, [computeSize, inHeight, inWidth]); - - if (process.env.NODE_ENV !== 'production') { - if (displayError.current && inWidth === undefined && width === 0) { - console.error( - `MUI X Charts: ChartContainer does not have \`width\` prop, and its container has no \`width\` defined.`, - ); - displayError.current = false; - } - if (displayError.current && inHeight === undefined && height === 0) { - console.error( - `MUI X Charts: ChartContainer does not have \`height\` prop, and its container has no \`height\` defined.`, - ); - displayError.current = false; - } - } - - return [rootRef, inWidth ?? width, inHeight ?? height]; -}; +import { useChartContainerDimensions } from './useChartContainerDimensions'; export interface ResponsiveChartContainerProps extends Omit { @@ -128,7 +42,7 @@ const ResponsiveChartContainer = React.forwardRef(function ResponsiveChartContai ref, ) { const { width: inWidth, height: inHeight, ...other } = props; - const [containerRef, width, height] = useChartDimensions(inWidth, inHeight); + const [containerRef, width, height] = useChartContainerDimensions(inWidth, inHeight); return ( diff --git a/packages/x-charts/src/ResponsiveChartContainer/useChartContainerDimensions.ts b/packages/x-charts/src/ResponsiveChartContainer/useChartContainerDimensions.ts new file mode 100644 index 000000000000..4e82a43889af --- /dev/null +++ b/packages/x-charts/src/ResponsiveChartContainer/useChartContainerDimensions.ts @@ -0,0 +1,88 @@ +import * as React from 'react'; +import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; +import ownerWindow from '@mui/utils/ownerWindow'; + +export const useChartContainerDimensions = ( + inWidth?: number, + inHeight?: number, +): [React.RefObject, number, number] => { + const rootRef = React.useRef(null); + const displayError = React.useRef(false); + + const [width, setWidth] = React.useState(0); + const [height, setHeight] = React.useState(0); + + // Adaptation of the `computeSizeAndPublishResizeEvent` from the grid. + const computeSize = React.useCallback(() => { + const mainEl = rootRef?.current; + + if (!mainEl) { + return; + } + + const win = ownerWindow(mainEl); + const computedStyle = win.getComputedStyle(mainEl); + + const newHeight = Math.floor(parseFloat(computedStyle.height)) || 0; + const newWidth = Math.floor(parseFloat(computedStyle.width)) || 0; + + setWidth(newWidth); + setHeight(newHeight); + }, []); + + React.useEffect(() => { + // Ensure the error detection occurs after the first rendering. + displayError.current = true; + }, []); + + useEnhancedEffect(() => { + if (inWidth !== undefined && inHeight !== undefined) { + return () => {}; + } + computeSize(); + + const elementToObserve = rootRef.current; + if (typeof ResizeObserver === 'undefined') { + return () => {}; + } + + let animationFrame: number; + const observer = new ResizeObserver(() => { + // See https://github.com/mui/mui-x/issues/8733 + animationFrame = requestAnimationFrame(() => { + computeSize(); + }); + }); + + if (elementToObserve) { + observer.observe(elementToObserve); + } + + return () => { + if (animationFrame) { + window.cancelAnimationFrame(animationFrame); + } + + if (elementToObserve) { + observer.unobserve(elementToObserve); + } + }; + }, [computeSize, inHeight, inWidth]); + + if (process.env.NODE_ENV !== 'production') { + if (displayError.current && inWidth === undefined && width === 0) { + console.error( + `MUI X Charts: ChartContainer does not have \`width\` prop, and its container has no \`width\` defined.`, + ); + displayError.current = false; + } + if (displayError.current && inHeight === undefined && height === 0) { + console.error( + `MUI X Charts: ChartContainer does not have \`height\` prop, and its container has no \`height\` defined.`, + ); + displayError.current = false; + } + } + + return [rootRef, inWidth ?? width, inHeight ?? height]; +}; diff --git a/packages/x-charts/src/index.ts b/packages/x-charts/src/index.ts index c4277d4de3a0..cda446bd3f88 100644 --- a/packages/x-charts/src/index.ts +++ b/packages/x-charts/src/index.ts @@ -20,6 +20,7 @@ export * from './LineChart'; export * from './PieChart'; export * from './ScatterChart'; export * from './SparkLineChart'; +export * from './Gauge'; export * from './ChartContainer'; export * from './ChartsSurface'; export * from './ResponsiveChartContainer'; diff --git a/scripts/buildApiDocs/chartsSettings/index.ts b/scripts/buildApiDocs/chartsSettings/index.ts index 768c036cf3c7..bf83efd12a60 100644 --- a/scripts/buildApiDocs/chartsSettings/index.ts +++ b/scripts/buildApiDocs/chartsSettings/index.ts @@ -58,7 +58,14 @@ export default apiPages; getComponentInfo, translationLanguages: LANGUAGES, skipComponent(filename) { - return filename.includes('/context/'); + if (filename.includes('/context/')) { + return true; + } + return [ + 'x-charts/src/Gauge/GaugeReferenceArc.tsx', + 'x-charts/src/Gauge/GaugeValueArc.tsx', + 'x-charts/src/Gauge/GaugeValueText.tsx', + ].some((invalidPath) => filename.endsWith(invalidPath)); }, skipAnnotatingComponentDefinition: true, translationPagesDirectory: 'docs/translations/api-docs/charts', diff --git a/scripts/x-charts.exports.json b/scripts/x-charts.exports.json index 17aaaa826636..92715f8d2fcd 100644 --- a/scripts/x-charts.exports.json +++ b/scripts/x-charts.exports.json @@ -126,12 +126,25 @@ { "name": "Direction", "kind": "TypeAlias" }, { "name": "DrawingProvider", "kind": "Function" }, { "name": "FadeOptions", "kind": "TypeAlias" }, + { "name": "Gauge", "kind": "Function" }, + { "name": "gaugeClasses", "kind": "Variable" }, + { "name": "GaugeClasses", "kind": "Interface" }, + { "name": "GaugeClassKey", "kind": "TypeAlias" }, + { "name": "GaugeContainer", "kind": "Variable" }, + { "name": "GaugeContainerProps", "kind": "Interface" }, + { "name": "GaugeFormatterParams", "kind": "Interface" }, + { "name": "GaugeProps", "kind": "Interface" }, + { "name": "GaugeReferenceArc", "kind": "Function" }, + { "name": "GaugeValueArc", "kind": "Function" }, + { "name": "GaugeValueText", "kind": "Function" }, + { "name": "GaugeValueTextProps", "kind": "Interface" }, { "name": "getAreaElementUtilityClass", "kind": "Function" }, { "name": "getAxisHighlightUtilityClass", "kind": "Function" }, { "name": "getAxisUtilityClass", "kind": "Function" }, { "name": "getBarElementUtilityClass", "kind": "Function" }, { "name": "getChartsGridUtilityClass", "kind": "Function" }, { "name": "getChartsTooltipUtilityClass", "kind": "Function" }, + { "name": "getGaugeUtilityClass", "kind": "Function" }, { "name": "getHighlightElementUtilityClass", "kind": "Function" }, { "name": "getLegendUtilityClass", "kind": "Function" }, { "name": "getLineElementUtilityClass", "kind": "Function" }, @@ -243,6 +256,7 @@ { "name": "StackOffsetType", "kind": "TypeAlias" }, { "name": "StackOrderType", "kind": "TypeAlias" }, { "name": "useDrawingArea", "kind": "Function" }, + { "name": "useGaugeState", "kind": "Function" }, { "name": "useXScale", "kind": "Function" }, { "name": "useYScale", "kind": "Function" } ]