[BREAKING CHANGE] fix chart cache collisions with key-based data plumbing (#21743)
closes https://discord.com/channels/1130383047699738754/1514946035317997709 This fixes Apollo cache collisions for pie slices and line series by keeping chart bucket identity as key end-to-end, matching how bar chart already works. What changed -- - Renamed pie/line chart response identity from id to key in the chart data path. - Kept key through frontend chart hooks, types, stories, and tooltip/drilldown logic. - Only adapt key to id at actual external boundaries like Nivo and GraphWidgetLegend. - Added/updated tests covering cache normalization and chart data behavior. before - <img width="2600" height="844" alt="CleanShot 2026-06-17 at 20 17 14@2x" src="https://github.com/user-attachments/assets/b9ee83e9-db4b-423e-8668-a7beb4c4c62e" /> after - <img width="2614" height="800" alt="CleanShot 2026-06-17 at 20 16 17@2x" src="https://github.com/user-attachments/assets/674a5417-ffc2-441d-9484-e1126438254c" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21743?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
@@ -2716,7 +2716,7 @@ type LineChartDataPoint {
|
||||
}
|
||||
|
||||
type LineChartSeries {
|
||||
id: String!
|
||||
key: String!
|
||||
label: String!
|
||||
data: [LineChartDataPoint!]!
|
||||
}
|
||||
@@ -2732,7 +2732,7 @@ type LineChartData {
|
||||
}
|
||||
|
||||
type PieChartDataItem {
|
||||
id: String!
|
||||
key: String!
|
||||
value: Float!
|
||||
}
|
||||
|
||||
|
||||
@@ -2372,7 +2372,7 @@ export interface LineChartDataPoint {
|
||||
}
|
||||
|
||||
export interface LineChartSeries {
|
||||
id: Scalars['String']
|
||||
key: Scalars['String']
|
||||
label: Scalars['String']
|
||||
data: LineChartDataPoint[]
|
||||
__typename: 'LineChartSeries'
|
||||
@@ -2390,7 +2390,7 @@ export interface LineChartData {
|
||||
}
|
||||
|
||||
export interface PieChartDataItem {
|
||||
id: Scalars['String']
|
||||
key: Scalars['String']
|
||||
value: Scalars['Float']
|
||||
__typename: 'PieChartDataItem'
|
||||
}
|
||||
@@ -5484,7 +5484,7 @@ export interface LineChartDataPointGenqlSelection{
|
||||
}
|
||||
|
||||
export interface LineChartSeriesGenqlSelection{
|
||||
id?: boolean | number
|
||||
key?: boolean | number
|
||||
label?: boolean | number
|
||||
data?: LineChartDataPointGenqlSelection
|
||||
__typename?: boolean | number
|
||||
@@ -5504,7 +5504,7 @@ export interface LineChartDataGenqlSelection{
|
||||
}
|
||||
|
||||
export interface PieChartDataItemGenqlSelection{
|
||||
id?: boolean | number
|
||||
key?: boolean | number
|
||||
value?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
|
||||
@@ -5366,7 +5366,7 @@ export default {
|
||||
]
|
||||
},
|
||||
"LineChartSeries": {
|
||||
"id": [
|
||||
"key": [
|
||||
1
|
||||
],
|
||||
"label": [
|
||||
@@ -5406,7 +5406,7 @@ export default {
|
||||
]
|
||||
},
|
||||
"PieChartDataItem": {
|
||||
"id": [
|
||||
"key": [
|
||||
1
|
||||
],
|
||||
"value": [
|
||||
|
||||
File diff suppressed because one or more lines are too long
+36
-36
@@ -121,13 +121,13 @@ export const Default: Story = {
|
||||
id: 'line-chart-default',
|
||||
data: [
|
||||
{
|
||||
id: 'series1',
|
||||
key: 'series1',
|
||||
label: 'Revenue',
|
||||
color: 'blue',
|
||||
data: generateLinearData(12),
|
||||
},
|
||||
{
|
||||
id: 'series2',
|
||||
key: 'series2',
|
||||
label: 'Profit',
|
||||
color: 'turquoise',
|
||||
data: generateLinearData(12),
|
||||
@@ -149,13 +149,13 @@ export const WithArea: Story = {
|
||||
id: 'line-chart-area',
|
||||
data: [
|
||||
{
|
||||
id: 'sales',
|
||||
key: 'sales',
|
||||
label: 'Sales',
|
||||
color: 'purple',
|
||||
data: generateLinearData(12),
|
||||
},
|
||||
{
|
||||
id: 'costs',
|
||||
key: 'costs',
|
||||
label: 'Costs',
|
||||
color: 'orange',
|
||||
data: generateLinearData(12),
|
||||
@@ -175,19 +175,19 @@ export const StackedArea: Story = {
|
||||
id: 'line-chart-stacked',
|
||||
data: [
|
||||
{
|
||||
id: 'product-a',
|
||||
key: 'product-a',
|
||||
label: 'Product A',
|
||||
color: 'blue',
|
||||
data: generateLinearData(8),
|
||||
},
|
||||
{
|
||||
id: 'product-b',
|
||||
key: 'product-b',
|
||||
label: 'Product B',
|
||||
color: 'turquoise',
|
||||
data: generateLinearData(8),
|
||||
},
|
||||
{
|
||||
id: 'product-c',
|
||||
key: 'product-c',
|
||||
label: 'Product C',
|
||||
color: 'purple',
|
||||
data: generateLinearData(8),
|
||||
@@ -209,7 +209,7 @@ export const StackedWithNegativeValues: Story = {
|
||||
id: 'line-chart-stacked-negative',
|
||||
data: [
|
||||
{
|
||||
id: 'series-positive',
|
||||
key: 'series-positive',
|
||||
label: 'Positive Series',
|
||||
color: 'blue',
|
||||
data: [
|
||||
@@ -219,7 +219,7 @@ export const StackedWithNegativeValues: Story = {
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'series-negative',
|
||||
key: 'series-negative',
|
||||
label: 'Negative Series',
|
||||
color: 'red',
|
||||
data: [
|
||||
@@ -245,7 +245,7 @@ export const StepChart: Story = {
|
||||
id: 'line-chart-step',
|
||||
data: [
|
||||
{
|
||||
id: 'inventory',
|
||||
key: 'inventory',
|
||||
label: 'Inventory Level',
|
||||
color: 'orange',
|
||||
data: generateLinearData(10),
|
||||
@@ -265,7 +265,7 @@ export const LogScaleDemo: Story = {
|
||||
id: 'line-chart-log-scale',
|
||||
data: [
|
||||
{
|
||||
id: 'exponential',
|
||||
key: 'exponential',
|
||||
label: 'Exponential Growth',
|
||||
color: 'purple',
|
||||
data: [
|
||||
@@ -278,7 +278,7 @@ export const LogScaleDemo: Story = {
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'linear',
|
||||
key: 'linear',
|
||||
label: 'Linear Growth',
|
||||
color: 'turquoise',
|
||||
data: [
|
||||
@@ -305,7 +305,7 @@ export const WithNullValues: Story = {
|
||||
id: 'line-chart-nulls',
|
||||
data: [
|
||||
{
|
||||
id: 'incomplete',
|
||||
key: 'incomplete',
|
||||
label: 'With Gaps',
|
||||
color: 'blue',
|
||||
data: [
|
||||
@@ -333,19 +333,19 @@ export const MultiSeriesMixed: Story = {
|
||||
id: 'line-chart-mixed',
|
||||
data: [
|
||||
{
|
||||
id: 'actual',
|
||||
key: 'actual',
|
||||
label: 'Actual',
|
||||
color: 'blue',
|
||||
data: generateLinearData(12),
|
||||
},
|
||||
{
|
||||
id: 'forecast',
|
||||
key: 'forecast',
|
||||
label: 'Forecast',
|
||||
color: 'purple',
|
||||
data: generateLinearData(12),
|
||||
},
|
||||
{
|
||||
id: 'target',
|
||||
key: 'target',
|
||||
label: 'Target',
|
||||
color: 'orange',
|
||||
data: generateLinearData(12).map((dataPoint) => ({
|
||||
@@ -368,7 +368,7 @@ export const OverlappingGradientBlend: Story = {
|
||||
id: 'line-chart-blend',
|
||||
data: [
|
||||
{
|
||||
id: 'red-series',
|
||||
key: 'red-series',
|
||||
label: 'Red Wave',
|
||||
color: 'red',
|
||||
data: [
|
||||
@@ -383,7 +383,7 @@ export const OverlappingGradientBlend: Story = {
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'blue-series',
|
||||
key: 'blue-series',
|
||||
label: 'Blue Wave',
|
||||
color: 'blue',
|
||||
data: [
|
||||
@@ -398,7 +398,7 @@ export const OverlappingGradientBlend: Story = {
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'green-series',
|
||||
key: 'green-series',
|
||||
label: 'Green Wave',
|
||||
color: 'turquoise',
|
||||
data: [
|
||||
@@ -427,7 +427,7 @@ export const HighContrastOverlap: Story = {
|
||||
id: 'line-chart-contrast',
|
||||
data: [
|
||||
{
|
||||
id: 'yellow-series',
|
||||
key: 'yellow-series',
|
||||
label: 'Yellow',
|
||||
color: 'yellow',
|
||||
data: [
|
||||
@@ -441,7 +441,7 @@ export const HighContrastOverlap: Story = {
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'purple-series',
|
||||
key: 'purple-series',
|
||||
label: 'Purple',
|
||||
color: 'purple',
|
||||
data: [
|
||||
@@ -469,7 +469,7 @@ export const CurveComparison: Story = {
|
||||
id: 'line-chart-curves',
|
||||
data: [
|
||||
{
|
||||
id: 'dataset',
|
||||
key: 'dataset',
|
||||
label: 'Same Data',
|
||||
color: 'orange',
|
||||
data: [
|
||||
@@ -497,7 +497,7 @@ export const StepInterpolations: Story = {
|
||||
id: 'line-chart-steps',
|
||||
data: [
|
||||
{
|
||||
id: 'step-normal',
|
||||
key: 'step-normal',
|
||||
label: 'Step',
|
||||
color: 'blue',
|
||||
data: [
|
||||
@@ -510,7 +510,7 @@ export const StepInterpolations: Story = {
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'step-before',
|
||||
key: 'step-before',
|
||||
label: 'Step Before',
|
||||
color: 'purple',
|
||||
data: [
|
||||
@@ -523,7 +523,7 @@ export const StepInterpolations: Story = {
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'step-after',
|
||||
key: 'step-after',
|
||||
label: 'Step After',
|
||||
color: 'turquoise',
|
||||
data: [
|
||||
@@ -550,7 +550,7 @@ export const NaturalVsMonotone: Story = {
|
||||
id: 'line-chart-smooth',
|
||||
data: [
|
||||
{
|
||||
id: 'natural',
|
||||
key: 'natural',
|
||||
label: 'Natural Curve',
|
||||
color: 'pink',
|
||||
data: [
|
||||
@@ -564,7 +564,7 @@ export const NaturalVsMonotone: Story = {
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'monotone',
|
||||
key: 'monotone',
|
||||
label: 'Monotone X',
|
||||
color: 'orange',
|
||||
data: [
|
||||
@@ -592,7 +592,7 @@ export const SliceTooltipDemo: Story = {
|
||||
id: 'line-chart-slice-tooltip',
|
||||
data: [
|
||||
{
|
||||
id: 'revenue',
|
||||
key: 'revenue',
|
||||
label: 'Revenue',
|
||||
color: 'blue',
|
||||
data: [
|
||||
@@ -605,7 +605,7 @@ export const SliceTooltipDemo: Story = {
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'costs',
|
||||
key: 'costs',
|
||||
label: 'Costs',
|
||||
color: 'red',
|
||||
data: [
|
||||
@@ -618,7 +618,7 @@ export const SliceTooltipDemo: Story = {
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'profit',
|
||||
key: 'profit',
|
||||
label: 'Profit',
|
||||
color: 'turquoise',
|
||||
data: [
|
||||
@@ -646,7 +646,7 @@ export const PointTooltipDemo: Story = {
|
||||
id: 'line-chart-point-tooltip',
|
||||
data: [
|
||||
{
|
||||
id: 'revenue',
|
||||
key: 'revenue',
|
||||
label: 'Revenue',
|
||||
color: 'blue',
|
||||
data: [
|
||||
@@ -659,7 +659,7 @@ export const PointTooltipDemo: Story = {
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'costs',
|
||||
key: 'costs',
|
||||
label: 'Costs',
|
||||
color: 'red',
|
||||
data: [
|
||||
@@ -672,7 +672,7 @@ export const PointTooltipDemo: Story = {
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'profit',
|
||||
key: 'profit',
|
||||
label: 'Profit',
|
||||
color: 'turquoise',
|
||||
data: [
|
||||
@@ -700,13 +700,13 @@ export const Catalog: Story = {
|
||||
id: 'line-chart-catalog',
|
||||
data: [
|
||||
{
|
||||
id: 'series1',
|
||||
key: 'series1',
|
||||
label: 'Series 1',
|
||||
color: 'blue',
|
||||
data: generateLinearData(8),
|
||||
},
|
||||
{
|
||||
id: 'series2',
|
||||
key: 'series2',
|
||||
label: 'Series 2',
|
||||
color: 'purple',
|
||||
data: generateLinearData(8),
|
||||
@@ -736,7 +736,7 @@ export const Catalog: Story = {
|
||||
props: (color: string) => ({
|
||||
data: [
|
||||
{
|
||||
id: 'series',
|
||||
key: 'series',
|
||||
label: `${color} Series`,
|
||||
color,
|
||||
data: generateLinearData(8),
|
||||
|
||||
+50
-50
@@ -97,11 +97,11 @@ const Container = ({ children }: { children: React.ReactNode }) => (
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
data: [
|
||||
{ id: 'Qualified', value: 35 },
|
||||
{ id: 'Contacted', value: 25 },
|
||||
{ id: 'Unqualified', value: 20 },
|
||||
{ id: 'Proposal', value: 15 },
|
||||
{ id: 'Negotiation', value: 5 },
|
||||
{ key: 'Qualified', value: 35 },
|
||||
{ key: 'Contacted', value: 25 },
|
||||
{ key: 'Unqualified', value: 20 },
|
||||
{ key: 'Proposal', value: 15 },
|
||||
{ key: 'Negotiation', value: 5 },
|
||||
],
|
||||
showLegend: true,
|
||||
id: 'pie-chart-default',
|
||||
@@ -127,9 +127,9 @@ export const Default: Story = {
|
||||
export const WithCenterMetric: Story = {
|
||||
args: {
|
||||
data: [
|
||||
{ id: 'Qualified', value: 35 },
|
||||
{ id: 'Contacted', value: 25 },
|
||||
{ id: 'Unqualified', value: 20 },
|
||||
{ key: 'Qualified', value: 35 },
|
||||
{ key: 'Contacted', value: 25 },
|
||||
{ key: 'Unqualified', value: 20 },
|
||||
],
|
||||
showCenterMetric: true,
|
||||
id: 'pie-chart-with-center-metric',
|
||||
@@ -151,9 +151,9 @@ export const WithCenterMetric: Story = {
|
||||
export const WithDataLabels: Story = {
|
||||
args: {
|
||||
data: [
|
||||
{ id: 'Qualified', value: 35 },
|
||||
{ id: 'Contacted', value: 25 },
|
||||
{ id: 'Unqualified', value: 20 },
|
||||
{ key: 'Qualified', value: 35 },
|
||||
{ key: 'Contacted', value: 25 },
|
||||
{ key: 'Unqualified', value: 20 },
|
||||
],
|
||||
showDataLabels: true,
|
||||
id: 'pie-chart-with-data-labels',
|
||||
@@ -174,10 +174,10 @@ export const WithDataLabels: Story = {
|
||||
export const Revenue: Story = {
|
||||
args: {
|
||||
data: [
|
||||
{ id: 'Product A', value: 420000 },
|
||||
{ id: 'Product B', value: 380000 },
|
||||
{ id: 'Product C', value: 250000 },
|
||||
{ id: 'Product D', value: 180000 },
|
||||
{ key: 'Product A', value: 420000 },
|
||||
{ key: 'Product B', value: 380000 },
|
||||
{ key: 'Product C', value: 250000 },
|
||||
{ key: 'Product D', value: 180000 },
|
||||
],
|
||||
prefix: '$',
|
||||
displayType: 'shortNumber',
|
||||
@@ -205,9 +205,9 @@ export const Revenue: Story = {
|
||||
export const TaskStatus: Story = {
|
||||
args: {
|
||||
data: [
|
||||
{ id: 'Completed', value: 45 },
|
||||
{ id: 'In Progress', value: 30 },
|
||||
{ id: 'To Do', value: 25 },
|
||||
{ key: 'Completed', value: 45 },
|
||||
{ key: 'In Progress', value: 30 },
|
||||
{ key: 'To Do', value: 25 },
|
||||
],
|
||||
displayType: 'percentage',
|
||||
showLegend: true,
|
||||
@@ -234,8 +234,8 @@ export const TaskStatus: Story = {
|
||||
export const TwoSlices: Story = {
|
||||
args: {
|
||||
data: [
|
||||
{ id: 'Active', value: 75 },
|
||||
{ id: 'Inactive', value: 25 },
|
||||
{ key: 'Active', value: 75 },
|
||||
{ key: 'Inactive', value: 25 },
|
||||
],
|
||||
displayType: 'percentage',
|
||||
showLegend: true,
|
||||
@@ -262,14 +262,14 @@ export const TwoSlices: Story = {
|
||||
export const ManySlices: Story = {
|
||||
args: {
|
||||
data: [
|
||||
{ id: 'Category 1', value: 20 },
|
||||
{ id: 'Category 2', value: 18 },
|
||||
{ id: 'Category 3', value: 16 },
|
||||
{ id: 'Category 4', value: 14 },
|
||||
{ id: 'Category 5', value: 12 },
|
||||
{ id: 'Category 6', value: 10 },
|
||||
{ id: 'Category 7', value: 6 },
|
||||
{ id: 'Category 8', value: 4 },
|
||||
{ key: 'Category 1', value: 20 },
|
||||
{ key: 'Category 2', value: 18 },
|
||||
{ key: 'Category 3', value: 16 },
|
||||
{ key: 'Category 4', value: 14 },
|
||||
{ key: 'Category 5', value: 12 },
|
||||
{ key: 'Category 6', value: 10 },
|
||||
{ key: 'Category 7', value: 6 },
|
||||
{ key: 'Category 8', value: 4 },
|
||||
],
|
||||
showLegend: true,
|
||||
id: 'pie-chart-many-slices',
|
||||
@@ -295,9 +295,9 @@ export const ManySlices: Story = {
|
||||
export const WithoutLegend: Story = {
|
||||
args: {
|
||||
data: [
|
||||
{ id: 'Web', value: 45 },
|
||||
{ id: 'Mobile', value: 35 },
|
||||
{ id: 'Desktop', value: 20 },
|
||||
{ key: 'Web', value: 45 },
|
||||
{ key: 'Mobile', value: 35 },
|
||||
{ key: 'Desktop', value: 20 },
|
||||
],
|
||||
displayType: 'percentage',
|
||||
showLegend: false,
|
||||
@@ -324,10 +324,10 @@ export const WithoutLegend: Story = {
|
||||
export const MarketShare: Story = {
|
||||
args: {
|
||||
data: [
|
||||
{ id: 'Brand A', value: 35.5 },
|
||||
{ id: 'Brand B', value: 28.2 },
|
||||
{ id: 'Brand C', value: 18.7 },
|
||||
{ id: 'Others', value: 17.6 },
|
||||
{ key: 'Brand A', value: 35.5 },
|
||||
{ key: 'Brand B', value: 28.2 },
|
||||
{ key: 'Brand C', value: 18.7 },
|
||||
{ key: 'Others', value: 17.6 },
|
||||
],
|
||||
displayType: 'percentage',
|
||||
showLegend: true,
|
||||
@@ -354,10 +354,10 @@ export const MarketShare: Story = {
|
||||
export const Storage: Story = {
|
||||
args: {
|
||||
data: [
|
||||
{ id: 'Documents', value: 125 },
|
||||
{ id: 'Media', value: 280 },
|
||||
{ id: 'Applications', value: 95 },
|
||||
{ id: 'System', value: 50 },
|
||||
{ key: 'Documents', value: 125 },
|
||||
{ key: 'Media', value: 280 },
|
||||
{ key: 'Applications', value: 95 },
|
||||
{ key: 'System', value: 50 },
|
||||
],
|
||||
suffix: ' GB',
|
||||
showLegend: true,
|
||||
@@ -400,23 +400,23 @@ export const Catalog: Story = {
|
||||
props: (sliceCount: number) => {
|
||||
const dataMap: Record<
|
||||
number,
|
||||
Array<{ id: string; value: number }>
|
||||
Array<{ key: string; value: number }>
|
||||
> = {
|
||||
2: [
|
||||
{ id: 'Yes', value: 65 },
|
||||
{ id: 'No', value: 35 },
|
||||
{ key: 'Yes', value: 65 },
|
||||
{ key: 'No', value: 35 },
|
||||
],
|
||||
3: [
|
||||
{ id: 'Gold', value: 45 },
|
||||
{ id: 'Silver', value: 35 },
|
||||
{ id: 'Bronze', value: 20 },
|
||||
{ key: 'Gold', value: 45 },
|
||||
{ key: 'Silver', value: 35 },
|
||||
{ key: 'Bronze', value: 20 },
|
||||
],
|
||||
5: [
|
||||
{ id: 'Item 1', value: 30 },
|
||||
{ id: 'Item 2', value: 25 },
|
||||
{ id: 'Item 3', value: 20 },
|
||||
{ id: 'Item 4', value: 15 },
|
||||
{ id: 'Item 5', value: 10 },
|
||||
{ key: 'Item 1', value: 30 },
|
||||
{ key: 'Item 2', value: 25 },
|
||||
{ key: 'Item 3', value: 20 },
|
||||
{ key: 'Item 4', value: 15 },
|
||||
{ key: 'Item 5', value: 10 },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
+4
-1
@@ -37,7 +37,10 @@ export const CustomStackedAreasLayer = ({
|
||||
const seriesById = useMemo(
|
||||
() =>
|
||||
new Map(
|
||||
enrichedSeries.map((seriesItem) => [String(seriesItem.id), seriesItem]),
|
||||
enrichedSeries.map((seriesItem) => [
|
||||
String(seriesItem.key),
|
||||
seriesItem,
|
||||
]),
|
||||
),
|
||||
[enrichedSeries],
|
||||
);
|
||||
|
||||
+6
-7
@@ -3,7 +3,6 @@ import { type GraphColorRegistry } from '@/page-layout/widgets/graph/types/Graph
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { useLineChartData } from '@/page-layout/widgets/graph/graph-widget-line-chart/hooks/useLineChartData';
|
||||
import { type LineChartSeries } from '~/generated-metadata/graphql';
|
||||
|
||||
const mockUseAtomComponentStateValue = jest.fn();
|
||||
jest.mock(
|
||||
@@ -60,7 +59,7 @@ describe('useLineChartData', () => {
|
||||
|
||||
const mockData: LineChartSeriesWithColor[] = [
|
||||
{
|
||||
id: 'series1',
|
||||
key: 'series1',
|
||||
data: [
|
||||
{ x: 'Jan', y: 100 },
|
||||
{ x: 'Feb', y: 120 },
|
||||
@@ -70,7 +69,7 @@ describe('useLineChartData', () => {
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
id: 'series2',
|
||||
key: 'series2',
|
||||
data: [
|
||||
{ x: 'Jan', y: 80 },
|
||||
{ x: 'Feb', y: 90 },
|
||||
@@ -171,10 +170,10 @@ describe('useLineChartData', () => {
|
||||
expect(result.current.legendItems).toEqual([]);
|
||||
});
|
||||
|
||||
it('should use series id as label when label is not provided', () => {
|
||||
const dataWithoutLabel: LineChartSeries[] = [
|
||||
it('should use series key as label when label is not provided', () => {
|
||||
const dataWithoutLabel: LineChartSeriesWithColor[] = [
|
||||
{
|
||||
id: 'series1',
|
||||
key: 'series1',
|
||||
data: [{ x: 'Jan', y: 100 }],
|
||||
label: 'series1',
|
||||
},
|
||||
@@ -205,7 +204,7 @@ describe('useLineChartData', () => {
|
||||
);
|
||||
|
||||
expect(result.current.visibleData).toHaveLength(1);
|
||||
expect(result.current.visibleData[0].id).toBe('series1');
|
||||
expect(result.current.visibleData[0].key).toBe('series1');
|
||||
expect(result.current.enrichedSeries).toHaveLength(1);
|
||||
expect(result.current.nivoData).toHaveLength(1);
|
||||
});
|
||||
|
||||
+4
-7
@@ -15,6 +15,7 @@ import {
|
||||
LineChartDataDocument,
|
||||
type LineChartConfiguration,
|
||||
type LineChartDataPoint,
|
||||
type LineChartSeries,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type UseGraphLineChartWidgetDataProps = {
|
||||
@@ -83,12 +84,8 @@ export const useGraphLineChartWidgetData = ({
|
||||
});
|
||||
|
||||
const series = queryData?.lineChartData?.series?.map(
|
||||
(seriesItem: {
|
||||
id: string;
|
||||
label: string;
|
||||
data: Array<LineChartDataPoint>;
|
||||
}): LineChartSeriesWithColor => {
|
||||
const rawValue = formattedToRawLookup.get(seriesItem.id);
|
||||
(seriesItem: LineChartSeries): LineChartSeriesWithColor => {
|
||||
const rawValue = formattedToRawLookup.get(seriesItem.key);
|
||||
|
||||
const itemColor = determineChartItemColor({
|
||||
configurationColor,
|
||||
@@ -97,7 +94,7 @@ export const useGraphLineChartWidgetData = ({
|
||||
});
|
||||
|
||||
return {
|
||||
id: seriesItem.id,
|
||||
key: seriesItem.key,
|
||||
label: seriesItem.label,
|
||||
color: itemColor,
|
||||
data: seriesItem.data.map(
|
||||
|
||||
+6
-6
@@ -37,10 +37,10 @@ export const useLineChartData = ({
|
||||
totalGroups: shouldApplyGradient ? data.length : undefined,
|
||||
});
|
||||
|
||||
const sanitizedSeriesId = series.id
|
||||
const sanitizedSeriesKey = series.key
|
||||
.replace(/\s+/g, '_')
|
||||
.replace(/[^a-zA-Z0-9_-]/g, '');
|
||||
const areaFillId = `areaFill-${id}-${sanitizedSeriesId}-${index}`;
|
||||
const areaFillId = `areaFill-${id}-${sanitizedSeriesKey}-${index}`;
|
||||
|
||||
return { ...series, colorScheme, areaFillId };
|
||||
});
|
||||
@@ -48,22 +48,22 @@ export const useLineChartData = ({
|
||||
|
||||
const legendItems: GraphWidgetLegendItem[] = allEnrichedSeries.map(
|
||||
(series) => ({
|
||||
id: series.id,
|
||||
id: series.key,
|
||||
label: series.label,
|
||||
color: series.colorScheme.solid,
|
||||
}),
|
||||
);
|
||||
|
||||
const visibleData = data.filter(
|
||||
(series) => !graphWidgetHiddenLegendIds.includes(series.id),
|
||||
(series) => !graphWidgetHiddenLegendIds.includes(series.key),
|
||||
);
|
||||
|
||||
const enrichedSeries = allEnrichedSeries.filter(
|
||||
(series) => !graphWidgetHiddenLegendIds.includes(series.id),
|
||||
(series) => !graphWidgetHiddenLegendIds.includes(series.key),
|
||||
);
|
||||
|
||||
const nivoData: LineSeries[] = visibleData.map((series) => ({
|
||||
id: series.id,
|
||||
id: series.key,
|
||||
data: series.data.map((point) => ({ x: point.x, y: point.y })),
|
||||
}));
|
||||
|
||||
|
||||
+2
-3
@@ -1,9 +1,8 @@
|
||||
import { type LineChartDataPoint } from '@/page-layout/widgets/graph/graph-widget-line-chart/types/LineChartDataPoint';
|
||||
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
|
||||
import { type LineChartSeries } from '~/generated-metadata/graphql';
|
||||
|
||||
export type LineChartSeriesWithColor = {
|
||||
id: string;
|
||||
label: string;
|
||||
export type LineChartSeriesWithColor = Omit<LineChartSeries, 'data'> & {
|
||||
color?: GraphColor;
|
||||
data: LineChartDataPoint[];
|
||||
};
|
||||
|
||||
+9
-10
@@ -1,13 +1,12 @@
|
||||
import { type LineChartSeriesWithColor } from '@/page-layout/widgets/graph/graph-widget-line-chart/types/LineChartSeriesWithColor';
|
||||
import { calculateValueRangeFromLineChartSeries } from '@/page-layout/widgets/graph/graph-widget-line-chart/utils/calculateValueRangeFromLineChartSeries';
|
||||
import { type LineChartSeries } from '~/generated-metadata/graphql';
|
||||
|
||||
describe('calculateValueRangeFromLineChartSeries', () => {
|
||||
describe('with valid data', () => {
|
||||
it('should calculate minimum and maximum from single series', () => {
|
||||
const data: LineChartSeriesWithColor[] = [
|
||||
{
|
||||
id: 'series1',
|
||||
key: 'series1',
|
||||
data: [
|
||||
{ x: 'Jan', y: 10 },
|
||||
{ x: 'Feb', y: 50 },
|
||||
@@ -25,20 +24,20 @@ describe('calculateValueRangeFromLineChartSeries', () => {
|
||||
it('should calculate minimum and maximum from multiple series', () => {
|
||||
const data: LineChartSeriesWithColor[] = [
|
||||
{
|
||||
id: 'series1',
|
||||
key: 'series1',
|
||||
data: [
|
||||
{ x: 'Jan', y: 10 },
|
||||
{ x: 'Feb', y: 20 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'series2',
|
||||
key: 'series2',
|
||||
data: [
|
||||
{ x: 'Jan', y: 5 },
|
||||
{ x: 'Feb', y: 100 },
|
||||
],
|
||||
},
|
||||
] as unknown as LineChartSeries[];
|
||||
] as unknown as LineChartSeriesWithColor[];
|
||||
|
||||
const result = calculateValueRangeFromLineChartSeries(data);
|
||||
|
||||
@@ -49,7 +48,7 @@ describe('calculateValueRangeFromLineChartSeries', () => {
|
||||
it('should handle negative values', () => {
|
||||
const data: LineChartSeriesWithColor[] = [
|
||||
{
|
||||
id: 'series1',
|
||||
key: 'series1',
|
||||
data: [
|
||||
{ x: 'Jan', y: -50 },
|
||||
{ x: 'Feb', y: 25 },
|
||||
@@ -67,7 +66,7 @@ describe('calculateValueRangeFromLineChartSeries', () => {
|
||||
it('should handle all same values', () => {
|
||||
const data: LineChartSeriesWithColor[] = [
|
||||
{
|
||||
id: 'series1',
|
||||
key: 'series1',
|
||||
data: [
|
||||
{ x: 'Jan', y: 42 },
|
||||
{ x: 'Feb', y: 42 },
|
||||
@@ -87,7 +86,7 @@ describe('calculateValueRangeFromLineChartSeries', () => {
|
||||
it('should treat null y values as 0', () => {
|
||||
const data: LineChartSeriesWithColor[] = [
|
||||
{
|
||||
id: 'series1',
|
||||
key: 'series1',
|
||||
data: [
|
||||
{ x: 'Jan', y: null },
|
||||
{ x: 'Feb', y: 50 },
|
||||
@@ -104,7 +103,7 @@ describe('calculateValueRangeFromLineChartSeries', () => {
|
||||
it('should treat undefined y values as 0', () => {
|
||||
const data: LineChartSeriesWithColor[] = [
|
||||
{
|
||||
id: 'series1',
|
||||
key: 'series1',
|
||||
data: [
|
||||
{ x: 'Jan', y: undefined },
|
||||
{ x: 'Feb', y: 30 },
|
||||
@@ -132,7 +131,7 @@ describe('calculateValueRangeFromLineChartSeries', () => {
|
||||
it('should handle series with empty data array', () => {
|
||||
const data: LineChartSeriesWithColor[] = [
|
||||
{
|
||||
id: 'series1',
|
||||
key: 'series1',
|
||||
data: [],
|
||||
},
|
||||
] as unknown as LineChartSeriesWithColor[];
|
||||
|
||||
+4
-4
@@ -17,7 +17,7 @@ describe('getLineChartLayout', () => {
|
||||
|
||||
const defaultData: LineChartSeriesWithColor[] = [
|
||||
{
|
||||
id: 'series1',
|
||||
key: 'series1',
|
||||
label: 'Series 1',
|
||||
data: [
|
||||
{ x: '2024-01', y: 10 },
|
||||
@@ -90,7 +90,7 @@ describe('getLineChartLayout', () => {
|
||||
...baseParams,
|
||||
data: [
|
||||
{
|
||||
id: 'series1',
|
||||
key: 'series1',
|
||||
label: 'Series 1',
|
||||
data: [{ x: '2024-01', y: 50 }],
|
||||
},
|
||||
@@ -119,7 +119,7 @@ describe('getLineChartLayout', () => {
|
||||
...baseParams,
|
||||
data: [
|
||||
{
|
||||
id: 'series1',
|
||||
key: 'series1',
|
||||
label: 'Series 1',
|
||||
data: [
|
||||
{ x: '2024-01', y: 10 },
|
||||
@@ -127,7 +127,7 @@ describe('getLineChartLayout', () => {
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'series2',
|
||||
key: 'series2',
|
||||
label: 'Series 2',
|
||||
data: [
|
||||
{ x: '2024-01', y: 15 },
|
||||
|
||||
+4
-4
@@ -43,17 +43,17 @@ const createMockSlice = (
|
||||
points,
|
||||
}) as unknown as SliceTooltipProps<LineSeries>['slice'];
|
||||
|
||||
const createSeries = (id: string): LineChartEnrichedSeries =>
|
||||
const createSeries = (key: string): LineChartEnrichedSeries =>
|
||||
({
|
||||
id,
|
||||
label: id,
|
||||
key,
|
||||
label: key,
|
||||
data: [],
|
||||
colorScheme: {
|
||||
name: 'test',
|
||||
solid: '#solid',
|
||||
variations: Array(12).fill('#v0'),
|
||||
},
|
||||
areaFillId: `area-${id}`,
|
||||
areaFillId: `area-${key}`,
|
||||
}) as unknown as LineChartEnrichedSeries;
|
||||
|
||||
describe('getLineChartTooltipData', () => {
|
||||
|
||||
+3
-3
@@ -26,7 +26,7 @@ export const getLineChartTooltipData = ({
|
||||
isStacked = false,
|
||||
}: GetLineChartTooltipDataParameters): LineChartTooltipData => {
|
||||
const enrichedSeriesMap = new Map(
|
||||
enrichedSeries.map((series) => [series.id, series]),
|
||||
enrichedSeries.map((series) => [series.key, series]),
|
||||
);
|
||||
|
||||
if (!isDefined(slice.points) || slice.points.length === 0) {
|
||||
@@ -37,7 +37,7 @@ export const getLineChartTooltipData = ({
|
||||
}
|
||||
|
||||
const seriesIndexMap = new Map(
|
||||
enrichedSeries.map((series, index) => [series.id, index]),
|
||||
enrichedSeries.map((series, index) => [series.key, index]),
|
||||
);
|
||||
|
||||
const getPointValue = (point: (typeof slice.points)[number]) =>
|
||||
@@ -65,7 +65,7 @@ export const getLineChartTooltipData = ({
|
||||
|
||||
const value = getPointValue(point);
|
||||
return {
|
||||
key: enrichedSeriesItem.id,
|
||||
key: enrichedSeriesItem.key,
|
||||
label: enrichedSeriesItem.label,
|
||||
formattedValue: formatGraphValue(value, formatOptions),
|
||||
value,
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ export const CustomArcsLayer = ({
|
||||
{transition((_, datum) => {
|
||||
const isDimmed =
|
||||
isDefined(graphWidgetHighlightedLegendId) &&
|
||||
String(graphWidgetHighlightedLegendId) !== String(datum.data.id);
|
||||
String(graphWidgetHighlightedLegendId) !== String(datum.data.key);
|
||||
const arcLength = datum.arc.endAngle - datum.arc.startAngle;
|
||||
const padAngleRadians = (padAngle * Math.PI) / 180;
|
||||
const clampedPadAngle = Math.min(
|
||||
|
||||
+8
-7
@@ -29,10 +29,7 @@ import {
|
||||
} from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import {
|
||||
type PieChartConfiguration,
|
||||
type PieChartDataItem,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { type PieChartConfiguration } from '~/generated-metadata/graphql';
|
||||
|
||||
type GraphWidgetPieChartProps = {
|
||||
data: PieChartDataItemWithColor[];
|
||||
@@ -46,7 +43,9 @@ type GraphWidgetPieChartProps = {
|
||||
showCenterMetric?: boolean;
|
||||
} & GraphValueFormatOptions;
|
||||
|
||||
const emptyStateData: PieChartDataItemWithColor[] = [{ id: 'empty', value: 1 }];
|
||||
const emptyStateData: PieChartDataItemWithColor[] = [
|
||||
{ key: 'empty', value: 1 },
|
||||
];
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
@@ -194,8 +193,10 @@ export const GraphWidgetPieChart = ({
|
||||
enableArcLabels={false}
|
||||
tooltip={() => null}
|
||||
layers={[ArcsLayer, 'arcLinkLabels']}
|
||||
id={(datum: PieChartDataItem) => `${id}:${String(datum.id)}`}
|
||||
arcLinkLabel={(datum: ComputedDatum<PieChartDataItem>) => {
|
||||
id={(datum: PieChartDataItemWithColor) =>
|
||||
`${id}:${String(datum.key)}`
|
||||
}
|
||||
arcLinkLabel={(datum: ComputedDatum<PieChartDataItemWithColor>) => {
|
||||
const formattedValue = getPieChartFormattedValue({
|
||||
datum,
|
||||
enrichedData,
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ export const GraphWidgetPieChartRenderer = () => {
|
||||
isFilteredViewRedirectionSupported(groupByField);
|
||||
|
||||
const handleSliceClick = (datum: PieChartDataItemWithColor) => {
|
||||
const rawValue = formattedToRawLookup.get(datum.id) ?? null;
|
||||
const rawValue = formattedToRawLookup.get(datum.key) ?? null;
|
||||
|
||||
const drilldownQueryParams = buildChartDrilldownQueryParams({
|
||||
objectMetadataItem,
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import { InMemoryCache } from '@apollo/client';
|
||||
|
||||
import { LINE_CHART_DATA } from '@/page-layout/widgets/graph/graphql/queries/lineChartData';
|
||||
import { PIE_CHART_DATA } from '@/page-layout/widgets/graph/graphql/queries/pieChartData';
|
||||
|
||||
// Pie slices and line series are returned with `key` (the bucket label) rather than `id`,
|
||||
// so Apollo's default id-based normalization never pools them into a shared cache entry.
|
||||
// Without this, two widgets sharing a slice/series label overwrite each other's value
|
||||
// (one chart "copies" the other). Mirrors the production cache config in useApolloFactory.ts.
|
||||
const buildCache = () =>
|
||||
new InMemoryCache({
|
||||
typePolicies: {
|
||||
RemoteTable: { keyFields: ['name'] },
|
||||
},
|
||||
});
|
||||
|
||||
const variablesForFilter = (filter: string) => ({
|
||||
input: { objectMetadataId: 'opportunity', configuration: { filter } },
|
||||
});
|
||||
|
||||
const writePie = (cache: InMemoryCache, filter: string, value: number) => {
|
||||
cache.writeQuery({
|
||||
query: PIE_CHART_DATA,
|
||||
variables: variablesForFilter(filter),
|
||||
data: {
|
||||
pieChartData: {
|
||||
__typename: 'PieChartData',
|
||||
data: [{ __typename: 'PieChartDataItem', key: 'New', value }],
|
||||
showLegend: true,
|
||||
showDataLabels: false,
|
||||
showCenterMetric: true,
|
||||
hasTooManyGroups: false,
|
||||
formattedToRawLookup: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const readPieNewValue = (cache: InMemoryCache, filter: string) =>
|
||||
cache.readQuery<{ pieChartData: { data: Array<{ value: number }> } }>({
|
||||
query: PIE_CHART_DATA,
|
||||
variables: variablesForFilter(filter),
|
||||
})?.pieChartData.data[0].value;
|
||||
|
||||
const writeLine = (cache: InMemoryCache, filter: string, y: number) => {
|
||||
cache.writeQuery({
|
||||
query: LINE_CHART_DATA,
|
||||
variables: variablesForFilter(filter),
|
||||
data: {
|
||||
lineChartData: {
|
||||
__typename: 'LineChartData',
|
||||
series: [
|
||||
{
|
||||
__typename: 'LineChartSeries',
|
||||
key: 'Won',
|
||||
label: 'Won',
|
||||
data: [{ __typename: 'LineChartDataPoint', x: '2024', y }],
|
||||
},
|
||||
],
|
||||
xAxisLabel: '',
|
||||
yAxisLabel: '',
|
||||
showLegend: true,
|
||||
showDataLabels: false,
|
||||
hasTooManyGroups: false,
|
||||
formattedToRawLookup: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const readLineFirstY = (cache: InMemoryCache, filter: string) =>
|
||||
cache.readQuery<{
|
||||
lineChartData: { series: Array<{ data: Array<{ y: number }> }> };
|
||||
}>({
|
||||
query: LINE_CHART_DATA,
|
||||
variables: variablesForFilter(filter),
|
||||
})?.lineChartData.series[0].data[0].y;
|
||||
|
||||
const hasNormalizedEntry = (cache: InMemoryCache, typename: string) =>
|
||||
Object.keys(cache.extract()).some((cacheKey) =>
|
||||
cacheKey.startsWith(`${typename}:`),
|
||||
);
|
||||
|
||||
describe('chart data cache normalization', () => {
|
||||
it('keeps pie slice values independent when two widgets share a slice label', () => {
|
||||
const cache = buildCache();
|
||||
|
||||
writePie(cache, 'all', 100);
|
||||
writePie(cache, 'lo', 5);
|
||||
|
||||
expect(readPieNewValue(cache, 'all')).toBe(100);
|
||||
expect(hasNormalizedEntry(cache, 'PieChartDataItem')).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps line series values independent when two widgets share a series key', () => {
|
||||
const cache = buildCache();
|
||||
|
||||
writeLine(cache, 'all', 100);
|
||||
writeLine(cache, 'lo', 5);
|
||||
|
||||
expect(readLineFirstY(cache, 'all')).toBe(100);
|
||||
expect(hasNormalizedEntry(cache, 'LineChartSeries')).toBe(false);
|
||||
});
|
||||
});
|
||||
+8
-7
@@ -2,7 +2,6 @@ import { usePieChartData } from '@/page-layout/widgets/graph/graph-widget-pie-ch
|
||||
import { type PieChartDataItemWithColor } from '@/page-layout/widgets/graph/graph-widget-pie-chart/types/PieChartDataItem';
|
||||
import { type GraphColorRegistry } from '@/page-layout/widgets/graph/types/GraphColorRegistry';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { type PieChartDataItem } from '~/generated-metadata/graphql';
|
||||
|
||||
const mockUseAtomComponentStateValue = jest.fn();
|
||||
jest.mock(
|
||||
@@ -58,9 +57,9 @@ describe('usePieChartData', () => {
|
||||
};
|
||||
|
||||
const mockData: PieChartDataItemWithColor[] = [
|
||||
{ id: 'item1', value: 30 },
|
||||
{ id: 'item2', value: 50 },
|
||||
{ id: 'item3', value: 20 },
|
||||
{ key: 'item1', value: 30 },
|
||||
{ key: 'item2', value: 50 },
|
||||
{ key: 'item3', value: 20 },
|
||||
];
|
||||
|
||||
it('should enrich data with color schemes and percentages', () => {
|
||||
@@ -74,7 +73,7 @@ describe('usePieChartData', () => {
|
||||
|
||||
expect(result.current.enrichedData).toHaveLength(3);
|
||||
expect(result.current.enrichedData[0]).toMatchObject({
|
||||
id: 'item1',
|
||||
key: 'item1',
|
||||
value: 30,
|
||||
percentage: 30,
|
||||
colorScheme: mockColorRegistry.red,
|
||||
@@ -96,7 +95,9 @@ describe('usePieChartData', () => {
|
||||
});
|
||||
|
||||
it('should handle single data item', () => {
|
||||
const singleData: PieChartDataItem[] = [{ id: 'single', value: 100 }];
|
||||
const singleData: PieChartDataItemWithColor[] = [
|
||||
{ key: 'single', value: 100 },
|
||||
];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePieChartData({
|
||||
@@ -151,7 +152,7 @@ describe('usePieChartData', () => {
|
||||
);
|
||||
|
||||
expect(result.current.enrichedData).toHaveLength(2);
|
||||
expect(result.current.enrichedData.map((d) => d.id)).toEqual([
|
||||
expect(result.current.enrichedData.map((item) => item.key)).toEqual([
|
||||
'item1',
|
||||
'item3',
|
||||
]);
|
||||
|
||||
+4
-3
@@ -15,6 +15,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
PieChartDataDocument,
|
||||
type PieChartConfiguration,
|
||||
type PieChartDataItem,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type UseGraphPieChartWidgetDataProps = {
|
||||
@@ -85,8 +86,8 @@ export const useGraphPieChartWidgetData = ({
|
||||
});
|
||||
|
||||
const chartData = queryData?.pieChartData?.data?.map(
|
||||
(item: PieChartDataItemWithColor): PieChartDataItemWithColor => {
|
||||
const rawValue = formattedToRawLookup.get(item.id);
|
||||
(item: PieChartDataItem): PieChartDataItemWithColor => {
|
||||
const rawValue = formattedToRawLookup.get(item.key);
|
||||
|
||||
const itemColor = determineChartItemColor({
|
||||
configurationColor,
|
||||
@@ -95,7 +96,7 @@ export const useGraphPieChartWidgetData = ({
|
||||
});
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
key: item.key,
|
||||
value: item.value,
|
||||
color: itemColor,
|
||||
};
|
||||
|
||||
+4
-4
@@ -48,17 +48,17 @@ export const usePieChartData = ({
|
||||
}, [data, colorRegistry, colorMode]);
|
||||
|
||||
const legendItems: GraphWidgetLegendItem[] = allEnrichedData.map((item) => ({
|
||||
id: item.id,
|
||||
label: String(item.id),
|
||||
id: item.key,
|
||||
label: String(item.key),
|
||||
color: item.colorScheme.solid,
|
||||
}));
|
||||
|
||||
const enrichedData = allEnrichedData.filter(
|
||||
(item) => !graphWidgetHiddenLegendIds.includes(item.id),
|
||||
(item) => !graphWidgetHiddenLegendIds.includes(item.key),
|
||||
);
|
||||
|
||||
const enrichedDataMap = useMemo(
|
||||
() => new Map(enrichedData.map((item) => [item.id, item])),
|
||||
() => new Map(enrichedData.map((item) => [item.key, item])),
|
||||
[enrichedData],
|
||||
);
|
||||
|
||||
|
||||
+10
-10
@@ -26,19 +26,19 @@ describe('getPieChartFormattedValue', () => {
|
||||
|
||||
const mockEnrichedData: PieChartEnrichedData[] = [
|
||||
{
|
||||
id: 'slice1',
|
||||
key: 'slice1',
|
||||
value: 30,
|
||||
percentage: 30,
|
||||
colorScheme: mockColorScheme,
|
||||
},
|
||||
{
|
||||
id: 'slice2',
|
||||
key: 'slice2',
|
||||
value: 50,
|
||||
percentage: 50,
|
||||
colorScheme: mockColorScheme,
|
||||
},
|
||||
{
|
||||
id: 'slice3',
|
||||
key: 'slice3',
|
||||
value: 20,
|
||||
percentage: 20,
|
||||
colorScheme: mockColorScheme,
|
||||
@@ -46,13 +46,13 @@ describe('getPieChartFormattedValue', () => {
|
||||
];
|
||||
|
||||
const createMockDatum = (
|
||||
id: string,
|
||||
key: string,
|
||||
options?: { computedId?: string },
|
||||
): ComputedDatum<PieChartDataItemWithColor> =>
|
||||
({
|
||||
id: options?.computedId ?? id,
|
||||
id: options?.computedId ?? key,
|
||||
value: 0,
|
||||
data: { id, value: 0 },
|
||||
data: { key, value: 0 },
|
||||
}) as unknown as ComputedDatum<PieChartDataItemWithColor>;
|
||||
|
||||
const defaultFormatOptions = {
|
||||
@@ -84,7 +84,7 @@ describe('getPieChartFormattedValue', () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should match by datum.data.id when computed id is namespaced per widget', () => {
|
||||
it('should match by datum.data.key when computed id is namespaced per widget', () => {
|
||||
const datum = createMockDatum('slice1', {
|
||||
computedId: 'widget-abc:slice1',
|
||||
});
|
||||
@@ -145,7 +145,7 @@ describe('getPieChartFormattedValue', () => {
|
||||
it('should format percentage to one decimal place', () => {
|
||||
const enrichedDataWithDecimal: PieChartEnrichedData[] = [
|
||||
{
|
||||
id: 'slice1',
|
||||
key: 'slice1',
|
||||
value: 33,
|
||||
percentage: 33.333,
|
||||
colorScheme: mockColorScheme,
|
||||
@@ -168,7 +168,7 @@ describe('getPieChartFormattedValue', () => {
|
||||
it('should handle zero value', () => {
|
||||
const enrichedDataWithZero: PieChartEnrichedData[] = [
|
||||
{
|
||||
id: 'zero',
|
||||
key: 'zero',
|
||||
value: 0,
|
||||
percentage: 0,
|
||||
colorScheme: mockColorScheme,
|
||||
@@ -189,7 +189,7 @@ describe('getPieChartFormattedValue', () => {
|
||||
it('should handle 100% value', () => {
|
||||
const enrichedDataWith100: PieChartEnrichedData[] = [
|
||||
{
|
||||
id: 'full',
|
||||
key: 'full',
|
||||
value: 100,
|
||||
percentage: 100,
|
||||
colorScheme: mockColorScheme,
|
||||
|
||||
+9
-9
@@ -26,19 +26,19 @@ describe('getPieChartTooltipData', () => {
|
||||
|
||||
const mockEnrichedData: PieChartEnrichedData[] = [
|
||||
{
|
||||
id: 'Product A',
|
||||
key: 'Product A',
|
||||
value: 500,
|
||||
percentage: 50,
|
||||
colorScheme: mockColorScheme,
|
||||
},
|
||||
{
|
||||
id: 'Product B',
|
||||
key: 'Product B',
|
||||
value: 300,
|
||||
percentage: 30,
|
||||
colorScheme: { ...mockColorScheme, solid: '#solidB' },
|
||||
},
|
||||
{
|
||||
id: 'Product C',
|
||||
key: 'Product C',
|
||||
value: 200,
|
||||
percentage: 20,
|
||||
colorScheme: { ...mockColorScheme, solid: '#solidC' },
|
||||
@@ -46,13 +46,13 @@ describe('getPieChartTooltipData', () => {
|
||||
];
|
||||
|
||||
const createMockDatum = (
|
||||
id: string,
|
||||
key: string,
|
||||
options?: { computedId?: string },
|
||||
): ComputedDatum<PieChartDataItemWithColor> =>
|
||||
({
|
||||
id: options?.computedId ?? id,
|
||||
id: options?.computedId ?? key,
|
||||
value: 0,
|
||||
data: { id, value: 0 },
|
||||
data: { key, value: 0 },
|
||||
}) as unknown as ComputedDatum<PieChartDataItemWithColor>;
|
||||
|
||||
const defaultFormatOptions = {
|
||||
@@ -150,7 +150,7 @@ describe('getPieChartTooltipData', () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should match by datum.data.id when computed id is namespaced per widget', () => {
|
||||
it('should match by datum.data.key when computed id is namespaced per widget', () => {
|
||||
const datum = createMockDatum('Product A', {
|
||||
computedId: 'widget-xyz:Product A',
|
||||
});
|
||||
@@ -182,7 +182,7 @@ describe('getPieChartTooltipData', () => {
|
||||
it('should handle item with zero value', () => {
|
||||
const enrichedDataWithZero: PieChartEnrichedData[] = [
|
||||
{
|
||||
id: 'Zero Item',
|
||||
key: 'Zero Item',
|
||||
value: 0,
|
||||
percentage: 0,
|
||||
colorScheme: mockColorScheme,
|
||||
@@ -203,7 +203,7 @@ describe('getPieChartTooltipData', () => {
|
||||
it('should handle special characters in id', () => {
|
||||
const enrichedDataWithSpecialChars: PieChartEnrichedData[] = [
|
||||
{
|
||||
id: 'Item & Special <chars>',
|
||||
key: 'Item & Special <chars>',
|
||||
value: 100,
|
||||
percentage: 100,
|
||||
colorScheme: mockColorScheme,
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ export const getPieChartFormattedValue = ({
|
||||
displayType,
|
||||
}: GetPieChartFormattedValueParams): string | null => {
|
||||
const item = enrichedData.find(
|
||||
(enrichedDataItem) => enrichedDataItem.id === datum.data.id,
|
||||
(enrichedDataItem) => enrichedDataItem.key === datum.data.key,
|
||||
);
|
||||
if (!isDefined(item)) return null;
|
||||
|
||||
|
||||
+3
-3
@@ -23,7 +23,7 @@ export const getPieChartTooltipData = ({
|
||||
tooltipItem: GraphWidgetTooltipItem;
|
||||
} | null => {
|
||||
const item = enrichedData.find(
|
||||
(enrichedDataItem) => enrichedDataItem.id === datum.data.id,
|
||||
(enrichedDataItem) => enrichedDataItem.key === datum.data.key,
|
||||
);
|
||||
if (!isDefined(item)) return null;
|
||||
|
||||
@@ -37,8 +37,8 @@ export const getPieChartTooltipData = ({
|
||||
|
||||
return {
|
||||
tooltipItem: {
|
||||
key: item.id,
|
||||
label: item.id,
|
||||
key: item.key,
|
||||
label: item.key,
|
||||
formattedValue,
|
||||
value: item.value,
|
||||
dotColor: item.colorScheme.solid,
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ export const LINE_CHART_DATA = gql`
|
||||
query LineChartData($input: LineChartDataInput!) {
|
||||
lineChartData(input: $input) {
|
||||
series {
|
||||
id
|
||||
key
|
||||
label
|
||||
data {
|
||||
x
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ export const PIE_CHART_DATA = gql`
|
||||
query PieChartData($input: PieChartDataInput!) {
|
||||
pieChartData(input: $input) {
|
||||
data {
|
||||
id
|
||||
key
|
||||
value
|
||||
}
|
||||
showLegend
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ export const UsageDailyChartSection = ({
|
||||
|
||||
const lineData: LineChartSeriesWithColor[] = [
|
||||
{
|
||||
id: chartId,
|
||||
key: chartId,
|
||||
label: chartLabel,
|
||||
data: timeSeries.map((point) => ({
|
||||
x: formatDate(point.date, 'MMM d'),
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { LineChartDataPointDTO } from 'src/modules/dashboard/chart-data/dtos/lin
|
||||
@ObjectType('LineChartSeries')
|
||||
export class LineChartSeriesDTO {
|
||||
@Field(() => String)
|
||||
id: string;
|
||||
key: string;
|
||||
|
||||
@Field(() => String)
|
||||
label: string;
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { Field, Float, ObjectType } from '@nestjs/graphql';
|
||||
@ObjectType('PieChartDataItem')
|
||||
export class PieChartDataItemDTO {
|
||||
@Field(() => String)
|
||||
id: string;
|
||||
key: string;
|
||||
|
||||
@Field(() => Float)
|
||||
value: number;
|
||||
|
||||
+9
-9
@@ -137,11 +137,11 @@ describe('PieChartDataService', () => {
|
||||
|
||||
expect(result.data).toHaveLength(2);
|
||||
expect(result.data[0]).toEqual({
|
||||
id: 'Active',
|
||||
key: 'Active',
|
||||
value: 10,
|
||||
});
|
||||
expect(result.data[1]).toEqual({
|
||||
id: 'Inactive',
|
||||
key: 'Inactive',
|
||||
value: 5,
|
||||
});
|
||||
expect(result.showLegend).toBe(true);
|
||||
@@ -162,8 +162,8 @@ describe('PieChartDataService', () => {
|
||||
});
|
||||
|
||||
expect(result.data).toEqual([
|
||||
{ id: 'Not Set', value: 2 },
|
||||
{ id: 'Active', value: 5 },
|
||||
{ key: 'Not Set', value: 2 },
|
||||
{ key: 'Active', value: 5 },
|
||||
]);
|
||||
expect(result.formattedToRawLookup?.['Not Set']).toBeUndefined();
|
||||
expect(result.formattedToRawLookup?.['Active']).toBe('Active');
|
||||
@@ -186,7 +186,7 @@ describe('PieChartDataService', () => {
|
||||
});
|
||||
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe('Active');
|
||||
expect(result.data[0].key).toBe('Active');
|
||||
});
|
||||
|
||||
it('should flag too many groups and limit slices', async () => {
|
||||
@@ -313,8 +313,8 @@ describe('PieChartDataService', () => {
|
||||
authContext: mockAuthContext,
|
||||
});
|
||||
|
||||
expect(result.data[0].id).toBe('Open');
|
||||
expect(result.data[1].id).toBe('Closed');
|
||||
expect(result.data[0].key).toBe('Open');
|
||||
expect(result.data[1].key).toBe('Closed');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -461,8 +461,8 @@ describe('PieChartDataService', () => {
|
||||
authContext: mockAuthContext,
|
||||
});
|
||||
|
||||
expect(result.data[0].id).toBe('Yes');
|
||||
expect(result.data[1].id).toBe('No');
|
||||
expect(result.data[0].key).toBe('Yes');
|
||||
expect(result.data[1].key).toBe('No');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+2
-2
@@ -295,7 +295,7 @@ export class LineChartDataService {
|
||||
|
||||
const series = [
|
||||
{
|
||||
id: `${seriesIdPrefix}${aggregateField.name}`,
|
||||
key: `${seriesIdPrefix}${aggregateField.name}`,
|
||||
label: aggregateField.label,
|
||||
data: dataPoints,
|
||||
},
|
||||
@@ -480,7 +480,7 @@ export class LineChartDataService {
|
||||
}
|
||||
|
||||
return {
|
||||
id: prefixedSeriesId,
|
||||
key: prefixedSeriesId,
|
||||
label: seriesId,
|
||||
data: dataPoints,
|
||||
};
|
||||
|
||||
+2
-2
@@ -203,7 +203,7 @@ export class PieChartDataService {
|
||||
: null;
|
||||
|
||||
return {
|
||||
id: point.formattedValue,
|
||||
key: point.formattedValue,
|
||||
value: point.aggregateValue,
|
||||
rawValue: rawValueString,
|
||||
};
|
||||
@@ -214,7 +214,7 @@ export class PieChartDataService {
|
||||
orderBy: configuration.orderBy,
|
||||
manualSortOrder: configuration.manualSortOrder,
|
||||
formattedToRawLookup,
|
||||
getFieldValue: (item) => item.id,
|
||||
getFieldValue: (item) => item.key,
|
||||
getNumericValue: (item) => item.value,
|
||||
selectFieldOptions: selectOptions,
|
||||
fieldType: groupByField.type,
|
||||
|
||||
Reference in New Issue
Block a user