diff --git a/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx b/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx index 4d8978a572..d40b0189cd 100644 --- a/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx +++ b/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx @@ -16,6 +16,7 @@ import { Widget } from './Widget/Widget'; import { FlagsProjectChart } from './FlagsProjectChart/FlagsProjectChart'; import { ProjectHealthChart } from './ProjectHealthChart/ProjectHealthChart'; import { TimeToProductionChart } from './TimeToProductionChart/TimeToProductionChart'; +import { TimeToProduction } from './TimeToProduction/TimeToProduction'; const StyledGrid = styled(Box)(({ theme }) => ({ display: 'grid', @@ -141,11 +142,11 @@ export const ExecutiveDashboard: VFC = () => { } /> - + + {/* FIXME: data from API */} + + + { + return { + x: centerX + radius * Math.cos(angleInRadians), + y: centerY + radius * Math.sin(angleInRadians), + }; +}; + +const degreesToRadians = (degrees: number) => degrees * (Math.PI / 180); + +const normalizeValue = (value: number, min: number, max: number) => + (value - min) / (max - min); + +const describeArc = (radius: number, startAngle: number, endAngle: number) => { + const start = polarToCartesian(0, 0, radius, endAngle); + const end = polarToCartesian(0, 0, radius, startAngle); + const largeArcFlag = endAngle - startAngle <= Math.PI ? '0' : '1'; + const d = [ + 'M', + start.x, + start.y, + 'A', + radius, + radius, + 0, + largeArcFlag, + 0, + end.x, + end.y, + ].join(' '); + return d; +}; + +const GaugeLines = () => { + const theme = useTheme(); + const startRadius = 0.875; + const endRadius = 1.14; + const lineWidth = 0.1; + const lineBorder = 0.05; + + const angles = [180 + 50, 180 + 90 + 40]; + + return ( + <> + {angles.map(degreesToRadians).map((angle) => { + const start = polarToCartesian(0, 0, startRadius, angle); + const end = polarToCartesian(0, 0, endRadius, angle); + + return ( + <> + + + ) + + ); + })} + + ); +}; + +const GaugeText = () => { + const fontSize = 0.175; + + return ( + <> + + Slow + + + Medium + + + Fast + + + ); +}; + +type GaugeProps = { + value?: number; + min?: number; + max?: number; +}; + +export const Gauge: VFC = ({ value, min = 0, max = 100 }) => { + const theme = useTheme(); + const radius = 1; + const lineWidth = 0.25; + const startAngle = -Math.PI; + const endAngle = 0; + + // Calculate the filled arc proportion based on the value + const valueAngle = + startAngle + + (endAngle - startAngle) * normalizeValue(value || 0, min, max); + + const backgroundArcPath = describeArc(radius, startAngle, endAngle); + const filledArcPath = describeArc(radius, startAngle, valueAngle); + + return ( + + + Gauge chart for time to production + + + + + + + + + } + /> + + + + + ); +}; diff --git a/frontend/src/component/executiveDashboard/LineChart/ChartTooltip/ChartTooltip.tsx b/frontend/src/component/executiveDashboard/LineChart/ChartTooltip/ChartTooltip.tsx index 40f3d5c3d0..4a48c5ac26 100644 --- a/frontend/src/component/executiveDashboard/LineChart/ChartTooltip/ChartTooltip.tsx +++ b/frontend/src/component/executiveDashboard/LineChart/ChartTooltip/ChartTooltip.tsx @@ -52,6 +52,7 @@ export const ChartTooltip: VFC = ({ tooltip }) => ( width: 220, padding: theme.spacing(1.5, 2), pointerEvents: 'none', + zIndex: theme.zIndex.tooltip, })} > { diff --git a/frontend/src/component/executiveDashboard/TimeToProduction/TimeToProduction.tsx b/frontend/src/component/executiveDashboard/TimeToProduction/TimeToProduction.tsx new file mode 100644 index 0000000000..d8a5a55b35 --- /dev/null +++ b/frontend/src/component/executiveDashboard/TimeToProduction/TimeToProduction.tsx @@ -0,0 +1,106 @@ +import { VFC } from 'react'; +import { Typography, styled } from '@mui/material'; +import { Gauge } from '../Gauge/Gauge'; + +const StyledContainer = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + gap: theme.spacing(1), + textAlign: 'center', +})); + +const StyledText = styled('div')(({ theme }) => ({ + marginTop: theme.spacing(-7), + display: 'flex', + flexDirection: 'column', +})); + +type TimeToProductionProps = { + daysToProduction: number | undefined; +}; + +const interpolate = ( + value: number, + [fromStart, fromEnd]: [number, number], + [toStart, toEnd]: [number, number], +): number => { + if (value < fromStart) { + return toStart; + } + + if (value > fromEnd) { + return toEnd; + } + + return ( + ((value - fromStart) / (fromEnd - fromStart)) * (toEnd - toStart) + + toStart + ); +}; + +const resolveValue = ( + daysToProduction: number | undefined, +): { + value: string | undefined; + gauge: number | undefined; + score: 'Fast' | 'Medium' | 'Slow' | undefined; +} => { + if (daysToProduction === undefined) { + return { + value: undefined, + gauge: undefined, + score: undefined, + }; + } + + if (daysToProduction <= 7) { + return { + value: `${daysToProduction.toFixed(1)} days`, + gauge: interpolate(daysToProduction, [1, 7], [100, 75]), + score: 'Fast', + }; + } + + if (daysToProduction <= 31) { + return { + value: `${(daysToProduction / 7).toFixed(1)} weeks`, + gauge: interpolate(daysToProduction, [7, 31], [67.5, 30]), + score: 'Medium', + }; + } + + return { + value: `${(daysToProduction / 30).toFixed(1)} months`, + gauge: interpolate(daysToProduction, [31, 365 / 4], [23, 0]), + score: 'Slow', + }; +}; + +export const TimeToProduction: VFC = ({ + daysToProduction, +}) => { + const { value, gauge, score } = resolveValue(daysToProduction); + + return ( + + + + + {daysToProduction !== undefined ? value : 'N/A'} + + ({ + color: score + ? theme.palette.primary.main + : theme.palette.text.secondary, + })} + > + {score || 'No data'} + + + + ); +}; diff --git a/frontend/src/component/executiveDashboard/useProjectChartData.ts b/frontend/src/component/executiveDashboard/useProjectChartData.ts index 15633c58b4..2f9788f962 100644 --- a/frontend/src/component/executiveDashboard/useProjectChartData.ts +++ b/frontend/src/component/executiveDashboard/useProjectChartData.ts @@ -35,7 +35,7 @@ export const useProjectChartData = ( }), borderColor: color, backgroundColor: color, - fill: true, + fill: false, }; }, ); diff --git a/frontend/src/themes/dark-theme.ts b/frontend/src/themes/dark-theme.ts index fd02437cee..920f1fd0b1 100644 --- a/frontend/src/themes/dark-theme.ts +++ b/frontend/src/themes/dark-theme.ts @@ -278,6 +278,19 @@ const theme = { // A700: '#A6000E', }, variants: colors.variants, + + /** + * Dashboard and charts + */ + charts: { + gauge: { + gradientStart: '#4C4992', + gradientEnd: '#9792ED', + background: '#39384C', + sectionLine: '#8c89bf', + text: colors.grey[800], + }, + }, }, }; diff --git a/frontend/src/themes/theme.ts b/frontend/src/themes/theme.ts index c4a35df0c2..75395e6413 100644 --- a/frontend/src/themes/theme.ts +++ b/frontend/src/themes/theme.ts @@ -263,6 +263,19 @@ export const theme = { // A700: '#A6000E', }, variants: colors.variants, + + /** + * Dashboard and charts + */ + charts: { + gauge: { + gradientStart: colors.purple[100], + gradientEnd: colors.purple[700], + background: colors.purple[50], + sectionLine: colors.purple[500], + text: colors.grey[600], + }, + }, }, }; diff --git a/frontend/src/themes/themeTypes.ts b/frontend/src/themes/themeTypes.ts index 3e221a458f..aa66d9f6c2 100644 --- a/frontend/src/themes/themeTypes.ts +++ b/frontend/src/themes/themeTypes.ts @@ -121,6 +121,19 @@ declare module '@mui/material/styles' { * Variants, percentage split in strategies **/ variants: string[]; + + /** + * Dashboard and charts + */ + charts: { + gauge: { + gradientStart: string; + gradientEnd: string; + background: string; + sectionLine: string; + text: string; + }; + }; } interface Theme extends CustomTheme {} interface ThemeOptions extends CustomTheme {}