Skip to content

Commit 2dd0fea

Browse files
committed
feat: Improve pie chart implementation
1 parent e984e20 commit 2dd0fea

File tree

12 files changed

+592
-243
lines changed

12 files changed

+592
-243
lines changed

.changeset/twenty-hats-deny.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@hyperdx/api": patch
3+
"@hyperdx/app": patch
4+
---
5+
6+
fix: Improve Pie Chart implemententation

packages/api/openapi.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,6 +1178,47 @@
11781178
}
11791179
}
11801180
},
1181+
"PieChartConfig": {
1182+
"type": "object",
1183+
"required": [
1184+
"displayType",
1185+
"sourceId",
1186+
"select"
1187+
],
1188+
"description": "Configuration for a pie chart tile. Each slice represents one group value.",
1189+
"properties": {
1190+
"displayType": {
1191+
"type": "string",
1192+
"enum": [
1193+
"pie"
1194+
],
1195+
"example": "pie"
1196+
},
1197+
"sourceId": {
1198+
"type": "string",
1199+
"description": "ID of the data source to query.",
1200+
"example": "65f5e4a3b9e77c001a111111"
1201+
},
1202+
"select": {
1203+
"type": "array",
1204+
"minItems": 1,
1205+
"maxItems": 1,
1206+
"description": "Exactly one aggregated value used to size each pie slice.",
1207+
"items": {
1208+
"$ref": "#/components/schemas/SelectItem"
1209+
}
1210+
},
1211+
"groupBy": {
1212+
"type": "string",
1213+
"maxLength": 10000,
1214+
"description": "Field expression to group results by (one slice per group value).",
1215+
"example": "service"
1216+
},
1217+
"numberFormat": {
1218+
"$ref": "#/components/schemas/NumberFormat"
1219+
}
1220+
}
1221+
},
11811222
"SearchChartConfig": {
11821223
"type": "object",
11831224
"required": [
@@ -1255,6 +1296,9 @@
12551296
{
12561297
"$ref": "#/components/schemas/NumberChartConfig"
12571298
},
1299+
{
1300+
"$ref": "#/components/schemas/PieChartConfig"
1301+
},
12581302
{
12591303
"$ref": "#/components/schemas/SearchChartConfig"
12601304
},
@@ -1269,6 +1313,7 @@
12691313
"stacked_bar": "#/components/schemas/BarChartConfig",
12701314
"table": "#/components/schemas/TableChartConfig",
12711315
"number": "#/components/schemas/NumberChartConfig",
1316+
"pie": "#/components/schemas/PieChartConfig",
12721317
"search": "#/components/schemas/SearchChartConfig",
12731318
"markdown": "#/components/schemas/MarkdownChartConfig"
12741319
}

packages/api/src/routers/external-api/__tests__/dashboards.test.ts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1648,6 +1648,26 @@ describe('External API v2 Dashboards - new format', () => {
16481648
},
16491649
});
16501650

1651+
const createPieChart = (sourceId: string): ExternalDashboardTileWithId => ({
1652+
name: 'Pie Chart',
1653+
x: 6,
1654+
y: 3,
1655+
w: 3,
1656+
h: 3,
1657+
id: new ObjectId().toString(),
1658+
config: {
1659+
displayType: 'pie',
1660+
sourceId,
1661+
select: [
1662+
{
1663+
aggFn: 'count',
1664+
where: '',
1665+
},
1666+
],
1667+
groupBy: 'service.name',
1668+
},
1669+
});
1670+
16511671
const server = getServer();
16521672
let agent, team, user, traceSource, metricSource;
16531673

@@ -1912,6 +1932,7 @@ describe('External API v2 Dashboards - new format', () => {
19121932
createTableChart(traceSource._id.toString()),
19131933
createNumberChart(traceSource._id.toString()),
19141934
createMarkdownChart(),
1935+
createPieChart(traceSource._id.toString()),
19151936
],
19161937
tags: ['test', 'chart-types'],
19171938
};
@@ -1922,18 +1943,45 @@ describe('External API v2 Dashboards - new format', () => {
19221943

19231944
const { id } = response.body.data;
19241945
expect(response.body.data).toHaveProperty('id');
1925-
expect(response.body.data.tiles.length).toBe(4);
1946+
expect(response.body.data.tiles.length).toBe(5);
19261947

19271948
// Verify by retrieving the dashboard
19281949
const retrieveResponse = await authRequest('get', `${BASE_URL}/${id}`);
19291950

19301951
expect(retrieveResponse.status).toBe(200);
1931-
expect(retrieveResponse.body.data.tiles.length).toBe(4);
1952+
expect(retrieveResponse.body.data.tiles.length).toBe(5);
19321953
expect(retrieveResponse.body.data.tags).toEqual(['test', 'chart-types']);
19331954
});
19341955

