Skip to content

Commit

Permalink
[charts] Add Gauge component
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfauquette committed Feb 8, 2024
1 parent f1120ce commit b631eb9
Show file tree
Hide file tree
Showing 13 changed files with 640 additions and 90 deletions.
6 changes: 4 additions & 2 deletions packages/x-charts/src/ChartsSurface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ const ChartsSurface = React.forwardRef<SVGSVGElement, ChartsSurfaceProps>(functi
viewBox,
disableAxisListener = false,
className,
title,
desc,
...other
} = props;
const svgView = { width, height, x: 0, y: 0, ...viewBox };
Expand All @@ -62,8 +64,8 @@ const ChartsSurface = React.forwardRef<SVGSVGElement, ChartsSurfaceProps>(functi
ref={ref}
{...other}
>
<title>{props.title}</title>
<desc>{props.desc}</desc>
<title>{title}</title>
<desc>{desc}</desc>
{children}
</ChartChartsSurfaceStyles>
);
Expand Down
35 changes: 35 additions & 0 deletions packages/x-charts/src/Gauge/Gauge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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';

export interface GaugeProps extends GaugeContainerProps {
classes?: Partial<GaugeClasses>;
}

const useUtilityClasses = (props: GaugeProps) => {
const { classes } = props;

const slots = {
root: ['root'],
valueArc: ['valueArc'],
referenceArc: ['referenceArc'],
};

return composeClasses(slots, getGaugeUtilityClass, classes);
};

function Gauge(props: GaugeProps) {
const classes = useUtilityClasses(props);
return (
<GaugeContainer width={200} height={200} {...props} className={classes.root}>
<GaugeReferenceArc className={classes.referenceArc} />
<GaugeValueArc className={classes.valueArc} />
</GaugeContainer>
);
}

export { Gauge };
118 changes: 118 additions & 0 deletions packages/x-charts/src/Gauge/GaugeContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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<ChartsSurfaceProps, 'width' | 'height' | 'children'>,
Omit<DrawingProviderProps, 'svgRef' | 'width' | 'height' | 'children'>,
Omit<GaugeProviderProps, 'children'> {
/**
* 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<GaugeContainerProps, 'width' | 'height'> }>(({ ownerState }) => ({
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%',
},
}));

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<SVGSVGElement>(null);
const handleRef = useForkRef(ref, svgRef);

return (
<ResizableContainer
ref={containerRef}
ownerState={{ width: inWidth, height: inHeight }}
role="meter"
aria-valuenow={value === null ? undefined : value}
aria-valuemin={valueMin}
aria-valuemax={valueMax}
{...other}
>
{width && height ? (
<DrawingProvider
width={width}
height={height}
margin={{ left: 10, right: 10, top: 10, bottom: 10, ...margin }}
svgRef={svgRef}
>
<GaugeProvider
value={value}
valueMin={valueMin}
valueMax={valueMax}
startAngle={startAngle}
endAngle={endAngle}
outerRadius={outerRadius}
innerRadius={innerRadius}
cornerRadius={cornerRadius}
cx={cx}
cy={cy}
>
<ChartsSurface
width={width}
height={height}
ref={handleRef}
title={title}
desc={desc}
disableAxisListener
aria-hidden="true"
>
{children}
</ChartsSurface>
</GaugeProvider>
</DrawingProvider>
) : null}
</ResizableContainer>
);
});

export { GaugeContainer };
190 changes: 190 additions & 0 deletions packages/x-charts/src/Gauge/GaugeProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// @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).
* @default '50%'
*/
cornerRadius?: number;
/**
* The x coordinate of the pie 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 pie 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` if no value to display.
*/
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<GaugeConfig> & ProcessedCircularConfig>({
value: null,
valueMin: 0,
valueMax: 0,
startAngle: 0,
endAngle: 0,
innerRadius: 0,
outerRadius: 0,
cornerRadius: 0,
cx: 0,
cy: 0,
});

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 availableRadius = 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 = availableRadius * (ratios.maxX - ratios.minX);
cx = left + (width - usedWidth) / 2 + ratios.cx * usedWidth;
}
if (cyParam === undefined) {
const usedHeight = availableRadius * (ratios.maxY - ratios.minY);
cy = top + (height - usedHeight) / 2 + ratios.cy * usedHeight;
}

const outerRadius = getPercentageValue(outerRadiusParam ?? availableRadius, availableRadius);
const innerRadius = getPercentageValue(innerRadiusParam ?? '80%', availableRadius);
const cornerRadius = getPercentageValue(cornerRadiusParam ?? '50%', outerRadius - innerRadius);

const contextValue = React.useMemo(
() => ({
value,
valueMin,
valueMax,
startAngle: (Math.PI * startAngle) / 180,
endAngle: (Math.PI * endAngle) / 180,
outerRadius,
innerRadius,
cornerRadius,
cx,
cy,
}),
[
value,
valueMin,
valueMax,
startAngle,
endAngle,
outerRadius,
innerRadius,
cornerRadius,
cx,
cy,
],
);

return <GaugeContext.Provider value={contextValue}>{children}</GaugeContext.Provider>;
}

export function useGaugeState() {
return React.useContext(GaugeContext);
}
31 changes: 31 additions & 0 deletions packages/x-charts/src/Gauge/GaugeReferenceArc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as React from 'react';
import { arc as d3Arc } from 'd3-shape';
import styled from '@mui/system/styled';
import { useGaugeState } from './GaugeProvider';

const StyledPath = styled('path', {
name: 'MuiGauge',
slot: 'ReferenceArc',
overridesResolver: (props, styles) => styles.referenceArc,
})(({ theme }) => ({
fill: theme.palette.divider,
}));

export function GaugeReferenceArc(props: React.ComponentProps<'path'>) {
const { startAngle, endAngle, outerRadius, innerRadius, cornerRadius, cx, cy } = useGaugeState();

return (
<StyledPath
transform={`translate(${cx}, ${cy})`}
d={
d3Arc().cornerRadius(cornerRadius)({
startAngle,
endAngle,
innerRadius,
outerRadius,
})!
}
{...props}
/>
);
}
Loading

0 comments on commit b631eb9

Please sign in to comment.