mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
[Dashboards] Graph pie chart component + refactor (#14045)
closes https://github.com/twentyhq/core-team-issues/issues/1373 closes https://github.com/twentyhq/core-team-issues/issues/1368 figma https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=72808-199932&t=c7GEJKFZKI0JhRv0-0 TODO: - ~~refactor to have a color registry~~ - ~~refactor legend and tooltips into a separate component for reuse across the graphs~~ - also refactor the border in between to be a common logic between pie and gauge graphs - ***open question*** - How should we code this border in between (renderValueEndLine), which is a layer - it's a function that returns JSX I could not figure out if it should be a util/component/hook? None of them fit into the category -- hence kept them inline in both gauge and pie charts for simplicity
This commit is contained in:
parent
8cb4058d2d
commit
50448c9b16
19 changed files with 1256 additions and 334 deletions
|
|
@ -47,6 +47,7 @@
|
|||
"@lingui/react": "^5.1.2",
|
||||
"@nivo/core": "^0.99.0",
|
||||
"@nivo/line": "^0.99.0",
|
||||
"@nivo/pie": "^0.99.0",
|
||||
"@nivo/radial-bar": "^0.99.0",
|
||||
"@react-pdf/renderer": "^4.1.6",
|
||||
"@scalar/api-reference-react": "^0.4.36",
|
||||
|
|
|
|||
|
|
@ -1,26 +1,33 @@
|
|||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { ResponsiveRadialBar } from '@nivo/radial-bar';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
AppTooltip,
|
||||
H1Title,
|
||||
H1TitleFontColor,
|
||||
IconArrowUpRight,
|
||||
TooltipDelay,
|
||||
} from 'twenty-ui/display';
|
||||
type RadialBarCustomLayerProps,
|
||||
ResponsiveRadialBar,
|
||||
} from '@nivo/radial-bar';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { H1Title, H1TitleFontColor } from 'twenty-ui/display';
|
||||
|
||||
import { createGradientDef } from '../utils/createGradientDef';
|
||||
import { createGraphColorRegistry } from '../utils/createGraphColorRegistry';
|
||||
import { getColorSchemeByName } from '../utils/getColorSchemeByName';
|
||||
import {
|
||||
formatGraphValue,
|
||||
type GraphValueFormatOptions,
|
||||
} from '../utils/graphFormatters';
|
||||
import { GraphWidgetLegend } from './GraphWidgetLegend';
|
||||
import { GraphWidgetTooltip } from './GraphWidgetTooltip';
|
||||
|
||||
type GraphWidgetGaugeChartProps = {
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
unit: string;
|
||||
showValue?: boolean;
|
||||
showLegend?: boolean;
|
||||
legendLabel: string;
|
||||
tooltipHref: string;
|
||||
tooltipHref?: string;
|
||||
id: string;
|
||||
};
|
||||
} & GraphValueFormatOptions;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
|
|
@ -44,88 +51,37 @@ const StyledH1Title = styled(H1Title)`
|
|||
transform: translate(-50%, -150%);
|
||||
`;
|
||||
|
||||
const StyledLegendContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(3)};
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledLegendItem = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
`;
|
||||
|
||||
const StyledLegendLabel = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
`;
|
||||
|
||||
const StyledLegendValue = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
`;
|
||||
|
||||
const StyledTooltipContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledTooltipRow = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.extraLight};
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledDot = styled.div`
|
||||
background: ${({ theme }) => theme.color.blue};
|
||||
border-radius: 50%;
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const StyledTooltipValue = styled.span`
|
||||
margin-left: auto;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledTooltipLink = styled.a`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
}
|
||||
`;
|
||||
|
||||
export const GraphWidgetGaugeChart = ({
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
unit,
|
||||
showValue = true,
|
||||
showLegend = true,
|
||||
legendLabel,
|
||||
tooltipHref,
|
||||
id,
|
||||
displayType,
|
||||
decimals,
|
||||
prefix,
|
||||
suffix,
|
||||
customFormatter,
|
||||
}: GraphWidgetGaugeChartProps) => {
|
||||
const theme = useTheme();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const formatValue = (val: number): string => {
|
||||
if (val % 1 !== 0) {
|
||||
return val.toFixed(1);
|
||||
}
|
||||
return val.toString();
|
||||
const colorRegistry = createGraphColorRegistry(theme);
|
||||
const colorScheme =
|
||||
getColorSchemeByName(colorRegistry, 'blue') || colorRegistry.blue;
|
||||
|
||||
const formatOptions: GraphValueFormatOptions = {
|
||||
displayType,
|
||||
decimals,
|
||||
prefix,
|
||||
suffix,
|
||||
customFormatter,
|
||||
};
|
||||
|
||||
const displayValue = formatValue(value);
|
||||
const formattedValue = formatGraphValue(value, formatOptions);
|
||||
|
||||
const normalizedValue = max === min ? 0 : ((value - min) / (max - min)) * 100;
|
||||
const clampedNormalizedValue = Math.max(0, Math.min(100, normalizedValue));
|
||||
|
|
@ -140,99 +96,125 @@ export const GraphWidgetGaugeChart = ({
|
|||
},
|
||||
];
|
||||
|
||||
const gradientColors = isHovered
|
||||
? {
|
||||
start: theme.adaptiveColors.blue4,
|
||||
end: theme.adaptiveColors.blue3,
|
||||
}
|
||||
: {
|
||||
start: theme.adaptiveColors.blue2,
|
||||
end: theme.adaptiveColors.blue1,
|
||||
};
|
||||
|
||||
const gradientId = `gaugeGradient-${id}`;
|
||||
|
||||
const defs = [
|
||||
{
|
||||
id: gradientId,
|
||||
type: 'linearGradient',
|
||||
colors: [
|
||||
{ offset: 0, color: gradientColors.start },
|
||||
{ offset: 100, color: gradientColors.end },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const tooltipContent = (
|
||||
<StyledTooltipContent>
|
||||
<StyledTooltipRow>
|
||||
<StyledDot />
|
||||
<span>{legendLabel}</span>
|
||||
<StyledTooltipValue>{`${displayValue}${unit}`}</StyledTooltipValue>
|
||||
</StyledTooltipRow>
|
||||
<StyledTooltipLink href={tooltipHref}>
|
||||
<span>{t`Click to see data`}</span>
|
||||
<IconArrowUpRight size={theme.icon.size.sm} />
|
||||
</StyledTooltipLink>
|
||||
</StyledTooltipContent>
|
||||
const gaugeAngle = -90 + (clampedNormalizedValue / 100) * 90;
|
||||
const gradientDef = createGradientDef(
|
||||
colorScheme,
|
||||
gradientId,
|
||||
isHovered,
|
||||
gaugeAngle,
|
||||
);
|
||||
const defs = [gradientDef];
|
||||
|
||||
const handleClick = () => {
|
||||
if (isDefined(tooltipHref)) {
|
||||
window.location.href = tooltipHref;
|
||||
}
|
||||
};
|
||||
|
||||
const renderTooltip = () => {
|
||||
const formattedWithPercentage = `${formattedValue} (${normalizedValue.toFixed(1)}%)`;
|
||||
|
||||
return (
|
||||
<GraphWidgetTooltip
|
||||
items={[
|
||||
{
|
||||
label: legendLabel,
|
||||
formattedValue: formattedWithPercentage,
|
||||
dotColor: colorScheme.solid,
|
||||
},
|
||||
]}
|
||||
showClickHint={isDefined(tooltipHref)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderValueEndLine = (props: RadialBarCustomLayerProps) => {
|
||||
if (clampedNormalizedValue === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { center, bars } = props;
|
||||
|
||||
const valueBar = bars?.find((bar) => bar.data.x === 'value');
|
||||
if (!valueBar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const endAngle = valueBar.arc.endAngle - Math.PI / 2;
|
||||
const arcInnerRadius = valueBar.arc.innerRadius;
|
||||
const arcOuterRadius = valueBar.arc.outerRadius;
|
||||
|
||||
const [centerX, centerY] = center;
|
||||
const x1 = centerX + Math.cos(endAngle) * arcInnerRadius;
|
||||
const y1 = centerY + Math.sin(endAngle) * arcInnerRadius;
|
||||
const x2 = centerX + Math.cos(endAngle) * arcOuterRadius;
|
||||
const y2 = centerY + Math.sin(endAngle) * arcOuterRadius;
|
||||
|
||||
return (
|
||||
<g>
|
||||
<line
|
||||
x1={x1}
|
||||
y1={y1}
|
||||
x2={x2}
|
||||
y2={y2}
|
||||
stroke={colorScheme.solid}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledContainer
|
||||
id={id}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<StyledChartContainer>
|
||||
<ResponsiveRadialBar
|
||||
data={data}
|
||||
startAngle={-90}
|
||||
endAngle={90}
|
||||
innerRadius={0.7}
|
||||
padding={0.2}
|
||||
colors={[`url(#${gradientId})`, theme.background.tertiary]}
|
||||
defs={defs}
|
||||
fill={[
|
||||
{
|
||||
match: (d: { x: string }) => d.x === 'value',
|
||||
id: gradientId,
|
||||
},
|
||||
]}
|
||||
enableTracks={false}
|
||||
enableRadialGrid={false}
|
||||
enableCircularGrid={false}
|
||||
enableLabels={false}
|
||||
isInteractive={false}
|
||||
radialAxisStart={null}
|
||||
radialAxisEnd={null}
|
||||
circularAxisInner={null}
|
||||
circularAxisOuter={null}
|
||||
<StyledContainer>
|
||||
<StyledChartContainer>
|
||||
<ResponsiveRadialBar
|
||||
data={data}
|
||||
startAngle={-90}
|
||||
endAngle={90}
|
||||
innerRadius={0.7}
|
||||
padding={0.2}
|
||||
colors={[`url(#${gradientId})`, theme.background.tertiary]}
|
||||
defs={defs}
|
||||
fill={[
|
||||
{
|
||||
match: (d: { x: string }) => d.x === 'value',
|
||||
id: gradientId,
|
||||
},
|
||||
]}
|
||||
enableTracks={false}
|
||||
enableRadialGrid={false}
|
||||
enableCircularGrid={false}
|
||||
enableLabels={false}
|
||||
isInteractive={true}
|
||||
tooltip={renderTooltip}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
radialAxisStart={null}
|
||||
radialAxisEnd={null}
|
||||
circularAxisInner={null}
|
||||
circularAxisOuter={null}
|
||||
layers={['bars', renderValueEndLine]}
|
||||
/>
|
||||
{showValue && (
|
||||
<StyledH1Title
|
||||
title={formattedValue}
|
||||
fontColor={H1TitleFontColor.Primary}
|
||||
/>
|
||||
{showValue && (
|
||||
<StyledH1Title
|
||||
title={`${displayValue}${unit}`}
|
||||
fontColor={H1TitleFontColor.Primary}
|
||||
/>
|
||||
)}
|
||||
</StyledChartContainer>
|
||||
<StyledLegendContainer>
|
||||
<StyledLegendItem>
|
||||
<StyledDot />
|
||||
<StyledLegendLabel>{legendLabel}</StyledLegendLabel>
|
||||
<StyledLegendValue>{`${displayValue}${unit}`}</StyledLegendValue>
|
||||
</StyledLegendItem>
|
||||
</StyledLegendContainer>
|
||||
</StyledContainer>
|
||||
<AppTooltip
|
||||
anchorSelect={`#${id}`}
|
||||
place="top-start"
|
||||
noArrow
|
||||
delay={TooltipDelay.noDelay}
|
||||
clickable
|
||||
>
|
||||
{tooltipContent}
|
||||
</AppTooltip>
|
||||
</>
|
||||
)}
|
||||
</StyledChartContainer>
|
||||
<GraphWidgetLegend
|
||||
show={showLegend}
|
||||
items={[
|
||||
{
|
||||
id: 'gauge',
|
||||
label: legendLabel,
|
||||
formattedValue: formattedValue,
|
||||
color: colorScheme.solid,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export type GraphWidgetLegendItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
formattedValue: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
type GraphWidgetLegendProps = {
|
||||
items: GraphWidgetLegendItem[];
|
||||
show?: boolean;
|
||||
};
|
||||
|
||||
const StyledLegendContainer = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${({ theme }) => theme.spacing(3)};
|
||||
justify-content: center;
|
||||
padding: ${({ theme }) => theme.spacing(2)} 0;
|
||||
`;
|
||||
|
||||
const StyledLegendItem = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
`;
|
||||
|
||||
const StyledLegendLabel = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
`;
|
||||
|
||||
const StyledLegendValue = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
`;
|
||||
|
||||
const StyledDot = styled.div<{ color: string }>`
|
||||
background: ${({ color }) => color};
|
||||
border-radius: 50%;
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
export const GraphWidgetLegend = ({
|
||||
items,
|
||||
show = true,
|
||||
}: GraphWidgetLegendProps) => {
|
||||
if (!show || items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledLegendContainer>
|
||||
{items.map((item) => (
|
||||
<StyledLegendItem key={item.id}>
|
||||
<StyledDot color={item.color} />
|
||||
<StyledLegendLabel>{item.label}</StyledLegendLabel>
|
||||
<StyledLegendValue>{item.formattedValue}</StyledLegendValue>
|
||||
</StyledLegendItem>
|
||||
))}
|
||||
</StyledLegendContainer>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
ResponsivePie,
|
||||
type ComputedDatum,
|
||||
type DatumId,
|
||||
type PieCustomLayerProps,
|
||||
} from '@nivo/pie';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { createGradientDef } from '../utils/createGradientDef';
|
||||
import { createGraphColorRegistry } from '../utils/createGraphColorRegistry';
|
||||
import { getColorSchemeByIndex } from '../utils/getColorSchemeByIndex';
|
||||
import {
|
||||
formatGraphValue,
|
||||
type GraphValueFormatOptions,
|
||||
} from '../utils/graphFormatters';
|
||||
import { GraphWidgetLegend } from './GraphWidgetLegend';
|
||||
import { GraphWidgetTooltip } from './GraphWidgetTooltip';
|
||||
|
||||
type GraphWidgetPieChartProps = {
|
||||
data: Array<{ id: string; value: number; label?: string }>;
|
||||
showLegend?: boolean;
|
||||
tooltipHref?: string;
|
||||
id: string;
|
||||
} & GraphValueFormatOptions;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledChartContainer = styled.div`
|
||||
flex: 1;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const GraphWidgetPieChart = ({
|
||||
data,
|
||||
showLegend = true,
|
||||
tooltipHref,
|
||||
id,
|
||||
displayType,
|
||||
decimals,
|
||||
prefix,
|
||||
suffix,
|
||||
customFormatter,
|
||||
}: GraphWidgetPieChartProps) => {
|
||||
const theme = useTheme();
|
||||
const [hoveredSliceId, setHoveredSliceId] = useState<DatumId | null>(null);
|
||||
|
||||
const colorRegistry = createGraphColorRegistry(theme);
|
||||
|
||||
const formatOptions: GraphValueFormatOptions = {
|
||||
displayType,
|
||||
decimals,
|
||||
prefix,
|
||||
suffix,
|
||||
customFormatter,
|
||||
};
|
||||
|
||||
const totalValue = data.reduce((sum, item) => sum + item.value, 0);
|
||||
|
||||
let cumulativeAngle = 0;
|
||||
const enrichedData = data.map((item, index) => {
|
||||
const colorScheme = getColorSchemeByIndex(colorRegistry, index);
|
||||
const isHovered = hoveredSliceId === item.id;
|
||||
const gradientId = `${colorScheme.name}Gradient-${id}-${index}`;
|
||||
const percentage = totalValue > 0 ? (item.value / totalValue) * 100 : 0;
|
||||
|
||||
const sliceAngle = (percentage / 100) * 360;
|
||||
const middleAngle = cumulativeAngle + sliceAngle / 2;
|
||||
cumulativeAngle += sliceAngle;
|
||||
|
||||
return {
|
||||
...item,
|
||||
gradientId,
|
||||
colorScheme,
|
||||
isHovered,
|
||||
percentage,
|
||||
middleAngle,
|
||||
};
|
||||
});
|
||||
|
||||
const defs = enrichedData.map((item) =>
|
||||
createGradientDef(
|
||||
item.colorScheme,
|
||||
item.gradientId,
|
||||
item.isHovered,
|
||||
item.middleAngle,
|
||||
),
|
||||
);
|
||||
|
||||
const fill = enrichedData.map((item) => ({
|
||||
match: { id: item.id },
|
||||
id: item.gradientId,
|
||||
}));
|
||||
|
||||
const handleSliceClick = () => {
|
||||
if (isDefined(tooltipHref)) {
|
||||
window.location.href = tooltipHref;
|
||||
}
|
||||
};
|
||||
|
||||
const renderTooltip = (
|
||||
datum: ComputedDatum<{ id: string; value: number; label?: string }>,
|
||||
) => {
|
||||
const item = enrichedData.find((d) => d.id === datum.id);
|
||||
if (!item) return null;
|
||||
|
||||
const formattedValue = formatGraphValue(item.value, formatOptions);
|
||||
const formattedWithPercentage = `${formattedValue} (${item.percentage.toFixed(1)}%)`;
|
||||
|
||||
return (
|
||||
<GraphWidgetTooltip
|
||||
items={[
|
||||
{
|
||||
label: item.label || item.id,
|
||||
formattedValue: formattedWithPercentage,
|
||||
dotColor: item.colorScheme.solid,
|
||||
},
|
||||
]}
|
||||
showClickHint={isDefined(tooltipHref)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSliceEndLines = (
|
||||
layerProps: PieCustomLayerProps<{
|
||||
id: string;
|
||||
value: number;
|
||||
label?: string;
|
||||
}>,
|
||||
) => {
|
||||
const { dataWithArc, centerX, centerY, innerRadius, radius } = layerProps;
|
||||
|
||||
if (!dataWithArc || !Array.isArray(dataWithArc) || dataWithArc.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<g>
|
||||
{dataWithArc.map((datum) => {
|
||||
const enrichedItem = enrichedData.find((d) => d.id === datum.id);
|
||||
const lineColor = enrichedItem
|
||||
? enrichedItem.colorScheme.solid
|
||||
: theme.border.color.strong;
|
||||
|
||||
const angle = datum.arc.endAngle - Math.PI / 2;
|
||||
const x1 = centerX + Math.cos(angle) * innerRadius;
|
||||
const y1 = centerY + Math.sin(angle) * innerRadius;
|
||||
const x2 = centerX + Math.cos(angle) * radius;
|
||||
const y2 = centerY + Math.sin(angle) * radius;
|
||||
|
||||
return (
|
||||
<line
|
||||
key={`${datum.id}-separator`}
|
||||
x1={x1}
|
||||
y1={y1}
|
||||
x2={x2}
|
||||
y2={y2}
|
||||
stroke={lineColor}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer id={id}>
|
||||
<StyledChartContainer>
|
||||
<ResponsivePie
|
||||
data={data}
|
||||
innerRadius={0.8}
|
||||
colors={enrichedData.map((item) => `url(#${item.gradientId})`)}
|
||||
borderWidth={0}
|
||||
enableArcLinkLabels={false}
|
||||
enableArcLabels={false}
|
||||
tooltip={({ datum }) => renderTooltip(datum)}
|
||||
onClick={handleSliceClick}
|
||||
onMouseEnter={(datum) => setHoveredSliceId(datum.id)}
|
||||
onMouseLeave={() => setHoveredSliceId(null)}
|
||||
defs={defs}
|
||||
fill={fill}
|
||||
layers={['arcs', renderSliceEndLines]}
|
||||
/>
|
||||
</StyledChartContainer>
|
||||
<GraphWidgetLegend
|
||||
show={showLegend}
|
||||
items={enrichedData.map((item) => ({
|
||||
id: item.id,
|
||||
label: item.label || item.id,
|
||||
formattedValue: formatGraphValue(item.value, formatOptions),
|
||||
color: item.colorScheme.solid,
|
||||
}))}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconArrowUpRight } from 'twenty-ui/display';
|
||||
|
||||
const StyledTooltipContent = styled.div`
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
const StyledTooltipRow = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.extraLight};
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledDot = styled.div<{ $color: string }>`
|
||||
background: ${({ $color }) => $color};
|
||||
border-radius: 50%;
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const StyledTooltipValue = styled.span`
|
||||
margin-left: auto;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledTooltipLink = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
cursor: default;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export type GraphWidgetTooltipItem = {
|
||||
label: string;
|
||||
formattedValue: string;
|
||||
dotColor: string;
|
||||
};
|
||||
|
||||
type GraphWidgetTooltipProps = {
|
||||
items: GraphWidgetTooltipItem[];
|
||||
showClickHint?: boolean;
|
||||
};
|
||||
|
||||
export const GraphWidgetTooltip = ({
|
||||
items,
|
||||
showClickHint = false,
|
||||
}: GraphWidgetTooltipProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<StyledTooltipContent>
|
||||
{items.map((item, index) => (
|
||||
<StyledTooltipRow key={index}>
|
||||
<StyledDot $color={item.dotColor} />
|
||||
<span>{item.label}</span>
|
||||
<StyledTooltipValue>{item.formattedValue}</StyledTooltipValue>
|
||||
</StyledTooltipRow>
|
||||
))}
|
||||
{showClickHint && (
|
||||
<StyledTooltipLink>
|
||||
<span>{t`Click to see data`}</span>
|
||||
<IconArrowUpRight size={theme.icon.size.sm} />
|
||||
</StyledTooltipLink>
|
||||
)}
|
||||
</StyledTooltipContent>
|
||||
);
|
||||
};
|
||||
|
|
@ -20,12 +20,25 @@ const meta: Meta<typeof GraphWidgetGaugeChart> = {
|
|||
max: {
|
||||
control: { type: 'number' },
|
||||
},
|
||||
unit: {
|
||||
displayType: {
|
||||
control: 'select',
|
||||
options: ['percentage', 'number', 'shortNumber', 'currency', 'custom'],
|
||||
},
|
||||
prefix: {
|
||||
control: 'text',
|
||||
},
|
||||
suffix: {
|
||||
control: 'text',
|
||||
},
|
||||
decimals: {
|
||||
control: 'number',
|
||||
},
|
||||
showValue: {
|
||||
control: 'boolean',
|
||||
},
|
||||
showLegend: {
|
||||
control: 'boolean',
|
||||
},
|
||||
legendLabel: {
|
||||
control: 'text',
|
||||
},
|
||||
|
|
@ -47,10 +60,10 @@ const Container = ({ children }: { children: React.ReactNode }) => (
|
|||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
value: 50,
|
||||
value: 0.5,
|
||||
min: 0,
|
||||
max: 100,
|
||||
unit: '%',
|
||||
max: 1,
|
||||
displayType: 'percentage',
|
||||
showValue: true,
|
||||
legendLabel: 'Conversion rate',
|
||||
tooltipHref: 'https://example.com',
|
||||
|
|
@ -62,8 +75,12 @@ export const Default: Story = {
|
|||
value={args.value}
|
||||
min={args.min}
|
||||
max={args.max}
|
||||
unit={args.unit}
|
||||
displayType={args.displayType}
|
||||
decimals={args.decimals}
|
||||
prefix={args.prefix}
|
||||
suffix={args.suffix}
|
||||
showValue={args.showValue}
|
||||
showLegend={args.showLegend}
|
||||
legendLabel={args.legendLabel}
|
||||
tooltipHref={args.tooltipHref}
|
||||
id={args.id}
|
||||
|
|
@ -81,10 +98,10 @@ export const Catalog: Story = {
|
|||
name: 'value',
|
||||
values: [0, 25, 50, 75, 100],
|
||||
props: (value: number) => ({
|
||||
value,
|
||||
value: value / 100,
|
||||
min: 0,
|
||||
max: 100,
|
||||
unit: '%',
|
||||
max: 1,
|
||||
displayType: 'percentage' as const,
|
||||
id: `gauge-chart-catalog-${value}`,
|
||||
}),
|
||||
labels: (value: number) => {
|
||||
|
|
@ -107,11 +124,11 @@ export const Catalog: Story = {
|
|||
value={args.value}
|
||||
min={args.min}
|
||||
max={args.max}
|
||||
unit={args.unit}
|
||||
displayType={args.displayType}
|
||||
showValue={true}
|
||||
legendLabel="Percentage"
|
||||
tooltipHref="https://example.com"
|
||||
id={args.id}
|
||||
id="gauge-chart-catalog"
|
||||
/>
|
||||
</Container>
|
||||
),
|
||||
|
|
@ -122,7 +139,7 @@ export const WithoutValue: Story = {
|
|||
value: 65,
|
||||
min: 0,
|
||||
max: 100,
|
||||
unit: '%',
|
||||
displayType: 'percentage',
|
||||
showValue: false,
|
||||
legendLabel: 'Conversion rate',
|
||||
tooltipHref: 'https://example.com',
|
||||
|
|
@ -134,8 +151,12 @@ export const WithoutValue: Story = {
|
|||
value={args.value}
|
||||
min={args.min}
|
||||
max={args.max}
|
||||
unit={args.unit}
|
||||
displayType={args.displayType}
|
||||
decimals={args.decimals}
|
||||
prefix={args.prefix}
|
||||
suffix={args.suffix}
|
||||
showValue={args.showValue}
|
||||
showLegend={args.showLegend}
|
||||
legendLabel={args.legendLabel}
|
||||
tooltipHref={args.tooltipHref}
|
||||
id={args.id}
|
||||
|
|
@ -149,7 +170,7 @@ export const Revenue: Story = {
|
|||
value: 750,
|
||||
min: 0,
|
||||
max: 1000,
|
||||
unit: 'K',
|
||||
displayType: 'shortNumber',
|
||||
showValue: true,
|
||||
legendLabel: 'Revenue',
|
||||
tooltipHref: 'https://example.com',
|
||||
|
|
@ -161,8 +182,12 @@ export const Revenue: Story = {
|
|||
value={args.value}
|
||||
min={args.min}
|
||||
max={args.max}
|
||||
unit={args.unit}
|
||||
displayType={args.displayType}
|
||||
decimals={args.decimals}
|
||||
prefix={args.prefix}
|
||||
suffix={args.suffix}
|
||||
showValue={args.showValue}
|
||||
showLegend={args.showLegend}
|
||||
legendLabel={args.legendLabel}
|
||||
tooltipHref={args.tooltipHref}
|
||||
id={args.id}
|
||||
|
|
@ -176,7 +201,7 @@ export const Temperature: Story = {
|
|||
value: 22,
|
||||
min: -10,
|
||||
max: 40,
|
||||
unit: '°C',
|
||||
suffix: '°C',
|
||||
showValue: true,
|
||||
legendLabel: 'Temperature',
|
||||
tooltipHref: 'https://example.com',
|
||||
|
|
@ -188,8 +213,12 @@ export const Temperature: Story = {
|
|||
value={args.value}
|
||||
min={args.min}
|
||||
max={args.max}
|
||||
unit={args.unit}
|
||||
displayType={args.displayType}
|
||||
decimals={args.decimals}
|
||||
prefix={args.prefix}
|
||||
suffix={args.suffix}
|
||||
showValue={args.showValue}
|
||||
showLegend={args.showLegend}
|
||||
legendLabel={args.legendLabel}
|
||||
tooltipHref={args.tooltipHref}
|
||||
id={args.id}
|
||||
|
|
@ -203,7 +232,7 @@ export const Storage: Story = {
|
|||
value: 384,
|
||||
min: 0,
|
||||
max: 512,
|
||||
unit: 'GB',
|
||||
suffix: ' GB',
|
||||
showValue: true,
|
||||
legendLabel: 'Storage Used',
|
||||
tooltipHref: 'https://example.com',
|
||||
|
|
@ -215,8 +244,12 @@ export const Storage: Story = {
|
|||
value={args.value}
|
||||
min={args.min}
|
||||
max={args.max}
|
||||
unit={args.unit}
|
||||
displayType={args.displayType}
|
||||
decimals={args.decimals}
|
||||
prefix={args.prefix}
|
||||
suffix={args.suffix}
|
||||
showValue={args.showValue}
|
||||
showLegend={args.showLegend}
|
||||
legendLabel={args.legendLabel}
|
||||
tooltipHref={args.tooltipHref}
|
||||
id={args.id}
|
||||
|
|
@ -230,7 +263,8 @@ export const Rating: Story = {
|
|||
value: 4.2,
|
||||
min: 0,
|
||||
max: 5,
|
||||
unit: ' ⭐',
|
||||
suffix: ' ⭐',
|
||||
decimals: 1,
|
||||
showValue: true,
|
||||
legendLabel: 'Average Rating',
|
||||
tooltipHref: 'https://example.com',
|
||||
|
|
@ -242,8 +276,43 @@ export const Rating: Story = {
|
|||
value={args.value}
|
||||
min={args.min}
|
||||
max={args.max}
|
||||
unit={args.unit}
|
||||
displayType={args.displayType}
|
||||
decimals={args.decimals}
|
||||
prefix={args.prefix}
|
||||
suffix={args.suffix}
|
||||
showValue={args.showValue}
|
||||
showLegend={args.showLegend}
|
||||
legendLabel={args.legendLabel}
|
||||
tooltipHref={args.tooltipHref}
|
||||
id={args.id}
|
||||
/>
|
||||
</Container>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithoutLegend: Story = {
|
||||
args: {
|
||||
value: 65,
|
||||
min: 0,
|
||||
max: 100,
|
||||
displayType: 'percentage',
|
||||
showValue: true,
|
||||
legendLabel: 'Conversion rate',
|
||||
tooltipHref: 'https://example.com',
|
||||
id: 'gauge-chart-without-legend',
|
||||
},
|
||||
render: (args) => (
|
||||
<Container>
|
||||
<GraphWidgetGaugeChart
|
||||
value={args.value}
|
||||
min={args.min}
|
||||
max={args.max}
|
||||
displayType={args.displayType}
|
||||
decimals={args.decimals}
|
||||
prefix={args.prefix}
|
||||
suffix={args.suffix}
|
||||
showValue={args.showValue}
|
||||
showLegend={false}
|
||||
legendLabel={args.legendLabel}
|
||||
tooltipHref={args.tooltipHref}
|
||||
id={args.id}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { GraphWidgetLegend } from '../GraphWidgetLegend';
|
||||
|
||||
const meta: Meta<typeof GraphWidgetLegend> = {
|
||||
title: 'Modules/Dashboards/Graphs/GraphWidgetLegend',
|
||||
component: GraphWidgetLegend,
|
||||
decorators: [ComponentDecorator],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof GraphWidgetLegend>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<GraphWidgetLegend
|
||||
show={true}
|
||||
items={[
|
||||
{
|
||||
id: 'sales',
|
||||
label: 'Sales',
|
||||
formattedValue: '$45,231',
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
id: 'marketing',
|
||||
label: 'Marketing',
|
||||
formattedValue: '$12,543',
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
id: 'operations',
|
||||
label: 'Operations',
|
||||
formattedValue: '$8,765',
|
||||
color: 'red',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const SingleItem: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<GraphWidgetLegend
|
||||
show={true}
|
||||
items={[
|
||||
{
|
||||
id: 'revenue',
|
||||
label: 'Revenue',
|
||||
formattedValue: '750',
|
||||
color: 'blue',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,332 @@
|
|||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
|
||||
import { CatalogDecorator, ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { GraphWidgetPieChart } from '../GraphWidgetPieChart';
|
||||
|
||||
const meta: Meta<typeof GraphWidgetPieChart> = {
|
||||
title: 'Modules/Dashboards/Graphs/GraphWidgetPieChart',
|
||||
component: GraphWidgetPieChart,
|
||||
decorators: [ComponentDecorator],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
argTypes: {
|
||||
data: {
|
||||
control: 'object',
|
||||
},
|
||||
displayType: {
|
||||
control: 'select',
|
||||
options: ['percentage', 'number', 'shortNumber', 'currency', 'custom'],
|
||||
},
|
||||
prefix: {
|
||||
control: 'text',
|
||||
},
|
||||
suffix: {
|
||||
control: 'text',
|
||||
},
|
||||
decimals: {
|
||||
control: 'number',
|
||||
},
|
||||
showLegend: {
|
||||
control: 'boolean',
|
||||
},
|
||||
tooltipHref: {
|
||||
control: 'text',
|
||||
},
|
||||
id: {
|
||||
control: 'text',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof GraphWidgetPieChart>;
|
||||
|
||||
const Container = ({ children }: { children: React.ReactNode }) => (
|
||||
<div style={{ width: '300px', height: '300px' }}>{children}</div>
|
||||
);
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
data: [
|
||||
{ id: 'qualified', value: 35, label: 'Qualified' },
|
||||
{ id: 'contacted', value: 25, label: 'Contacted' },
|
||||
{ id: 'unqualified', value: 20, label: 'Unqualified' },
|
||||
{ id: 'proposal', value: 15, label: 'Proposal' },
|
||||
{ id: 'negotiation', value: 5, label: 'Negotiation' },
|
||||
],
|
||||
showLegend: true,
|
||||
tooltipHref: 'https://example.com/leads',
|
||||
id: 'pie-chart-default',
|
||||
},
|
||||
render: (args) => (
|
||||
<Container>
|
||||
<GraphWidgetPieChart
|
||||
data={args.data}
|
||||
displayType={args.displayType}
|
||||
prefix={args.prefix}
|
||||
suffix={args.suffix}
|
||||
decimals={args.decimals}
|
||||
showLegend={args.showLegend}
|
||||
tooltipHref={args.tooltipHref}
|
||||
id={args.id}
|
||||
/>
|
||||
</Container>
|
||||
),
|
||||
};
|
||||
|
||||
export const Revenue: Story = {
|
||||
args: {
|
||||
data: [
|
||||
{ id: 'product-a', value: 420000, label: 'Product A' },
|
||||
{ id: 'product-b', value: 380000, label: 'Product B' },
|
||||
{ id: 'product-c', value: 250000, label: 'Product C' },
|
||||
{ id: 'product-d', value: 180000, label: 'Product D' },
|
||||
],
|
||||
prefix: '$',
|
||||
displayType: 'shortNumber',
|
||||
showLegend: true,
|
||||
tooltipHref: 'https://example.com/revenue',
|
||||
id: 'pie-chart-revenue',
|
||||
},
|
||||
render: (args) => (
|
||||
<Container>
|
||||
<GraphWidgetPieChart
|
||||
data={args.data}
|
||||
displayType={args.displayType}
|
||||
prefix={args.prefix}
|
||||
suffix={args.suffix}
|
||||
decimals={args.decimals}
|
||||
showLegend={args.showLegend}
|
||||
tooltipHref={args.tooltipHref}
|
||||
id={args.id}
|
||||
/>
|
||||
</Container>
|
||||
),
|
||||
};
|
||||
|
||||
export const TaskStatus: Story = {
|
||||
args: {
|
||||
data: [
|
||||
{ id: 'completed', value: 45, label: 'Completed' },
|
||||
{ id: 'in-progress', value: 30, label: 'In Progress' },
|
||||
{ id: 'todo', value: 25, label: 'To Do' },
|
||||
],
|
||||
displayType: 'percentage',
|
||||
showLegend: true,
|
||||
tooltipHref: 'https://example.com/tasks',
|
||||
id: 'pie-chart-task-status',
|
||||
},
|
||||
render: (args) => (
|
||||
<Container>
|
||||
<GraphWidgetPieChart
|
||||
data={args.data}
|
||||
displayType={args.displayType}
|
||||
prefix={args.prefix}
|
||||
suffix={args.suffix}
|
||||
decimals={args.decimals}
|
||||
showLegend={args.showLegend}
|
||||
tooltipHref={args.tooltipHref}
|
||||
id={args.id}
|
||||
/>
|
||||
</Container>
|
||||
),
|
||||
};
|
||||
|
||||
export const TwoSlices: Story = {
|
||||
args: {
|
||||
data: [
|
||||
{ id: 'active', value: 75, label: 'Active' },
|
||||
{ id: 'inactive', value: 25, label: 'Inactive' },
|
||||
],
|
||||
displayType: 'percentage',
|
||||
showLegend: true,
|
||||
tooltipHref: 'https://example.com/status',
|
||||
id: 'pie-chart-two-slices',
|
||||
},
|
||||
render: (args) => (
|
||||
<Container>
|
||||
<GraphWidgetPieChart
|
||||
data={args.data}
|
||||
displayType={args.displayType}
|
||||
prefix={args.prefix}
|
||||
suffix={args.suffix}
|
||||
decimals={args.decimals}
|
||||
showLegend={args.showLegend}
|
||||
tooltipHref={args.tooltipHref}
|
||||
id={args.id}
|
||||
/>
|
||||
</Container>
|
||||
),
|
||||
};
|
||||
|
||||
export const ManySlices: Story = {
|
||||
args: {
|
||||
data: [
|
||||
{ id: 'category-1', value: 20, label: 'Category 1' },
|
||||
{ id: 'category-2', value: 18, label: 'Category 2' },
|
||||
{ id: 'category-3', value: 16, label: 'Category 3' },
|
||||
{ id: 'category-4', value: 14, label: 'Category 4' },
|
||||
{ id: 'category-5', value: 12, label: 'Category 5' },
|
||||
{ id: 'category-6', value: 10, label: 'Category 6' },
|
||||
{ id: 'category-7', value: 6, label: 'Category 7' },
|
||||
{ id: 'category-8', value: 4, label: 'Category 8' },
|
||||
],
|
||||
showLegend: true,
|
||||
tooltipHref: 'https://example.com/categories',
|
||||
id: 'pie-chart-many-slices',
|
||||
},
|
||||
render: (args) => (
|
||||
<Container>
|
||||
<GraphWidgetPieChart
|
||||
data={args.data}
|
||||
displayType={args.displayType}
|
||||
prefix={args.prefix}
|
||||
suffix={args.suffix}
|
||||
decimals={args.decimals}
|
||||
showLegend={args.showLegend}
|
||||
tooltipHref={args.tooltipHref}
|
||||
id={args.id}
|
||||
/>
|
||||
</Container>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithoutLegend: Story = {
|
||||
args: {
|
||||
data: [
|
||||
{ id: 'web', value: 45, label: 'Web' },
|
||||
{ id: 'mobile', value: 35, label: 'Mobile' },
|
||||
{ id: 'desktop', value: 20, label: 'Desktop' },
|
||||
],
|
||||
displayType: 'percentage',
|
||||
showLegend: false,
|
||||
tooltipHref: 'https://example.com/platforms',
|
||||
id: 'pie-chart-without-legend',
|
||||
},
|
||||
render: (args) => (
|
||||
<Container>
|
||||
<GraphWidgetPieChart
|
||||
data={args.data}
|
||||
displayType={args.displayType}
|
||||
prefix={args.prefix}
|
||||
suffix={args.suffix}
|
||||
decimals={args.decimals}
|
||||
showLegend={args.showLegend}
|
||||
tooltipHref={args.tooltipHref}
|
||||
id={args.id}
|
||||
/>
|
||||
</Container>
|
||||
),
|
||||
};
|
||||
|
||||
export const MarketShare: Story = {
|
||||
args: {
|
||||
data: [
|
||||
{ id: 'brand-a', value: 35.5, label: 'Brand A' },
|
||||
{ id: 'brand-b', value: 28.2, label: 'Brand B' },
|
||||
{ id: 'brand-c', value: 18.7, label: 'Brand C' },
|
||||
{ id: 'others', value: 17.6, label: 'Others' },
|
||||
],
|
||||
displayType: 'percentage',
|
||||
showLegend: true,
|
||||
tooltipHref: 'https://example.com/market',
|
||||
id: 'pie-chart-market-share',
|
||||
},
|
||||
render: (args) => (
|
||||
<Container>
|
||||
<GraphWidgetPieChart
|
||||
data={args.data}
|
||||
displayType={args.displayType}
|
||||
prefix={args.prefix}
|
||||
suffix={args.suffix}
|
||||
decimals={args.decimals}
|
||||
showLegend={args.showLegend}
|
||||
tooltipHref={args.tooltipHref}
|
||||
id={args.id}
|
||||
/>
|
||||
</Container>
|
||||
),
|
||||
};
|
||||
|
||||
export const Storage: Story = {
|
||||
args: {
|
||||
data: [
|
||||
{ id: 'documents', value: 125, label: 'Documents' },
|
||||
{ id: 'media', value: 280, label: 'Media' },
|
||||
{ id: 'applications', value: 95, label: 'Applications' },
|
||||
{ id: 'system', value: 50, label: 'System' },
|
||||
],
|
||||
suffix: ' GB',
|
||||
showLegend: true,
|
||||
tooltipHref: 'https://example.com/storage',
|
||||
id: 'pie-chart-storage',
|
||||
},
|
||||
render: (args) => (
|
||||
<Container>
|
||||
<GraphWidgetPieChart
|
||||
data={args.data}
|
||||
displayType={args.displayType}
|
||||
prefix={args.prefix}
|
||||
suffix={args.suffix}
|
||||
decimals={args.decimals}
|
||||
showLegend={args.showLegend}
|
||||
tooltipHref={args.tooltipHref}
|
||||
id={args.id}
|
||||
/>
|
||||
</Container>
|
||||
),
|
||||
};
|
||||
|
||||
export const Catalog: Story = {
|
||||
decorators: [CatalogDecorator],
|
||||
parameters: {
|
||||
catalog: {
|
||||
dimensions: [
|
||||
{
|
||||
name: 'slices',
|
||||
values: [2, 3, 5],
|
||||
props: (sliceCount: number) => {
|
||||
const dataMap: Record<
|
||||
number,
|
||||
Array<{ id: string; value: number; label?: string }>
|
||||
> = {
|
||||
2: [
|
||||
{ id: 'yes', value: 65, label: 'Yes' },
|
||||
{ id: 'no', value: 35, label: 'No' },
|
||||
],
|
||||
3: [
|
||||
{ id: 'gold', value: 45, label: 'Gold' },
|
||||
{ id: 'silver', value: 35, label: 'Silver' },
|
||||
{ id: 'bronze', value: 20, label: 'Bronze' },
|
||||
],
|
||||
5: [
|
||||
{ id: 'item-1', value: 30, label: 'Item 1' },
|
||||
{ id: 'item-2', value: 25, label: 'Item 2' },
|
||||
{ id: 'item-3', value: 20, label: 'Item 3' },
|
||||
{ id: 'item-4', value: 15, label: 'Item 4' },
|
||||
{ id: 'item-5', value: 10, label: 'Item 5' },
|
||||
],
|
||||
};
|
||||
|
||||
return {
|
||||
data: dataMap[sliceCount] || dataMap[3],
|
||||
id: `pie-chart-catalog-${sliceCount}`,
|
||||
};
|
||||
},
|
||||
labels: (sliceCount: number) => `${sliceCount} slices`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
render: (args) => (
|
||||
<Container>
|
||||
<GraphWidgetPieChart
|
||||
data={args.data}
|
||||
displayType="percentage"
|
||||
showLegend={true}
|
||||
id={args.id}
|
||||
/>
|
||||
</Container>
|
||||
),
|
||||
};
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { GraphWidgetTooltip } from '../GraphWidgetTooltip';
|
||||
|
||||
const meta: Meta<typeof GraphWidgetTooltip> = {
|
||||
title: 'Modules/Dashboards/Graphs/GraphWidgetTooltip',
|
||||
component: GraphWidgetTooltip,
|
||||
decorators: [ComponentDecorator],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof GraphWidgetTooltip>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{
|
||||
label: 'Revenue',
|
||||
formattedValue: '$45,231',
|
||||
dotColor: 'blue',
|
||||
},
|
||||
],
|
||||
showClickHint: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithClickHint: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{
|
||||
label: 'Sales',
|
||||
formattedValue: '1,234 units',
|
||||
dotColor: 'green',
|
||||
},
|
||||
],
|
||||
showClickHint: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const MultipleItems: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{
|
||||
label: 'Q1',
|
||||
formattedValue: '$12,345',
|
||||
dotColor: 'blue',
|
||||
},
|
||||
{
|
||||
label: 'Q2',
|
||||
formattedValue: '$23,456',
|
||||
dotColor: 'green',
|
||||
},
|
||||
{
|
||||
label: 'Q3',
|
||||
formattedValue: '$34,567',
|
||||
dotColor: 'red',
|
||||
},
|
||||
],
|
||||
showClickHint: false,
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { type GraphColorScheme } from './GraphColorScheme';
|
||||
|
||||
export type GraphColorRegistry = {
|
||||
[key: string]: GraphColorScheme;
|
||||
};
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
export type GraphColorScheme = {
|
||||
name: string;
|
||||
gradient: {
|
||||
normal: [string, string];
|
||||
hover: [string, string];
|
||||
};
|
||||
solid: string;
|
||||
};
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
export const calculateAngularGradient = (angle: number) => {
|
||||
const gradientAngle = (angle * Math.PI) / 180 + Math.PI / 2;
|
||||
|
||||
const dx = Math.sin(gradientAngle);
|
||||
const dy = -Math.cos(gradientAngle);
|
||||
|
||||
return {
|
||||
x1: `${50 - dx * 50}%`,
|
||||
y1: `${50 - dy * 50}%`,
|
||||
x2: `${50 + dx * 50}%`,
|
||||
y2: `${50 + dy * 50}%`,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { type GraphColorScheme } from '../types/GraphColorScheme';
|
||||
import { calculateAngularGradient } from './calculateAngularGradient';
|
||||
|
||||
export const createGradientDef = (
|
||||
colorScheme: GraphColorScheme,
|
||||
id: string,
|
||||
isHovered: boolean = false,
|
||||
angle?: number,
|
||||
) => {
|
||||
const colors = isHovered
|
||||
? colorScheme.gradient.hover
|
||||
: colorScheme.gradient.normal;
|
||||
|
||||
const coords =
|
||||
angle !== undefined
|
||||
? calculateAngularGradient(angle)
|
||||
: { x1: '0%', y1: '0%', x2: '0%', y2: '100%' };
|
||||
|
||||
return {
|
||||
id,
|
||||
type: 'linearGradient' as const,
|
||||
...coords,
|
||||
colors: [
|
||||
{ offset: 0, color: colors[0] },
|
||||
{ offset: 100, color: colors[1] },
|
||||
],
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { type ThemeType } from 'twenty-ui/theme';
|
||||
|
||||
import { type GraphColorRegistry } from '../types/GraphColorRegistry';
|
||||
|
||||
export const createGraphColorRegistry = (
|
||||
theme: ThemeType,
|
||||
): GraphColorRegistry => ({
|
||||
blue: {
|
||||
name: 'blue',
|
||||
gradient: {
|
||||
normal: [theme.adaptiveColors.blue1, theme.adaptiveColors.blue2],
|
||||
hover: [theme.adaptiveColors.blue3, theme.adaptiveColors.blue4],
|
||||
},
|
||||
solid: theme.color.blue,
|
||||
},
|
||||
purple: {
|
||||
name: 'purple',
|
||||
gradient: {
|
||||
normal: [theme.adaptiveColors.purple1, theme.adaptiveColors.purple2],
|
||||
hover: [theme.adaptiveColors.purple3, theme.adaptiveColors.purple4],
|
||||
},
|
||||
solid: theme.color.purple,
|
||||
},
|
||||
turquoise: {
|
||||
name: 'turquoise',
|
||||
gradient: {
|
||||
normal: [
|
||||
theme.adaptiveColors.turquoise1,
|
||||
theme.adaptiveColors.turquoise2,
|
||||
],
|
||||
hover: [theme.adaptiveColors.turquoise3, theme.adaptiveColors.turquoise4],
|
||||
},
|
||||
solid: theme.color.turquoise,
|
||||
},
|
||||
orange: {
|
||||
name: 'orange',
|
||||
gradient: {
|
||||
normal: [theme.adaptiveColors.orange1, theme.adaptiveColors.orange2],
|
||||
hover: [theme.adaptiveColors.orange3, theme.adaptiveColors.orange4],
|
||||
},
|
||||
solid: theme.color.orange,
|
||||
},
|
||||
pink: {
|
||||
name: 'pink',
|
||||
gradient: {
|
||||
normal: [theme.adaptiveColors.pink1, theme.adaptiveColors.pink2],
|
||||
hover: [theme.adaptiveColors.pink3, theme.adaptiveColors.pink4],
|
||||
},
|
||||
solid: theme.color.pink,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { type GraphColorRegistry } from '../types/GraphColorRegistry';
|
||||
import { type GraphColorScheme } from '../types/GraphColorScheme';
|
||||
|
||||
export const getColorSchemeByIndex = (
|
||||
registry: GraphColorRegistry,
|
||||
index: number,
|
||||
): GraphColorScheme => {
|
||||
const schemes = Object.values(registry);
|
||||
return schemes[index % schemes.length];
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { type GraphColorRegistry } from '../types/GraphColorRegistry';
|
||||
import { type GraphColorScheme } from '../types/GraphColorScheme';
|
||||
|
||||
export const getColorSchemeByName = (
|
||||
registry: GraphColorRegistry,
|
||||
name: string,
|
||||
): GraphColorScheme | undefined => {
|
||||
return registry[name];
|
||||
};
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { formatAmount } from '~/utils/format/formatAmount';
|
||||
import { formatNumber } from '~/utils/format/number';
|
||||
|
||||
export type GraphValueFormatOptions = {
|
||||
displayType?: 'percentage' | 'number' | 'shortNumber' | 'currency' | 'custom';
|
||||
decimals?: number;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
customFormatter?: (value: number) => string;
|
||||
};
|
||||
|
||||
export const formatGraphValue = (
|
||||
value: number,
|
||||
options?: GraphValueFormatOptions,
|
||||
): string => {
|
||||
const {
|
||||
displayType = 'number',
|
||||
decimals,
|
||||
prefix = '',
|
||||
suffix = '',
|
||||
customFormatter,
|
||||
} = options || {};
|
||||
|
||||
if (displayType === 'custom' && isDefined(customFormatter)) {
|
||||
return customFormatter(value);
|
||||
}
|
||||
|
||||
switch (displayType) {
|
||||
case 'percentage':
|
||||
return `${formatNumber(value * 100, decimals)}%`;
|
||||
|
||||
case 'shortNumber':
|
||||
return `${prefix}${formatAmount(value)}${suffix}`;
|
||||
|
||||
case 'currency': {
|
||||
const currencyPrefix = prefix || '$';
|
||||
return `${currencyPrefix}${formatNumber(value, decimals)}${suffix}`;
|
||||
}
|
||||
|
||||
case 'number':
|
||||
default:
|
||||
return `${prefix}${formatNumber(value, decimals)}${suffix}`;
|
||||
}
|
||||
};
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
"@keystatic/core": "^0.5.45",
|
||||
"@keystatic/next": "^5.0.3",
|
||||
"@markdoc/markdoc": "^0.5.1",
|
||||
"@nivo/calendar": "^0.87.0",
|
||||
"@nivo/calendar": "^0.99.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"drizzle-kit": "^0.20.14",
|
||||
"facepaint": "^1.2.1",
|
||||
|
|
|
|||
175
yarn.lock
175
yarn.lock
|
|
@ -9718,13 +9718,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@nivo/calendar@npm:^0.87.0":
|
||||
version: 0.87.0
|
||||
resolution: "@nivo/calendar@npm:0.87.0"
|
||||
"@nivo/calendar@npm:^0.99.0":
|
||||
version: 0.99.0
|
||||
resolution: "@nivo/calendar@npm:0.99.0"
|
||||
dependencies:
|
||||
"@nivo/core": "npm:0.87.0"
|
||||
"@nivo/legends": "npm:0.87.0"
|
||||
"@nivo/tooltip": "npm:0.87.0"
|
||||
"@nivo/core": "npm:0.99.0"
|
||||
"@nivo/legends": "npm:0.99.0"
|
||||
"@nivo/text": "npm:0.99.0"
|
||||
"@nivo/theming": "npm:0.99.0"
|
||||
"@nivo/tooltip": "npm:0.99.0"
|
||||
"@types/d3-scale": "npm:^4.0.8"
|
||||
"@types/d3-time": "npm:^1.0.10"
|
||||
"@types/d3-time-format": "npm:^3.0.0"
|
||||
|
|
@ -9733,28 +9735,8 @@ __metadata:
|
|||
d3-time-format: "npm:^3.0.0"
|
||||
lodash: "npm:^4.17.21"
|
||||
peerDependencies:
|
||||
react: ">= 16.14.0 < 19.0.0"
|
||||
checksum: 10c0/5cb0f4ceb45109695f1b86e4215ef464b7c33449c50d45a436f71a30029bcfca6b91956d6be9862d17cd0d8bf4dd80e2ce281cb85ace83ee07159a0e9f603242
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@nivo/colors@npm:0.87.0":
|
||||
version: 0.87.0
|
||||
resolution: "@nivo/colors@npm:0.87.0"
|
||||
dependencies:
|
||||
"@nivo/core": "npm:0.87.0"
|
||||
"@types/d3-color": "npm:^3.0.0"
|
||||
"@types/d3-scale": "npm:^4.0.8"
|
||||
"@types/d3-scale-chromatic": "npm:^3.0.0"
|
||||
"@types/prop-types": "npm:^15.7.2"
|
||||
d3-color: "npm:^3.1.0"
|
||||
d3-scale: "npm:^4.0.2"
|
||||
d3-scale-chromatic: "npm:^3.0.0"
|
||||
lodash: "npm:^4.17.21"
|
||||
prop-types: "npm:^15.7.2"
|
||||
peerDependencies:
|
||||
react: ">= 16.14.0 < 19.0.0"
|
||||
checksum: 10c0/872f4e2d8392f89633531250e0474a365e0456dff146d2e8ba8576226effd1eda7e721ce1dd15a792b05d06caa28a11510a5ca8e55fb84aedb8ba3d964f357f5
|
||||
react: ^16.14 || ^17.0 || ^18.0 || ^19.0
|
||||
checksum: 10c0/b647566eb890e7fc839bc36de99c5426f62c11b35841fcb54afca167c45150ea159ef4a7a1706c3b1ca2ca12da99d6b8b26fe5499656d037deea92e35a4cfa9a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -9777,28 +9759,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@nivo/core@npm:0.87.0":
|
||||
version: 0.87.0
|
||||
resolution: "@nivo/core@npm:0.87.0"
|
||||
dependencies:
|
||||
"@nivo/tooltip": "npm:0.87.0"
|
||||
"@react-spring/web": "npm:9.4.5 || ^9.7.2"
|
||||
"@types/d3-shape": "npm:^3.1.6"
|
||||
d3-color: "npm:^3.1.0"
|
||||
d3-format: "npm:^1.4.4"
|
||||
d3-interpolate: "npm:^3.0.1"
|
||||
d3-scale: "npm:^4.0.2"
|
||||
d3-scale-chromatic: "npm:^3.0.0"
|
||||
d3-shape: "npm:^3.2.0"
|
||||
d3-time-format: "npm:^3.0.0"
|
||||
lodash: "npm:^4.17.21"
|
||||
prop-types: "npm:^15.7.2"
|
||||
peerDependencies:
|
||||
react: ">= 16.14.0 < 19.0.0"
|
||||
checksum: 10c0/75bb5e48cf57c8e31fbd9b9f33121fb4bffb98d47a967e5135527f8fde51537b68da3d894dc7231bf3c969523985eac6a5fc6d50827b958d51e6907a6391ed84
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@nivo/core@npm:0.99.0, @nivo/core@npm:^0.99.0":
|
||||
version: 0.99.0
|
||||
resolution: "@nivo/core@npm:0.99.0"
|
||||
|
|
@ -9823,20 +9783,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@nivo/legends@npm:0.87.0":
|
||||
version: 0.87.0
|
||||
resolution: "@nivo/legends@npm:0.87.0"
|
||||
dependencies:
|
||||
"@nivo/colors": "npm:0.87.0"
|
||||
"@nivo/core": "npm:0.87.0"
|
||||
"@types/d3-scale": "npm:^4.0.8"
|
||||
d3-scale: "npm:^4.0.2"
|
||||
peerDependencies:
|
||||
react: ">= 16.14.0 < 19.0.0"
|
||||
checksum: 10c0/1501bf698cefa2695d1124c38da5fc7712a5ce9911683c6694cbea1a481b9daef8ca7b0fc49eb85530c50347c571ea5b686caff3d6a81174b66bc38318bd317d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@nivo/legends@npm:0.99.0":
|
||||
version: 0.99.0
|
||||
resolution: "@nivo/legends@npm:0.99.0"
|
||||
|
|
@ -9875,6 +9821,24 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@nivo/pie@npm:^0.99.0":
|
||||
version: 0.99.0
|
||||
resolution: "@nivo/pie@npm:0.99.0"
|
||||
dependencies:
|
||||
"@nivo/arcs": "npm:0.99.0"
|
||||
"@nivo/colors": "npm:0.99.0"
|
||||
"@nivo/core": "npm:0.99.0"
|
||||
"@nivo/legends": "npm:0.99.0"
|
||||
"@nivo/theming": "npm:0.99.0"
|
||||
"@nivo/tooltip": "npm:0.99.0"
|
||||
"@types/d3-shape": "npm:^3.1.6"
|
||||
d3-shape: "npm:^3.2.0"
|
||||
peerDependencies:
|
||||
react: ^16.14 || ^17.0 || ^18.0 || ^19.0
|
||||
checksum: 10c0/402a791f101337c5c1254ad68c46efeee41956372e75dcf93f2cf9b4e79c03f75a464c817782c65741000ddcf2c7ca6423dcce646072991a97b4e46d344520ec
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@nivo/polar-axes@npm:0.99.0":
|
||||
version: 0.99.0
|
||||
resolution: "@nivo/polar-axes@npm:0.99.0"
|
||||
|
|
@ -9955,18 +9919,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@nivo/tooltip@npm:0.87.0":
|
||||
version: 0.87.0
|
||||
resolution: "@nivo/tooltip@npm:0.87.0"
|
||||
dependencies:
|
||||
"@nivo/core": "npm:0.87.0"
|
||||
"@react-spring/web": "npm:9.4.5 || ^9.7.2"
|
||||
peerDependencies:
|
||||
react: ">= 16.14.0 < 19.0.0"
|
||||
checksum: 10c0/ac6b1b0bb0a09017c0e5055432e4c5ee771301615db3ee3d34abb55c900f765af78830eb2b18c8d94ff49ddeaa19895e907c793fb431943ade11cc2d7369b7e4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@nivo/tooltip@npm:0.99.0":
|
||||
version: 0.99.0
|
||||
resolution: "@nivo/tooltip@npm:0.99.0"
|
||||
|
|
@ -15249,18 +15201,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-spring/animated@npm:~9.7.4":
|
||||
version: 9.7.4
|
||||
resolution: "@react-spring/animated@npm:9.7.4"
|
||||
dependencies:
|
||||
"@react-spring/shared": "npm:~9.7.4"
|
||||
"@react-spring/types": "npm:~9.7.4"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
checksum: 10c0/7620fe7f058ca81d321710dd391062eda8bd298d2abdf63c5e230348aecd1b80730e5b96bdf052807838a20c8961efe1f9e2840e6cce3cacae5b74a28622eff8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-spring/core@npm:9.4.5 || ^9.7.2 || ^10.0, @react-spring/core@npm:~10.0.1":
|
||||
version: 10.0.1
|
||||
resolution: "@react-spring/core@npm:10.0.1"
|
||||
|
|
@ -15274,19 +15214,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-spring/core@npm:~9.7.4":
|
||||
version: 9.7.4
|
||||
resolution: "@react-spring/core@npm:9.7.4"
|
||||
dependencies:
|
||||
"@react-spring/animated": "npm:~9.7.4"
|
||||
"@react-spring/shared": "npm:~9.7.4"
|
||||
"@react-spring/types": "npm:~9.7.4"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
checksum: 10c0/cdbc5e222edc5f25746cad36086acf20563157543557a3436b55bf0527460f68e5eb98e2927e186937eba37abe3067c227b685c07b9dc393f987a8ff7abe2455
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-spring/rafz@npm:~10.0.1":
|
||||
version: 10.0.1
|
||||
resolution: "@react-spring/rafz@npm:10.0.1"
|
||||
|
|
@ -15294,13 +15221,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-spring/rafz@npm:~9.7.4":
|
||||
version: 9.7.4
|
||||
resolution: "@react-spring/rafz@npm:9.7.4"
|
||||
checksum: 10c0/975e27d6c19ed055dea91e97e473831a862aad2dc882f5402e98ac1f3f636f4ec05ee5cff623e5f6ece25388bb50a024bbc6dcdb6b750e43d53f0321140c5e52
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-spring/shared@npm:~10.0.1":
|
||||
version: 10.0.1
|
||||
resolution: "@react-spring/shared@npm:10.0.1"
|
||||
|
|
@ -15313,18 +15233,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-spring/shared@npm:~9.7.4":
|
||||
version: 9.7.4
|
||||
resolution: "@react-spring/shared@npm:9.7.4"
|
||||
dependencies:
|
||||
"@react-spring/rafz": "npm:~9.7.4"
|
||||
"@react-spring/types": "npm:~9.7.4"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
checksum: 10c0/ae5cbb41f4876ebf365e0b2293466678d421eb68e736bfb4a0836a550518eb916fee763274f81c5af8a6a25f776df1480031e39f2981639aa79ac6cb6d290a15
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-spring/types@npm:~10.0.1":
|
||||
version: 10.0.1
|
||||
resolution: "@react-spring/types@npm:10.0.1"
|
||||
|
|
@ -15332,28 +15240,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-spring/types@npm:~9.7.4":
|
||||
version: 9.7.4
|
||||
resolution: "@react-spring/types@npm:9.7.4"
|
||||
checksum: 10c0/2ad43c0463dadb8caa58a7eeafc9ba447fa66cd7d9b9ea2241d58c619353155ea94d7e6a89fc6da0f071d500216c654b35a64a12a3a586d85e304ed6c93546fd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-spring/web@npm:9.4.5 || ^9.7.2":
|
||||
version: 9.7.4
|
||||
resolution: "@react-spring/web@npm:9.7.4"
|
||||
dependencies:
|
||||
"@react-spring/animated": "npm:~9.7.4"
|
||||
"@react-spring/core": "npm:~9.7.4"
|
||||
"@react-spring/shared": "npm:~9.7.4"
|
||||
"@react-spring/types": "npm:~9.7.4"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
checksum: 10c0/6312b612e53ae7a7cd0ab5fe16aaccbc6fba5d71248e30373a1f46e6aec997723095eff3c1a47f59aa8d5788b7092a898dee2120f841814f1d1295e5a4fcf5fb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-spring/web@npm:9.4.5 || ^9.7.2 || ^10.0":
|
||||
version: 10.0.1
|
||||
resolution: "@react-spring/web@npm:10.0.1"
|
||||
|
|
@ -20655,7 +20541,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/prop-types@npm:*, @types/prop-types@npm:^15.7.2":
|
||||
"@types/prop-types@npm:*":
|
||||
version: 15.7.12
|
||||
resolution: "@types/prop-types@npm:15.7.12"
|
||||
checksum: 10c0/1babcc7db6a1177779f8fde0ccc78d64d459906e6ef69a4ed4dd6339c920c2e05b074ee5a92120fe4e9d9f1a01c952f843ebd550bee2332fc2ef81d1706878f8
|
||||
|
|
@ -50963,6 +50849,7 @@ __metadata:
|
|||
"@lingui/vite-plugin": "npm:^5.1.2"
|
||||
"@nivo/core": "npm:^0.99.0"
|
||||
"@nivo/line": "npm:^0.99.0"
|
||||
"@nivo/pie": "npm:^0.99.0"
|
||||
"@nivo/radial-bar": "npm:^0.99.0"
|
||||
"@react-pdf/renderer": "npm:^4.1.6"
|
||||
"@scalar/api-reference-react": "npm:^0.4.36"
|
||||
|
|
@ -51333,7 +51220,7 @@ __metadata:
|
|||
"@keystatic/next": "npm:^5.0.3"
|
||||
"@markdoc/markdoc": "npm:^0.5.1"
|
||||
"@next/eslint-plugin-next": "npm:^14.1.4"
|
||||
"@nivo/calendar": "npm:^0.87.0"
|
||||
"@nivo/calendar": "npm:^0.99.0"
|
||||
"@types/facepaint": "npm:^1.2.5"
|
||||
date-fns: "npm:^2.30.0"
|
||||
drizzle-kit: "npm:^0.20.14"
|
||||
|
|
|
|||
Loading…
Reference in a new issue