[Dashboard]- Add GraphWidgetLineChart (#14386)

closes https://github.com/twentyhq/core-team-issues/issues/1371
This commit is contained in:
nitin
2025-09-11 14:09:17 +05:30
committed by GitHub
parent 8d10e738ac
commit ceffc82b9c
11 changed files with 1382 additions and 2 deletions
@@ -5,8 +5,10 @@ import {
WidgetType,
} from '@/settings/page-layout/mocks/mockWidgets';
import styled from '@emotion/styled';
import {
IconChartBar,
IconChartLine,
IconChartPie,
IconGauge,
IconNumber,
@@ -49,6 +51,11 @@ const graphTypeOptions = [
icon: IconNumber,
title: 'Number',
},
{
type: GraphType.LINE,
icon: IconChartLine,
title: 'Line Chart',
},
];
export const CommandMenuPageLayoutGraphTypeSelect = () => {
@@ -0,0 +1,380 @@
import styled from '@emotion/styled';
import {
ResponsiveLine,
type LineSeries,
type Point,
type SliceTooltipProps,
} from '@nivo/line';
import { type ScaleLinearSpec, type ScaleSpec } from '@nivo/scales';
import { useId, useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useTheme } from '@emotion/react';
import { type GraphColor } from '../types/GraphColor';
import { createGradientDef } from '../utils/createGradientDef';
import { createGraphColorRegistry } from '../utils/createGraphColorRegistry';
import { getColorScheme } from '../utils/getColorScheme';
import {
formatGraphValue,
type GraphValueFormatOptions,
} from '../utils/graphFormatters';
import { GraphWidgetLegend } from './GraphWidgetLegend';
import { GraphWidgetTooltip } from './GraphWidgetTooltip';
type LineChartDataPoint = {
x: number | string | Date;
y: number | null;
to?: string;
};
type LineChartSeries = {
id: string;
label?: string;
color?: GraphColor;
data: LineChartDataPoint[];
enableArea?: boolean;
};
type GraphWidgetLineChartProps = {
data: LineChartSeries[];
showLegend?: boolean;
showGrid?: boolean;
enablePoints?: boolean;
xAxisLabel?: string;
yAxisLabel?: string;
id: string;
enableArea?: boolean;
stackedArea?: boolean;
curve?:
| 'linear'
| 'monotoneX'
| 'step'
| 'stepBefore'
| 'stepAfter'
| 'natural';
lineWidth?: number;
enableSlices?: 'x' | 'y' | false;
xScale?: ScaleSpec;
yScale?: ScaleSpec;
} & GraphValueFormatOptions;
const getYScaleWithStacking = (
yScale: ScaleSpec | undefined,
stackedArea: boolean | undefined,
): ScaleSpec => {
if (!yScale || yScale.type === 'linear') {
const linearScale: ScaleLinearSpec = {
min: 0,
max: 'auto',
...yScale,
type: 'linear',
stacked: stackedArea,
};
return linearScale;
}
return yScale;
};
const StyledContainer = styled.div`
align-items: center;
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
width: 100%;
`;
const StyledChartContainer = styled.div<{ $isClickable?: boolean }>`
flex: 1;
position: relative;
width: 100%;
${({ $isClickable }) =>
$isClickable &&
`
svg g circle {
cursor: pointer;
}
`}
`;
export const GraphWidgetLineChart = ({
data,
showLegend = true,
showGrid = true,
enablePoints = false,
xAxisLabel,
yAxisLabel,
id,
enableArea = false,
stackedArea = false,
curve = 'monotoneX',
lineWidth = 2,
enableSlices = 'x',
xScale = { type: 'linear' },
yScale = { type: 'linear', min: 0, max: 'auto' },
displayType,
decimals,
prefix,
suffix,
customFormatter,
}: GraphWidgetLineChartProps) => {
const theme = useTheme();
const instanceId = useId();
const colorRegistry = createGraphColorRegistry(theme);
const formatOptions: GraphValueFormatOptions = {
displayType,
decimals,
prefix,
suffix,
customFormatter,
};
const dataMap = useMemo(() => {
const map: Record<string, LineChartSeries> = {};
for (const series of data) {
map[series.id] = series;
}
return map;
}, [data]);
const enrichedSeries = useMemo(() => {
return data.map((series, index) => {
const colorScheme = getColorScheme(colorRegistry, series.color, index);
const shouldEnableArea = series.enableArea ?? enableArea;
const gradientId = `lineGradient-${id}-${instanceId}-${series.id}-${index}`;
return {
...series,
colorScheme,
gradientId,
shouldEnableArea,
label: series.label || series.id,
};
});
}, [data, colorRegistry, id, instanceId, enableArea]);
const nivoData = data.map((series) => ({
id: series.id,
data: series.data.map((point) => ({
x: point.x,
y: point.y,
})),
}));
const defs = enrichedSeries
.filter((series) => series.shouldEnableArea)
.map((series) =>
createGradientDef(
series.colorScheme,
series.gradientId,
false,
90,
theme.name === 'light',
),
);
const fill = enrichedSeries
.filter((series) => series.shouldEnableArea)
.map((series) => ({
match: { id: series.id },
id: series.gradientId,
}));
const colors = enrichedSeries.map((series) => series.colorScheme.solid);
const handlePointClick = (point: Point<LineSeries>) => {
const series = dataMap[point.seriesId];
if (isDefined(series)) {
const dataPoint = series.data[point.indexInSeries];
if (isDefined(dataPoint?.to) === true) {
window.location.href = dataPoint.to;
}
}
};
const hasClickableItems = data.some((series) =>
series.data.some((point) => isDefined(point.to)),
);
const renderSliceTooltip = ({ slice }: SliceTooltipProps<LineSeries>) => {
const tooltipItems = slice.points
.map((point) => {
const enrichedSeriesItem = enrichedSeries.find(
(s) => s.id === point.seriesId,
);
if (!enrichedSeriesItem) return null;
return {
label: enrichedSeriesItem.label,
formattedValue: formatGraphValue(
Number(point.data.y || 0),
formatOptions,
),
dotColor: enrichedSeriesItem.colorScheme.solid,
};
})
.filter(isDefined);
const hasClickablePoint = slice.points.some((point) => {
const series = dataMap[point.seriesId];
if (isDefined(series)) {
const dataPoint = series.data[point.indexInSeries];
return isDefined(dataPoint?.to);
}
return false;
});
return (
<GraphWidgetTooltip
items={tooltipItems}
showClickHint={hasClickablePoint}
/>
);
};
const renderPointTooltip = (point: Point<LineSeries>) => {
const enrichedSeriesItem = enrichedSeries.find(
(s) => s.id === point.seriesId,
);
if (!enrichedSeriesItem) return null;
const series = dataMap[point.seriesId];
const dataPoint = series?.data[point.indexInSeries];
return (
<GraphWidgetTooltip
items={[
{
label: enrichedSeriesItem.label,
formattedValue: formatGraphValue(
Number(point.data.y || 0),
formatOptions,
),
dotColor: enrichedSeriesItem.colorScheme.solid,
},
]}
showClickHint={isDefined(dataPoint?.to)}
/>
);
};
const getAxisBottomConfig = () => ({
tickSize: 0,
tickPadding: 5,
tickRotation: 0,
legend: xAxisLabel,
legendPosition: 'middle' as const,
legendOffset: 40,
});
const getAxisLeftConfig = () => ({
tickSize: 0,
tickPadding: 5,
tickRotation: 0,
legend: yAxisLabel,
legendPosition: 'middle' as const,
legendOffset: -50,
format: (value: number) => formatGraphValue(value, formatOptions),
});
const legendItems = enrichedSeries.map((series) => {
const total = series.data.reduce((sum, point) => sum + (point.y || 0), 0);
return {
id: series.id,
label: series.label,
formattedValue: formatGraphValue(total, formatOptions),
color: series.colorScheme.solid,
};
});
return (
<StyledContainer id={id}>
<StyledChartContainer $isClickable={hasClickableItems}>
<ResponsiveLine
data={nivoData}
margin={{ top: 20, right: 20, bottom: 60, left: 70 }}
xScale={xScale}
yScale={getYScaleWithStacking(yScale, stackedArea)}
curve={curve}
lineWidth={lineWidth}
enableArea={enableArea}
areaBaselineValue={0}
enablePoints={enablePoints}
pointSize={6}
pointBorderWidth={0}
areaOpacity={theme.name === 'dark' ? 0.8 : 1}
colors={colors}
areaBlendMode={theme.name === 'dark' ? 'screen' : 'multiply'}
defs={defs}
fill={fill}
axisTop={null}
axisRight={null}
axisBottom={getAxisBottomConfig()}
axisLeft={getAxisLeftConfig()}
enableGridX={showGrid}
enableGridY={showGrid}
enableSlices={enableSlices}
sliceTooltip={enableSlices === 'x' ? renderSliceTooltip : undefined}
tooltip={
enableSlices === false
? ({ point }) => renderPointTooltip(point)
: undefined
}
onClick={(datum) => {
if ('seriesId' in datum) {
handlePointClick(datum as Point<LineSeries>);
}
}}
useMesh={true}
crosshairType="cross"
theme={{
axis: {
domain: {
line: {
stroke: theme.border.color.light,
strokeWidth: 1,
},
},
ticks: {
line: {
stroke: theme.border.color.light,
strokeWidth: 1,
},
text: {
fill: theme.font.color.secondary,
fontSize: 12,
},
},
legend: {
text: {
fill: theme.font.color.secondary,
fontSize: 12,
fontWeight: theme.font.weight.regular,
},
},
},
grid: {
line: {
stroke: theme.border.color.light,
strokeWidth: 1,
strokeDasharray: '4 4',
},
},
crosshair: {
line: {
stroke: theme.font.color.tertiary,
strokeWidth: 1,
strokeDasharray: '2 2',
},
},
}}
/>
</StyledChartContainer>
<GraphWidgetLegend show={showLegend} items={legendItems} />
</StyledContainer>
);
};
@@ -0,0 +1,862 @@
import { type Meta, type StoryObj } from '@storybook/react';
import { CatalogDecorator, ComponentDecorator } from 'twenty-ui/testing';
import { GraphWidgetLineChart } from '../GraphWidgetLineChart';
import { type ComponentProps } from 'react';
const meta: Meta<typeof GraphWidgetLineChart> = {
title: 'Modules/Dashboards/Graphs/GraphWidgetLineChart',
component: GraphWidgetLineChart,
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',
},
showGrid: {
control: 'boolean',
},
enablePoints: {
control: 'boolean',
},
xAxisLabel: {
control: 'text',
},
yAxisLabel: {
control: 'text',
},
enableArea: {
control: 'boolean',
},
stackedArea: {
control: 'boolean',
},
curve: {
control: 'select',
options: [
'linear',
'monotoneX',
'step',
'stepBefore',
'stepAfter',
'natural',
],
},
lineWidth: {
control: 'number',
},
enableSlices: {
control: 'select',
options: ['x', 'y', false],
},
},
};
export default meta;
type Story = StoryObj<typeof GraphWidgetLineChart>;
type ChartArgs = ComponentProps<typeof GraphWidgetLineChart>;
const Container = ({ children }: { children: React.ReactNode }) => (
<div style={{ width: '700px', height: '500px' }}>{children}</div>
);
const renderChart = (args: ChartArgs) => (
<Container>
<GraphWidgetLineChart
id={args.id}
data={args.data}
showLegend={args.showLegend}
showGrid={args.showGrid}
enablePoints={args.enablePoints}
xAxisLabel={args.xAxisLabel}
yAxisLabel={args.yAxisLabel}
displayType={args.displayType}
prefix={args.prefix}
suffix={args.suffix}
decimals={args.decimals}
enableArea={args.enableArea}
stackedArea={args.stackedArea}
curve={args.curve}
lineWidth={args.lineWidth}
enableSlices={args.enableSlices}
xScale={args.xScale}
yScale={args.yScale}
/>
</Container>
);
const generateLinearData = (points: number = 10) => {
return Array.from({ length: points }, (_, i) => ({
x: i,
y: Math.floor(Math.random() * 100) + 20,
}));
};
export const Default: Story = {
args: {
id: 'line-chart-default',
data: [
{
id: 'series1',
label: 'Revenue',
color: 'blue',
data: generateLinearData(12),
},
{
id: 'series2',
label: 'Profit',
color: 'turquoise',
data: generateLinearData(12),
},
],
showLegend: true,
showGrid: true,
enablePoints: false,
xAxisLabel: 'Month',
yAxisLabel: 'Value',
displayType: 'shortNumber',
prefix: '$',
},
render: renderChart,
};
export const WithArea: Story = {
render: renderChart,
args: {
id: 'line-chart-area',
data: [
{
id: 'sales',
label: 'Sales',
color: 'purple',
data: generateLinearData(12),
},
{
id: 'costs',
label: 'Costs',
color: 'orange',
data: generateLinearData(12),
},
],
enableArea: true,
showLegend: true,
showGrid: true,
enablePoints: false,
xAxisLabel: 'Period',
yAxisLabel: 'Amount',
displayType: 'currency',
},
};
export const StackedArea: Story = {
render: renderChart,
args: {
id: 'line-chart-stacked',
data: [
{
id: 'product-a',
label: 'Product A',
color: 'blue',
data: generateLinearData(8),
},
{
id: 'product-b',
label: 'Product B',
color: 'turquoise',
data: generateLinearData(8),
},
{
id: 'product-c',
label: 'Product C',
color: 'purple',
data: generateLinearData(8),
},
],
enableArea: true,
stackedArea: true,
showLegend: true,
showGrid: true,
enablePoints: false,
curve: 'monotoneX',
xAxisLabel: 'Quarter',
yAxisLabel: 'Revenue',
yScale: {
type: 'linear',
min: 0,
max: 'auto',
},
displayType: 'shortNumber',
prefix: '$',
},
};
export const WithPoints: Story = {
render: renderChart,
args: {
id: 'line-chart-points',
data: [
{
id: 'performance',
label: 'Performance',
color: 'pink',
data: generateLinearData(8),
},
],
showLegend: true,
showGrid: true,
enablePoints: true,
lineWidth: 3,
xAxisLabel: 'Week',
yAxisLabel: 'Score',
displayType: 'percentage',
},
};
export const StepChart: Story = {
render: renderChart,
args: {
id: 'line-chart-step',
data: [
{
id: 'inventory',
label: 'Inventory Level',
color: 'orange',
data: generateLinearData(10),
},
],
curve: 'step',
showLegend: true,
showGrid: true,
enablePoints: false,
lineWidth: 2,
xAxisLabel: 'Day',
yAxisLabel: 'Units',
displayType: 'number',
},
};
export const LogScaleDemo: Story = {
render: renderChart,
args: {
id: 'line-chart-log-scale',
data: [
{
id: 'exponential',
label: 'Exponential Growth',
color: 'purple',
data: [
{ x: 0, y: 10 },
{ x: 1, y: 100 },
{ x: 2, y: 1000 },
{ x: 3, y: 10000 },
{ x: 4, y: 100000 },
{ x: 5, y: 1000000 },
],
},
{
id: 'linear',
label: 'Linear Growth',
color: 'turquoise',
data: [
{ x: 0, y: 50 },
{ x: 1, y: 100 },
{ x: 2, y: 150 },
{ x: 3, y: 200 },
{ x: 4, y: 250 },
{ x: 5, y: 300 },
],
},
],
showLegend: true,
showGrid: true,
enablePoints: true,
xAxisLabel: 'Time',
yAxisLabel: 'Value (log scale)',
yScale: {
type: 'log',
base: 10,
min: 'auto',
max: 'auto',
},
displayType: 'shortNumber',
},
};
export const WithNullValues: Story = {
render: renderChart,
args: {
id: 'line-chart-nulls',
data: [
{
id: 'incomplete',
label: 'With Gaps',
color: 'blue',
data: [
{ x: 0, y: 30 },
{ x: 1, y: 40 },
{ x: 2, y: null },
{ x: 3, y: null },
{ x: 4, y: 60 },
{ x: 5, y: 70 },
{ x: 6, y: 65 },
],
},
],
showLegend: true,
showGrid: true,
enablePoints: true,
xAxisLabel: 'Time',
yAxisLabel: 'Measurement',
displayType: 'number',
},
};
export const InteractiveWithLinks: Story = {
render: renderChart,
args: {
id: 'line-chart-interactive',
data: [
{
id: 'clickable',
label: 'Click Points',
color: 'turquoise',
data: [
{ x: 0, y: 30, to: '#point-0' },
{ x: 1, y: 45, to: '#point-1' },
{ x: 2, y: 38, to: '#point-2' },
{ x: 3, y: 52, to: '#point-3' },
{ x: 4, y: 48, to: '#point-4' },
{ x: 5, y: 60, to: '#point-5' },
],
},
],
showLegend: true,
showGrid: true,
enablePoints: true,
enableSlices: false,
xAxisLabel: 'Step',
yAxisLabel: 'Progress',
displayType: 'percentage',
},
};
export const MultiSeriesMixed: Story = {
render: renderChart,
args: {
id: 'line-chart-mixed',
data: [
{
id: 'actual',
label: 'Actual',
color: 'blue',
data: generateLinearData(12),
enableArea: true,
},
{
id: 'forecast',
label: 'Forecast',
color: 'purple',
data: generateLinearData(12),
enableArea: false,
},
{
id: 'target',
label: 'Target',
color: 'orange',
data: generateLinearData(12).map((d) => ({ ...d, y: 75 })),
enableArea: false,
},
],
enableArea: false,
showLegend: true,
showGrid: true,
enablePoints: false,
curve: 'monotoneX',
xAxisLabel: 'Month',
yAxisLabel: 'Value',
displayType: 'shortNumber',
enableSlices: 'x',
},
};
export const OverlappingGradientBlend: Story = {
render: renderChart,
args: {
id: 'line-chart-blend',
data: [
{
id: 'red-series',
label: 'Red Wave',
color: 'red',
data: [
{ x: 0, y: 50 },
{ x: 1, y: 70 },
{ x: 2, y: 65 },
{ x: 3, y: 80 },
{ x: 4, y: 75 },
{ x: 5, y: 90 },
{ x: 6, y: 85 },
{ x: 7, y: 95 },
],
},
{
id: 'blue-series',
label: 'Blue Wave',
color: 'blue',
data: [
{ x: 0, y: 40 },
{ x: 1, y: 60 },
{ x: 2, y: 70 },
{ x: 3, y: 75 },
{ x: 4, y: 80 },
{ x: 5, y: 70 },
{ x: 6, y: 65 },
{ x: 7, y: 60 },
],
},
{
id: 'green-series',
label: 'Green Wave',
color: 'turquoise',
data: [
{ x: 0, y: 30 },
{ x: 1, y: 45 },
{ x: 2, y: 55 },
{ x: 3, y: 60 },
{ x: 4, y: 65 },
{ x: 5, y: 60 },
{ x: 6, y: 55 },
{ x: 7, y: 50 },
],
},
],
enableArea: true,
showLegend: true,
showGrid: true,
enablePoints: false,
curve: 'monotoneX',
xAxisLabel: 'Time',
yAxisLabel: 'Value',
displayType: 'number',
},
};
export const HighContrastOverlap: Story = {
render: renderChart,
args: {
id: 'line-chart-contrast',
data: [
{
id: 'yellow-series',
label: 'Yellow',
color: 'yellow',
data: [
{ x: 0, y: 60 },
{ x: 1, y: 80 },
{ x: 2, y: 85 },
{ x: 3, y: 90 },
{ x: 4, y: 85 },
{ x: 5, y: 80 },
{ x: 6, y: 75 },
],
},
{
id: 'purple-series',
label: 'Purple',
color: 'purple',
data: [
{ x: 0, y: 50 },
{ x: 1, y: 70 },
{ x: 2, y: 80 },
{ x: 3, y: 85 },
{ x: 4, y: 90 },
{ x: 5, y: 85 },
{ x: 6, y: 70 },
],
},
],
enableArea: true,
showLegend: true,
showGrid: true,
enablePoints: true,
curve: 'natural',
xAxisLabel: 'Day',
yAxisLabel: 'Score',
displayType: 'number',
},
};
export const CurveComparison: Story = {
render: renderChart,
args: {
id: 'line-chart-curves',
data: [
{
id: 'dataset',
label: 'Same Data',
color: 'orange',
data: [
{ x: 0, y: 20 },
{ x: 1, y: 60 },
{ x: 2, y: 40 },
{ x: 3, y: 80 },
{ x: 4, y: 30 },
{ x: 5, y: 70 },
{ x: 6, y: 50 },
],
},
],
showLegend: true,
showGrid: true,
enablePoints: true,
curve: 'linear',
xAxisLabel: 'X Axis',
yAxisLabel: 'Y Axis',
displayType: 'number',
},
};
export const StepInterpolations: Story = {
render: renderChart,
args: {
id: 'line-chart-steps',
data: [
{
id: 'step-normal',
label: 'Step',
color: 'blue',
data: [
{ x: 0, y: 30 },
{ x: 1, y: 50 },
{ x: 2, y: 45 },
{ x: 3, y: 60 },
{ x: 4, y: 55 },
{ x: 5, y: 70 },
],
},
{
id: 'step-before',
label: 'Step Before',
color: 'purple',
data: [
{ x: 0, y: 25 },
{ x: 1, y: 45 },
{ x: 2, y: 40 },
{ x: 3, y: 55 },
{ x: 4, y: 50 },
{ x: 5, y: 65 },
],
},
{
id: 'step-after',
label: 'Step After',
color: 'turquoise',
data: [
{ x: 0, y: 20 },
{ x: 1, y: 40 },
{ x: 2, y: 35 },
{ x: 3, y: 50 },
{ x: 4, y: 45 },
{ x: 5, y: 60 },
],
},
],
showLegend: true,
showGrid: true,
enablePoints: true,
curve: 'step',
xAxisLabel: 'Time',
yAxisLabel: 'Value',
displayType: 'number',
},
};
export const NaturalVsMonotone: Story = {
render: renderChart,
args: {
id: 'line-chart-smooth',
data: [
{
id: 'natural',
label: 'Natural Curve',
color: 'pink',
data: [
{ x: 0, y: 40 },
{ x: 1, y: 70 },
{ x: 2, y: 50 },
{ x: 3, y: 80 },
{ x: 4, y: 45 },
{ x: 5, y: 75 },
{ x: 6, y: 60 },
],
},
{
id: 'monotone',
label: 'Monotone X',
color: 'orange',
data: [
{ x: 0, y: 35 },
{ x: 1, y: 65 },
{ x: 2, y: 45 },
{ x: 3, y: 75 },
{ x: 4, y: 40 },
{ x: 5, y: 70 },
{ x: 6, y: 55 },
],
},
],
enableArea: true,
showLegend: true,
showGrid: true,
enablePoints: true,
curve: 'natural',
xAxisLabel: 'Sample',
yAxisLabel: 'Measurement',
displayType: 'number',
},
};
export const SliceTooltipDemo: Story = {
render: renderChart,
args: {
id: 'line-chart-slice-tooltip',
data: [
{
id: 'revenue',
label: 'Revenue',
color: 'blue',
data: [
{ x: 'Jan', y: 4500 },
{ x: 'Feb', y: 5200 },
{ x: 'Mar', y: 4800 },
{ x: 'Apr', y: 6100 },
{ x: 'May', y: 5500 },
{ x: 'Jun', y: 7200 },
],
},
{
id: 'costs',
label: 'Costs',
color: 'red',
data: [
{ x: 'Jan', y: 3200 },
{ x: 'Feb', y: 3500 },
{ x: 'Mar', y: 3100 },
{ x: 'Apr', y: 3800 },
{ x: 'May', y: 3600 },
{ x: 'Jun', y: 4200 },
],
},
{
id: 'profit',
label: 'Profit',
color: 'turquoise',
data: [
{ x: 'Jan', y: 1300 },
{ x: 'Feb', y: 1700 },
{ x: 'Mar', y: 1700 },
{ x: 'Apr', y: 2300 },
{ x: 'May', y: 1900 },
{ x: 'Jun', y: 3000 },
],
},
],
showLegend: true,
showGrid: true,
enablePoints: false,
enableSlices: 'x',
xScale: { type: 'point' },
xAxisLabel: 'Month',
yAxisLabel: 'Amount ($)',
displayType: 'shortNumber',
prefix: '$',
},
};
export const PointTooltipDemo: Story = {
render: renderChart,
args: {
id: 'line-chart-point-tooltip',
data: [
{
id: 'revenue',
label: 'Revenue',
color: 'blue',
data: [
{ x: 'Jan', y: 4500 },
{ x: 'Feb', y: 5200 },
{ x: 'Mar', y: 4800 },
{ x: 'Apr', y: 6100 },
{ x: 'May', y: 5500 },
{ x: 'Jun', y: 7200 },
],
},
{
id: 'costs',
label: 'Costs',
color: 'red',
data: [
{ x: 'Jan', y: 3200 },
{ x: 'Feb', y: 3500 },
{ x: 'Mar', y: 3100 },
{ x: 'Apr', y: 3800 },
{ x: 'May', y: 3600 },
{ x: 'Jun', y: 4200 },
],
},
{
id: 'profit',
label: 'Profit',
color: 'turquoise',
data: [
{ x: 'Jan', y: 1300 },
{ x: 'Feb', y: 1700 },
{ x: 'Mar', y: 1700 },
{ x: 'Apr', y: 2300 },
{ x: 'May', y: 1900 },
{ x: 'Jun', y: 3000 },
],
},
],
showLegend: true,
showGrid: true,
enablePoints: true,
enableSlices: false,
xScale: { type: 'point' },
xAxisLabel: 'Month',
yAxisLabel: 'Amount ($)',
displayType: 'shortNumber',
prefix: '$',
},
};
export const IntenseOverlapRGB: Story = {
render: renderChart,
args: {
id: 'line-chart-rgb',
data: [
{
id: 'red',
label: 'Red Channel',
color: 'red',
data: [
{ x: 0, y: 70 },
{ x: 1, y: 85 },
{ x: 2, y: 75 },
{ x: 3, y: 90 },
{ x: 4, y: 80 },
{ x: 5, y: 85 },
],
},
{
id: 'green',
label: 'Green Channel',
color: 'turquoise',
data: [
{ x: 0, y: 65 },
{ x: 1, y: 75 },
{ x: 2, y: 85 },
{ x: 3, y: 80 },
{ x: 4, y: 75 },
{ x: 5, y: 70 },
],
},
{
id: 'blue',
label: 'Blue Channel',
color: 'blue',
data: [
{ x: 0, y: 60 },
{ x: 1, y: 70 },
{ x: 2, y: 80 },
{ x: 3, y: 75 },
{ x: 4, y: 85 },
{ x: 5, y: 80 },
],
},
],
enableArea: true,
showLegend: true,
showGrid: true,
enablePoints: false,
curve: 'monotoneX',
xAxisLabel: 'Position',
yAxisLabel: 'Intensity',
displayType: 'percentage',
},
};
export const Catalog: Story = {
render: renderChart,
args: {
id: 'line-chart-catalog',
data: [
{
id: 'series1',
label: 'Series 1',
color: 'blue',
data: generateLinearData(8),
},
{
id: 'series2',
label: 'Series 2',
color: 'purple',
data: generateLinearData(8),
},
],
showLegend: true,
showGrid: true,
enablePoints: false,
enableArea: true,
},
decorators: [CatalogDecorator],
parameters: {
pseudo: { hover: ['.content'] },
catalog: {
dimensions: [
{
name: 'colors',
values: [
'blue',
'purple',
'turquoise',
'orange',
'pink',
'red',
'yellow',
'green',
'sky',
],
props: (color: string) => ({
data: [
{
id: 'series',
label: `${color} Series`,
color,
data: generateLinearData(8),
},
],
}),
},
],
},
},
};
@@ -1 +1,10 @@
export type GraphColor = 'blue' | 'purple' | 'turquoise' | 'orange' | 'pink';
export type GraphColor =
| 'blue'
| 'purple'
| 'turquoise'
| 'orange'
| 'pink'
| 'yellow'
| 'red'
| 'green'
| 'sky';
@@ -48,4 +48,36 @@ export const createGraphColorRegistry = (
},
solid: theme.color.pink,
},
yellow: {
name: 'yellow',
gradient: {
normal: [theme.adaptiveColors.yellow1, theme.adaptiveColors.yellow2],
hover: [theme.adaptiveColors.yellow3, theme.adaptiveColors.yellow4],
},
solid: theme.color.yellow,
},
red: {
name: 'red',
gradient: {
normal: [theme.adaptiveColors.red1, theme.adaptiveColors.red2],
hover: [theme.adaptiveColors.red3, theme.adaptiveColors.red4],
},
solid: theme.color.red,
},
green: {
name: 'green',
gradient: {
normal: [theme.adaptiveColors.green1, theme.adaptiveColors.green2],
hover: [theme.adaptiveColors.green3, theme.adaptiveColors.green4],
},
solid: theme.color.green,
},
sky: {
name: 'sky',
gradient: {
normal: [theme.adaptiveColors.sky1, theme.adaptiveColors.sky2],
hover: [theme.adaptiveColors.sky3, theme.adaptiveColors.sky4],
},
solid: theme.color.sky,
},
});
@@ -1,5 +1,6 @@
import { GraphWidgetBarChart } from '@/dashboards/widgets/graph/components/GraphWidgetBarChart';
import { GraphWidgetGaugeChart } from '@/dashboards/widgets/graph/components/GraphWidgetGaugeChart';
import { GraphWidgetLineChart } from '@/dashboards/widgets/graph/components/GraphWidgetLineChart';
import { GraphWidgetNumberChart } from '@/dashboards/widgets/graph/components/GraphWidgetNumberChart';
import { GraphWidgetPieChart } from '@/dashboards/widgets/graph/components/GraphWidgetPieChart';
import { GraphType } from '../mocks/mockWidgets';
@@ -69,6 +70,28 @@ export const GraphWidgetRenderer = ({ widget }: GraphWidgetRendererProps) => {
/>
);
case GraphType.LINE:
return (
<GraphWidgetLineChart
id={`line-chart-${widget.id}`}
data={widget.data.series}
enableArea={widget.data.enableArea}
showLegend={widget.data.showLegend}
showGrid={widget.data.showGrid}
enablePoints={widget.data.enablePoints}
xAxisLabel={widget.data.xAxisLabel}
yAxisLabel={widget.data.yAxisLabel}
displayType={widget.data.displayType}
prefix={widget.data.prefix}
suffix={widget.data.suffix}
xScale={widget.data.xScale}
yScale={widget.data.yScale}
curve={widget.data.curve}
stackedArea={widget.data.stackedArea}
enableSlices={widget.data.enableSlices}
/>
);
default:
return null;
}
@@ -13,6 +13,7 @@ export enum GraphType {
GAUGE = 'GAUGE',
PIE = 'PIE',
BAR = 'BAR',
LINE = 'LINE',
}
export const mockPageLayoutWidgets: PageLayoutWidget[] = [
@@ -41,6 +41,67 @@ export const getDefaultWidgetData = (graphType: GraphType) => {
layout: 'vertical' as const,
};
case GraphType.LINE:
return {
series: [
{
id: 'revenue',
label: 'Revenue',
color: 'blue',
data: [
{ x: 0, y: 30 },
{ x: 1, y: 50 },
{ x: 2, y: 45 },
{ x: 3, y: 70 },
{ x: 4, y: 65 },
{ x: 5, y: 80 },
{ x: 6, y: 75 },
{ x: 7, y: 85 },
],
enableArea: true,
},
{
id: 'costs',
label: 'Costs',
color: 'red',
data: [
{ x: 0, y: 60 },
{ x: 1, y: 45 },
{ x: 2, y: 55 },
{ x: 3, y: 40 },
{ x: 4, y: 60 },
{ x: 5, y: 50 },
{ x: 6, y: 70 },
{ x: 7, y: 65 },
],
enableArea: true,
},
{
id: 'profit',
label: 'Profit',
color: 'turquoise',
data: [
{ x: 0, y: 45 },
{ x: 1, y: 60 },
{ x: 2, y: 35 },
{ x: 3, y: 55 },
{ x: 4, y: 50 },
{ x: 5, y: 65 },
{ x: 6, y: 40 },
{ x: 7, y: 75 },
],
enableArea: true,
},
],
enableArea: true,
showLegend: true,
showGrid: true,
enablePoints: false,
curve: 'monotoneX',
displayType: 'shortNumber',
prefix: '$',
};
default:
return {};
}
@@ -52,6 +113,7 @@ export const getWidgetTitle = (graphType: GraphType, index: number): string => {
[GraphType.GAUGE]: 'Gauge',
[GraphType.PIE]: 'Pie Chart',
[GraphType.BAR]: 'Bar Chart',
[GraphType.LINE]: 'Line Chart',
};
return `${baseNames[graphType] || 'Widget'} ${index + 1}`;
@@ -67,6 +129,8 @@ export const getWidgetSize = (graphType: GraphType) => {
return { w: 4, h: 4 };
case GraphType.BAR:
return { w: 6, h: 4 };
case GraphType.LINE:
return { w: 6, h: 10 };
default:
return { w: 4, h: 4 };
}
@@ -52,7 +52,7 @@ import { Button } from 'twenty-ui/input';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
const StyledGridContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};
background: ${({ theme }) => theme.background.primary};
box-sizing: border-box;
flex: 1;
min-height: 100%;
@@ -47,6 +47,7 @@ export {
IconChartBar,
IconChartCandle,
IconChartDots3,
IconChartLine,
IconChartPie,
IconCheck,
IconCheckbox,
+1
View File
@@ -109,6 +109,7 @@ export {
IconChartBar,
IconChartCandle,
IconChartDots3,
IconChartLine,
IconChartPie,
IconCheck,
IconCheckbox,