mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-01 13:47:27 +02:00
Improve impact metrics layout (#10326)
- narrow screen no longer breaks - fixed size of series indicators in tooltips - simplified grid layout props - updated X axis ticks
This commit is contained in:
parent
2d83f297a1
commit
1eefede62e
@ -44,7 +44,6 @@ const StyledWidget = styled(Paper)(({ theme }) => ({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
overflow: 'hidden',
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledChartContent = styled(Box)({
|
const StyledChartContent = styled(Box)({
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import type { FC, ReactNode } from 'react';
|
import type { FC, ReactNode } from 'react';
|
||||||
import { useMemo, useCallback } from 'react';
|
import { useMemo, useCallback } from 'react';
|
||||||
import { Responsive, WidthProvider } from 'react-grid-layout';
|
import GridLayout, { WidthProvider } from 'react-grid-layout';
|
||||||
import { styled } from '@mui/material';
|
import { styled, useTheme, useMediaQuery } from '@mui/material';
|
||||||
import 'react-grid-layout/css/styles.css';
|
import 'react-grid-layout/css/styles.css';
|
||||||
import 'react-resizable/css/styles.css';
|
import 'react-resizable/css/styles.css';
|
||||||
|
|
||||||
const ResponsiveGridLayout = WidthProvider(Responsive);
|
const ResponsiveGridLayout = WidthProvider(GridLayout);
|
||||||
|
|
||||||
const StyledGridContainer = styled('div')(({ theme }) => ({
|
const StyledGridContainer = styled('div')(({ theme }) => ({
|
||||||
'& .react-grid-item': {
|
'& .react-grid-item': {
|
||||||
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
||||||
},
|
},
|
||||||
'& .react-resizable-handle': {
|
'& .grid-item-drag-handle': {
|
||||||
'&::after': {
|
[theme.breakpoints.down('md')]: {
|
||||||
opacity: 0.5,
|
display: 'none',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@ -32,65 +32,74 @@ export type GridItem = {
|
|||||||
static?: boolean;
|
static?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type LayoutItem = {
|
||||||
|
i: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
minW?: number;
|
||||||
|
minH?: number;
|
||||||
|
maxW?: number;
|
||||||
|
maxH?: number;
|
||||||
|
static?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type GridLayoutWrapperProps = {
|
type GridLayoutWrapperProps = {
|
||||||
items: GridItem[];
|
items: GridItem[];
|
||||||
onLayoutChange?: (layout: unknown[]) => void;
|
onLayoutChange?: (layout: LayoutItem[]) => void;
|
||||||
cols?: { lg: number; md: number; sm: number; xs: number; xxs: number };
|
cols?: { lg: number; md: number; sm: number; xs: number; xxs: number };
|
||||||
rowHeight?: number;
|
rowHeight?: number;
|
||||||
margin?: [number, number];
|
|
||||||
isDraggable?: boolean;
|
|
||||||
isResizable?: boolean;
|
|
||||||
compactType?: 'vertical' | 'horizontal' | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GridLayoutWrapper: FC<GridLayoutWrapperProps> = ({
|
export const GridLayoutWrapper: FC<GridLayoutWrapperProps> = ({
|
||||||
items,
|
items,
|
||||||
onLayoutChange,
|
onLayoutChange,
|
||||||
cols = { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 },
|
cols = { lg: 12, md: 12, sm: 6, xs: 4, xxs: 2 },
|
||||||
rowHeight = 180,
|
rowHeight = 180,
|
||||||
margin = [16, 16],
|
|
||||||
isDraggable = true,
|
|
||||||
isResizable = true,
|
|
||||||
compactType = 'vertical',
|
|
||||||
}) => {
|
}) => {
|
||||||
const layouts = useMemo(() => {
|
const theme = useTheme();
|
||||||
const baseLayout = items.map((item, index) => ({
|
const isMobileBreakpoint = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
|
const layout = useMemo(() => {
|
||||||
|
if (isMobileBreakpoint) {
|
||||||
|
let currentY = 0;
|
||||||
|
return items.map((item) => {
|
||||||
|
const layoutItem = {
|
||||||
|
i: item.id,
|
||||||
|
x: 0,
|
||||||
|
y: currentY,
|
||||||
|
w: cols.xs,
|
||||||
|
h: item.h ?? 4,
|
||||||
|
minW: cols.xs,
|
||||||
|
minH: item.minH ?? 3,
|
||||||
|
maxW: cols.xs,
|
||||||
|
maxH: item.maxH ?? 8,
|
||||||
|
static: false,
|
||||||
|
};
|
||||||
|
currentY += layoutItem.h;
|
||||||
|
return layoutItem;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.map((item, index) => ({
|
||||||
i: item.id,
|
i: item.id,
|
||||||
x: item.x ?? (index % cols.lg) * (item.w ?? 6),
|
x:
|
||||||
y: item.y ?? Math.floor(index / cols.lg) * (item.h ?? 4),
|
item.x ??
|
||||||
w: item.w ?? 6,
|
(index % Math.floor(cols.lg / (item.w ?? 4))) * (item.w ?? 4),
|
||||||
|
y:
|
||||||
|
item.y ??
|
||||||
|
Math.floor(index / Math.floor(cols.lg / (item.w ?? 4))) *
|
||||||
|
(item.h ?? 4),
|
||||||
|
w: item.w ?? 4,
|
||||||
h: item.h ?? 4,
|
h: item.h ?? 4,
|
||||||
minW: item.minW ?? 3,
|
minW: item.minW ?? 4,
|
||||||
minH: item.minH ?? 3,
|
minH: item.minH ?? 3,
|
||||||
maxW: item.maxW ?? 12,
|
maxW: item.maxW ?? 12,
|
||||||
maxH: item.maxH ?? 8,
|
maxH: item.maxH ?? 8,
|
||||||
static: item.static ?? false,
|
static: item.static ?? false,
|
||||||
}));
|
}));
|
||||||
|
}, [items, cols, isMobileBreakpoint]);
|
||||||
return {
|
|
||||||
lg: baseLayout,
|
|
||||||
md: baseLayout.map((item) => ({
|
|
||||||
...item,
|
|
||||||
w: Math.min(item.w, cols.md),
|
|
||||||
x: Math.min(item.x, cols.md - item.w),
|
|
||||||
})),
|
|
||||||
sm: baseLayout.map((item) => ({
|
|
||||||
...item,
|
|
||||||
w: Math.min(item.w, cols.sm),
|
|
||||||
x: Math.min(item.x, cols.sm - item.w),
|
|
||||||
})),
|
|
||||||
xs: baseLayout.map((item) => ({
|
|
||||||
...item,
|
|
||||||
w: Math.min(item.w, cols.xs),
|
|
||||||
x: Math.min(item.x, cols.xs - item.w),
|
|
||||||
})),
|
|
||||||
xxs: baseLayout.map((item) => ({
|
|
||||||
...item,
|
|
||||||
w: Math.min(item.w, cols.xxs),
|
|
||||||
x: Math.min(item.x, cols.xxs - item.w),
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}, [items, cols]);
|
|
||||||
|
|
||||||
const children = useMemo(
|
const children = useMemo(
|
||||||
() => items.map((item) => <div key={item.id}>{item.component}</div>),
|
() => items.map((item) => <div key={item.id}>{item.component}</div>),
|
||||||
@ -98,31 +107,36 @@ export const GridLayoutWrapper: FC<GridLayoutWrapperProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleLayoutChange = useCallback(
|
const handleLayoutChange = useCallback(
|
||||||
(layout: unknown[], layouts: unknown) => {
|
(layout: LayoutItem[]) => {
|
||||||
onLayoutChange?.(layout);
|
if (!isMobileBreakpoint) {
|
||||||
|
onLayoutChange?.(layout);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[onLayoutChange],
|
[onLayoutChange, isMobileBreakpoint],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledGridContainer>
|
<StyledGridContainer>
|
||||||
<ResponsiveGridLayout
|
<ResponsiveGridLayout
|
||||||
className='impact-metrics-grid'
|
className='impact-metrics-grid'
|
||||||
layouts={layouts}
|
layout={layout}
|
||||||
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
cols={isMobileBreakpoint ? cols.xs : cols.lg}
|
||||||
cols={cols}
|
|
||||||
rowHeight={rowHeight}
|
rowHeight={rowHeight}
|
||||||
margin={margin}
|
margin={[
|
||||||
|
Number.parseInt(theme.spacing(2)),
|
||||||
|
Number.parseInt(theme.spacing(2)),
|
||||||
|
]}
|
||||||
containerPadding={[0, 0]}
|
containerPadding={[0, 0]}
|
||||||
isDraggable={isDraggable}
|
isDraggable={!isMobileBreakpoint}
|
||||||
isResizable={isResizable}
|
isResizable={!isMobileBreakpoint}
|
||||||
onLayoutChange={handleLayoutChange}
|
onLayoutChange={handleLayoutChange}
|
||||||
resizeHandles={['se']}
|
resizeHandles={['se']}
|
||||||
draggableHandle='.grid-item-drag-handle'
|
draggableHandle='.grid-item-drag-handle'
|
||||||
compactType={compactType}
|
compactType={isMobileBreakpoint ? null : 'vertical'}
|
||||||
preventCollision={false}
|
preventCollision={false}
|
||||||
useCSSTransforms={true}
|
useCSSTransforms={true}
|
||||||
autoSize={true}
|
autoSize={true}
|
||||||
|
allowOverlap={false}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ResponsiveGridLayout>
|
</ResponsiveGridLayout>
|
||||||
|
@ -14,8 +14,8 @@ const StyledEmptyState = styled(Paper)(({ theme }) => ({
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
padding: theme.spacing(8),
|
padding: theme.spacing(8),
|
||||||
backgroundColor: theme.palette.background.default,
|
backgroundColor: theme.palette.background.default,
|
||||||
borderRadius: theme.shape.borderRadius * 2,
|
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
||||||
border: `2px dashed ${theme.palette.divider}`,
|
boxShadow: 'none',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const ImpactMetrics: FC = () => {
|
export const ImpactMetrics: FC = () => {
|
||||||
@ -144,9 +144,6 @@ export const ImpactMetrics: FC = () => {
|
|||||||
<GridLayoutWrapper
|
<GridLayoutWrapper
|
||||||
items={gridItems}
|
items={gridItems}
|
||||||
onLayoutChange={handleLayoutChange}
|
onLayoutChange={handleLayoutChange}
|
||||||
rowHeight={180}
|
|
||||||
margin={[16, 16]}
|
|
||||||
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
@ -104,6 +104,11 @@ export const ImpactMetricsChart: FC<ImpactMetricsChartProps> = ({
|
|||||||
},
|
},
|
||||||
tooltipFormat: 'PPpp',
|
tooltipFormat: 'PPpp',
|
||||||
},
|
},
|
||||||
|
ticks: {
|
||||||
|
maxRotation: 45,
|
||||||
|
minRotation: 45,
|
||||||
|
maxTicksLimit: 8,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
beginAtZero,
|
beginAtZero,
|
||||||
|
@ -43,15 +43,25 @@ export const useChartData = (
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
// Create a comprehensive timestamp range for consistent X-axis
|
||||||
const allTimestamps = new Set<number>();
|
const allTimestamps = new Set<number>();
|
||||||
timeSeriesData.forEach((series) => {
|
timeSeriesData.forEach((series) => {
|
||||||
series.data.forEach(([timestamp]) => {
|
series.data.forEach(([timestamp]) => {
|
||||||
allTimestamps.add(timestamp);
|
allTimestamps.add(timestamp);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (allTimestamps.size === 0) {
|
||||||
|
return {
|
||||||
|
labels: [],
|
||||||
|
datasets: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const sortedTimestamps = Array.from(allTimestamps).sort(
|
const sortedTimestamps = Array.from(allTimestamps).sort(
|
||||||
(a, b) => a - b,
|
(a, b) => a - b,
|
||||||
);
|
);
|
||||||
|
|
||||||
const labels = sortedTimestamps.map(
|
const labels = sortedTimestamps.map(
|
||||||
(timestamp) => new Date(timestamp * 1000),
|
(timestamp) => new Date(timestamp * 1000),
|
||||||
);
|
);
|
||||||
@ -72,7 +82,6 @@ export const useChartData = (
|
|||||||
borderColor: color,
|
borderColor: color,
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
fill: false,
|
fill: false,
|
||||||
spanGaps: false,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -7,22 +7,24 @@ export const getTimeUnit = (selectedRange: string) => {
|
|||||||
case 'week':
|
case 'week':
|
||||||
return 'day';
|
return 'day';
|
||||||
case 'month':
|
case 'month':
|
||||||
return 'day';
|
return 'week';
|
||||||
default:
|
default:
|
||||||
return 'hour';
|
return 'day';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDisplayFormat = (selectedRange: string) => {
|
export const getDisplayFormat = (selectedRange: string) => {
|
||||||
switch (selectedRange) {
|
switch (selectedRange) {
|
||||||
case 'hour':
|
case 'hour':
|
||||||
|
return 'HH:mm';
|
||||||
case 'day':
|
case 'day':
|
||||||
return 'HH:mm';
|
return 'HH:mm';
|
||||||
case 'week':
|
case 'week':
|
||||||
|
return 'MMM dd';
|
||||||
case 'month':
|
case 'month':
|
||||||
return 'MMM dd';
|
return 'MMM dd';
|
||||||
default:
|
default:
|
||||||
return 'MMM dd HH:mm';
|
return 'MMM dd';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Box, Paper, styled, Typography } from '@mui/material';
|
import { Box, Paper, styled, Typography } from '@mui/material';
|
||||||
import type { TooltipItem } from 'chart.js';
|
import type { TooltipItem } from 'chart.js';
|
||||||
|
import { Truncator } from 'component/common/Truncator/Truncator';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import type { FC, VFC } from 'react';
|
import type { FC, VFC } from 'react';
|
||||||
import { objectId } from 'utils/objectId';
|
import { objectId } from 'utils/objectId';
|
||||||
@ -32,12 +33,13 @@ const StyledItem = styled('li')(({ theme }) => ({
|
|||||||
marginBottom: theme.spacing(0.5),
|
marginBottom: theme.spacing(0.5),
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
fontSize: theme.typography.body2.fontSize,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledLabelIcon = styled('span')(({ theme }) => ({
|
const StyledLabelIcon = styled('span')(({ theme }) => ({
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
width: 8,
|
minWidth: 8,
|
||||||
height: 8,
|
minHeight: 8,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
marginRight: theme.spacing(1),
|
marginRight: theme.spacing(1),
|
||||||
}));
|
}));
|
||||||
@ -119,14 +121,7 @@ export const ChartTooltip: VFC<IChartTooltipProps> = ({ tooltip }) => (
|
|||||||
>
|
>
|
||||||
{' '}
|
{' '}
|
||||||
</StyledLabelIcon>
|
</StyledLabelIcon>
|
||||||
<Typography
|
<Truncator lines={2}>{item.title}</Truncator>
|
||||||
variant='body2'
|
|
||||||
sx={{
|
|
||||||
display: 'inline-block',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.title}
|
|
||||||
</Typography>
|
|
||||||
</StyledItem>
|
</StyledItem>
|
||||||
))}
|
))}
|
||||||
</StyledList>
|
</StyledList>
|
||||||
|
Loading…
Reference in New Issue
Block a user