19351956
it('can round-trip all supported chart types and all supported fields on each chart type', async () => {
19361957
// Arrange
1958+
const pieChart: ExternalDashboardTile = {
1959+
name: 'Pie Chart',
1960+
x: 6,
1961+
y: 3,
1962+
w: 6,
1963+
h: 3,
1964+
config: {
1965+
displayType: 'pie',
1966+
sourceId: traceSource._id.toString(),
1967+
select: [
1968+
{
1969+
aggFn: 'quantile',
1970+
level: 0.5,
1971+
valueExpression: 'Duration',
1972+
alias: 'Median Duration',
1973+
where: "env = 'production'",
1974+
whereLanguage: 'sql',
1975+
},
1976+
],
1977+
groupBy: 'service.name',
1978+
numberFormat: {
1979+
output: 'number',
1980+
mantissa: 2,
1981+
},
1982+
},
1983+
};
1984+
19371985
const lineChart: ExternalDashboardTile = {
19381986
name: 'Line Chart',
19391987
x: 0,
@@ -2087,7 +2135,14 @@ describe('External API v2 Dashboards - new format', () => {
20872135
const response = await authRequest('post', BASE_URL)
20882136
.send({
20892137
name: 'Dashboard with All Chart Types',
2090-
tiles: [lineChart, barChart, tableChart, numberChart, markdownChart],
2138+
tiles: [
2139+
lineChart,
2140+
barChart,
2141+
tableChart,
2142+
numberChart,
2143+
markdownChart,
2144+
pieChart,
2145+
],
20912146
tags: ['round-trip-test'],
20922147
})
20932148
.expect(200);
@@ -2098,6 +2153,7 @@ describe('External API v2 Dashboards - new format', () => {
20982153
expect(omit(response.body.data.tiles[2], ['id'])).toEqual(tableChart);
20992154
expect(omit(response.body.data.tiles[3], ['id'])).toEqual(numberChart);
21002155
expect(omit(response.body.data.tiles[4], ['id'])).toEqual(markdownChart);
2156+
expect(omit(response.body.data.tiles[5], ['id'])).toEqual(pieChart);
21012157
});
21022158

21032159
it('should return 400 when source IDs do not exist', async () => {

packages/api/src/routers/external-api/v2/dashboards.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,37 @@ async function getMissingSources(
585585
* numberFormat:
586586
* $ref: '#/components/schemas/NumberFormat'
587587
*
588+
* PieChartConfig:
589+
* type: object
590+
* required:
591+
* - displayType
592+
* - sourceId
593+
* - select
594+
* description: Configuration for a pie chart tile. Each slice represents one group value.
595+
* properties:
596+
* displayType:
597+
* type: string
598+
* enum: [pie]
599+
* example: "pie"
600+
* sourceId:
601+
* type: string
602+
* description: ID of the data source to query.
603+
* example: "65f5e4a3b9e77c001a111111"
604+
* select:
605+
* type: array
606+
* minItems: 1
607+
* maxItems: 1
608+
* description: Exactly one aggregated value used to size each pie slice.
609+
* items:
610+
* $ref: '#/components/schemas/SelectItem'
611+
* groupBy:
612+
* type: string
613+
* maxLength: 10000
614+
* description: Field expression to group results by (one slice per group value).
615+
* example: "service"
616+
* numberFormat:
617+
* $ref: '#/components/schemas/NumberFormat'
618+
*
588619
* SearchChartConfig:
589620
* type: object
590621
* required:
@@ -641,6 +672,7 @@ async function getMissingSources(
641672
* - $ref: '#/components/schemas/BarChartConfig'
642673
* - $ref: '#/components/schemas/TableChartConfig'
643674
* - $ref: '#/components/schemas/NumberChartConfig'
675+
* - $ref: '#/components/schemas/PieChartConfig'
644676
* - $ref: '#/components/schemas/SearchChartConfig'
645677
* - $ref: '#/components/schemas/MarkdownChartConfig'
646678
* discriminator:
@@ -650,6 +682,7 @@ async function getMissingSources(
650682
* stacked_bar: '#/components/schemas/BarChartConfig'
651683
* table: '#/components/schemas/TableChartConfig'
652684
* number: '#/components/schemas/NumberChartConfig'
685+
* pie: '#/components/schemas/PieChartConfig'
653686
* search: '#/components/schemas/SearchChartConfig'
654687
* markdown: '#/components/schemas/MarkdownChartConfig'
655688
*

packages/api/src/routers/external-api/v2/utils/dashboards.ts

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ const convertToExternalTileChartConfig = (
9191
};
9292

9393
switch (config.displayType) {
94-
case 'line':
95-
case 'stacked_bar':
94+
case DisplayType.Line:
95+
case DisplayType.StackedBar:
9696
return {
9797
displayType: config.displayType,
9898
sourceId,
@@ -106,12 +106,12 @@ const convertToExternalTileChartConfig = (
106106
select: Array.isArray(config.select)
107107
? config.select.map(convertToExternalSelectItem)
108108
: [DEFAULT_SELECT_ITEM],
109-
...(config.displayType === 'line'
109+
...(config.displayType === DisplayType.Line
110110
? { compareToPreviousPeriod: config.compareToPreviousPeriod }
111111
: {}),
112112
numberFormat: config.numberFormat,
113113
};
114-
case 'number':
114+
case DisplayType.Number:
115115
return {
116116
displayType: config.displayType,
117117
sourceId,
@@ -120,7 +120,17 @@ const convertToExternalTileChartConfig = (
120120
: [DEFAULT_SELECT_ITEM],
121121
numberFormat: config.numberFormat,
122122
};
123-
case 'table':
123+
case DisplayType.Pie:
124+
return {
125+
displayType: config.displayType,
126+
sourceId,
127+
select: Array.isArray(config.select)
128+
? [convertToExternalSelectItem(config.select[0])]
129+
: [DEFAULT_SELECT_ITEM],
130+
groupBy: stringValueOrDefault(config.groupBy, undefined),
131+
numberFormat: config.numberFormat,
132+
};
133+
case DisplayType.Table:
124134
return {
125135
...pick(config, ['having', 'numberFormat']),
126136
displayType: config.displayType,
@@ -135,25 +145,28 @@ const convertToExternalTileChartConfig = (
135145
: [DEFAULT_SELECT_ITEM],
136146
orderBy: stringValueOrDefault(config.orderBy, undefined),
137147
};
138-
case 'search':
148+
case DisplayType.Search:
139149
return {
140150
displayType: config.displayType,
141151
sourceId,
142152
select: stringValueOrDefault(config.select, ''),
143153
where: config.where,
144154
whereLanguage: config.whereLanguage ?? 'lucene',
145155
};
146-
case 'markdown':
156+
case DisplayType.Markdown:
147157
return {
148158
displayType: config.displayType,
149159
markdown: stringValueOrDefault(config.markdown, ''),
150160
};
151-
default:
161+
case DisplayType.Heatmap:
162+
case undefined:
152163
logger.error(
153164
{ config },
154-
'Error converting chart config to external chart - unrecognized display type',
165+
'Error converting chart config to external chart - unsupported display type',
155166
);
156167
return undefined;
168+
default:
169+
config.displayType satisfies never;
157170
}
158171
};
159172

@@ -257,6 +270,16 @@ export function convertToInternalTileConfig(
257270
name,
258271
};
259272
break;
273+
case 'pie':
274+
internalConfig = {
275+
...pick(externalConfig, ['groupBy', 'numberFormat']),
276+
displayType: DisplayType.Pie,
277+
select: [convertToInternalSelectItem(externalConfig.select[0])],
278+
source: externalConfig.sourceId,
279+
where: '',
280+
name,
281+
};
282+
break;
260283
case 'search':
261284
internalConfig = {
262285
...pick(externalConfig, ['select', 'where']),
@@ -276,6 +299,13 @@ export function convertToInternalTileConfig(
276299
name,
277300
};
278301
break;
302+
default:
303+
// Typecheck to ensure all display types are handled
304+
externalConfig satisfies never;
305+
306+
// We should never hit this due to the typecheck above.
307+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
308+
internalConfig = {} as SavedChartConfig;
279309
}
280310

281311
// Omit keys that are null/undefined, so that they're not saved as null in Mongo.

packages/api/src/utils/zod.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,14 @@ const externalDashboardNumberChartConfigSchema = z.object({
234234
numberFormat: NumberFormatSchema.optional(),
235235
});
236236

237+
const externalDashboardPieChartConfigSchema = z.object({
238+
displayType: z.literal('pie'),
239+
sourceId: objectIdSchema,
240+
select: z.array(externalDashboardSelectItemSchema).length(1),
241+
groupBy: z.string().max(10000).optional(),
242+
numberFormat: NumberFormatSchema.optional(),
243+
});
244+
237245
const externalDashboardSearchChartConfigSchema = z.object({
238246
displayType: z.literal('search'),
239247
sourceId: objectIdSchema,
@@ -253,6 +261,7 @@ export const externalDashboardTileConfigSchema = z
253261
externalDashboardBarChartConfigSchema,
254262
externalDashboardTableChartConfigSchema,
255263
externalDashboardNumberChartConfigSchema,
264+
externalDashboardPieChartConfigSchema,
256265
externalDashboardMarkdownChartConfigSchema,
257266
externalDashboardSearchChartConfigSchema,
258267
])

0 commit comments

Comments
 (0)