@adminforth/dashboard 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +116 -39
  2. package/custom/api/dashboardApi.ts +4 -0
  3. package/custom/composables/useElementSize.ts +17 -2
  4. package/custom/model/dashboard.types.ts +337 -236
  5. package/custom/skills/adminforth-dashboard/SKILL.md +113 -2
  6. package/custom/widgets/chart/ChartWidget.vue +38 -53
  7. package/custom/widgets/chart/bar/BarChart.vue +20 -12
  8. package/custom/widgets/chart/chart.types.ts +17 -66
  9. package/custom/widgets/chart/chart.utils.ts +11 -0
  10. package/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
  11. package/custom/widgets/chart/line/LineChart.vue +23 -15
  12. package/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
  13. package/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -12
  14. package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  15. package/custom/widgets/pivot-table/PivotTableWidget.vue +8 -7
  16. package/custom/widgets/table/TableWidget.vue +8 -3
  17. package/dist/custom/api/dashboardApi.d.ts +1 -0
  18. package/dist/custom/api/dashboardApi.js +5 -0
  19. package/dist/custom/api/dashboardApi.ts +4 -0
  20. package/dist/custom/composables/useElementSize.js +14 -2
  21. package/dist/custom/composables/useElementSize.ts +17 -2
  22. package/dist/custom/model/dashboard.types.d.ts +181 -61
  23. package/dist/custom/model/dashboard.types.js +82 -93
  24. package/dist/custom/model/dashboard.types.ts +337 -236
  25. package/dist/custom/queries/useDashboardConfig.d.ts +852 -66
  26. package/dist/custom/queries/useWidgetData.d.ts +848 -62
  27. package/dist/custom/skills/adminforth-dashboard/SKILL.md +113 -2
  28. package/dist/custom/widgets/chart/ChartWidget.vue +38 -53
  29. package/dist/custom/widgets/chart/bar/BarChart.vue +20 -12
  30. package/dist/custom/widgets/chart/chart.types.d.ts +13 -22
  31. package/dist/custom/widgets/chart/chart.types.js +2 -25
  32. package/dist/custom/widgets/chart/chart.types.ts +17 -66
  33. package/dist/custom/widgets/chart/chart.utils.d.ts +1 -0
  34. package/dist/custom/widgets/chart/chart.utils.js +7 -0
  35. package/dist/custom/widgets/chart/chart.utils.ts +11 -0
  36. package/dist/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
  37. package/dist/custom/widgets/chart/line/LineChart.vue +23 -15
  38. package/dist/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
  39. package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -12
  40. package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  41. package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +8 -7
  42. package/dist/custom/widgets/table/TableWidget.vue +8 -3
  43. package/dist/endpoint/dashboard.d.ts +7 -2
  44. package/dist/endpoint/dashboard.js +45 -1
  45. package/dist/endpoint/widgets.d.ts +2 -1
  46. package/dist/endpoint/widgets.js +6 -2
  47. package/dist/schema/api.d.ts +2773 -736
  48. package/dist/schema/api.js +5 -0
  49. package/dist/schema/widget.d.ts +1648 -476
  50. package/dist/schema/widget.js +208 -139
  51. package/dist/services/widgetConfigValidator.js +16 -40
  52. package/dist/services/widgetDataService.d.ts +2 -1
  53. package/dist/services/widgetDataService.js +389 -82
  54. package/endpoint/dashboard.ts +77 -4
  55. package/endpoint/widgets.ts +11 -4
  56. package/package.json +1 -1
  57. package/schema/api.ts +6 -0
  58. package/schema/widget.ts +225 -139
  59. package/services/widgetConfigValidator.ts +29 -53
  60. package/services/widgetDataService.ts +522 -100
@@ -41,6 +41,7 @@ If the user asks how the schema works, how to implement the API, or how to chang
41
41
  Use these tools whenever available:
42
42
 
43
43
  - `dashboard_get_config`
44
+ - `dashboard_set_dashboard_config`
44
45
  - `dashboard_add_dashboard_group`
45
46
  - `dashboard_set_dashboard_group_config`
46
47
  - `dashboard_move_dashboard_group`
@@ -58,6 +59,7 @@ If a dashboard tool is known by name but its argument schema is not loaded, call
58
59
  Do not pass fields between dashboard tools by analogy. Use each tool's schema.
59
60
 
60
61
  - `dashboard_add_dashboard_group` creates a new group. It accepts the dashboard slug only. Never pass `groupId` to this tool.
62
+ - `dashboard_set_dashboard_config` replaces the full dashboard config. Use it when the user explicitly asks to edit the whole dashboard config.
61
63
  - `dashboard_add_dashboard_widget` creates a widget inside an existing group. Use it when you already have a `groupId`.
62
64
  - `dashboard_set_dashboard_group_config`, `dashboard_move_dashboard_group`, and `dashboard_remove_dashboard_group` operate on an existing group and need `groupId`.
63
65
  - `dashboard_set_widget_config`, `dashboard_move_dashboard_widget`, `dashboard_remove_dashboard_widget`, and `dashboard_get_dashboard_widget_data` operate on an existing widget and need `widgetId`.
@@ -113,13 +115,122 @@ For group requests:
113
115
 
114
116
  If slug is missing, use `default`.
115
117
 
118
+ ## Dashboard Config Workflow
119
+
120
+ Use `dashboard_set_dashboard_config` only when the user explicitly asks to edit the whole dashboard root config.
121
+
122
+ For requests like:
123
+
124
+ - "update root dashboard config"
125
+ - "replace the whole dashboard config"
126
+
127
+ do this:
128
+
129
+ 1. Call `dashboard_get_config`.
130
+ 2. Modify the returned root config, preserving existing `version`, `groups`, and `widgets` unless the user asked to change them.
131
+ 3. Call `dashboard_set_dashboard_config` with the full updated config.
132
+ 4. Return a short summary of the root-level fields changed.
133
+
134
+ Do not use `dashboard_set_dashboard_config` to store reusable widget variables.
135
+
116
136
  ## Widget Config Rules
117
137
 
118
138
  Use the current schema keys exactly:
119
139
 
120
140
  - Use `target`, not `type`.
121
141
  - Use `label`, not `title`.
122
- - Use `data_source`, not `dataSource`.
123
- - Use `resource_id`, not `resourceId`.
142
+ - Use `query`, not `data_source`.
143
+ - Use `resource`, not `resource_id`.
124
144
  - Use `group_by`, not `groupBy`.
145
+ - Use `order_by`, not `orderBy`.
125
146
  - Use `page_size`, not `pageSize`.
147
+ - For step-based chart queries, use `query.steps` as an ordered array of `{ name, resource, metric, filters }` steps and add `query.calcs` when derived fields are needed.
148
+ - Use `card` for KPI and gauge widget view config.
149
+ - Use `pivot` for pivot table view config.
150
+ - Use `variables` for reusable static maps or constants at widget level.
151
+ - In `query.calcs`, use `lookup($variables.some.map, row_field, default_number)` to read a numeric value from a variable map by the current row/group field.
152
+
153
+ ## Variables And Lookup Calcs
154
+
155
+ Widget config can define variables:
156
+
157
+ ```yaml
158
+ variables:
159
+ token_prices_per_1m:
160
+ input:
161
+ gpt-4.1: 2.00
162
+ gpt-4.1-mini: 0.40
163
+ gpt-4o-mini: 0.15
164
+ output:
165
+ gpt-4.1: 8.00
166
+ gpt-4.1-mini: 1.60
167
+ gpt-4o-mini: 0.60
168
+ cached:
169
+ gpt-4.1: 0.50
170
+ gpt-4.1-mini: 0.10
171
+ gpt-4o-mini: 0.075
172
+ ```
173
+
174
+ Use variables when a calculation needs a static rate table, threshold table, coefficient map, or other reusable constants. In calcs, `lookup($variables.path.to.map, field_name, 0)` returns the value from the map using `field_name` from the current row/group. The third argument is the numeric fallback when the key is missing.
175
+
176
+ Example widget:
177
+
178
+ ```yaml
179
+ target: chart
180
+ label: Model costs
181
+ size: large
182
+ variables:
183
+ token_prices_per_1m:
184
+ input:
185
+ gpt-4.1: 2.00
186
+ gpt-4.1-mini: 0.40
187
+ gpt-4o-mini: 0.15
188
+ output:
189
+ gpt-4.1: 8.00
190
+ gpt-4.1-mini: 1.60
191
+ gpt-4o-mini: 0.60
192
+ cached:
193
+ gpt-4.1: 0.50
194
+ gpt-4.1-mini: 0.10
195
+ gpt-4o-mini: 0.075
196
+
197
+ chart:
198
+ type: stacked_bar
199
+ title: LLM costs by model
200
+ x:
201
+ field: model
202
+ label: Model
203
+ y:
204
+ - field: input_cost
205
+ label: Input
206
+ format: currency
207
+ - field: output_cost
208
+ label: Output
209
+ format: currency
210
+ - field: cached_cost
211
+ label: Cached
212
+ format: currency
213
+
214
+ query:
215
+ resource: model_usage
216
+ select:
217
+ - field: model
218
+ - agg: sum
219
+ field: input_tokens
220
+ as: input_tokens
221
+ - agg: sum
222
+ field: output_tokens
223
+ as: output_tokens
224
+ - agg: sum
225
+ field: cached_tokens
226
+ as: cached_tokens
227
+ group_by:
228
+ - model
229
+ calcs:
230
+ - calc: input_tokens / 1000000 * lookup($variables.token_prices_per_1m.input, model, 0)
231
+ as: input_cost
232
+ - calc: output_tokens / 1000000 * lookup($variables.token_prices_per_1m.output, model, 0)
233
+ as: output_cost
234
+ - calc: cached_tokens / 1000000 * lookup($variables.token_prices_per_1m.cached, model, 0)
235
+ as: cached_cost
236
+ ```
@@ -26,7 +26,7 @@
26
26
  :rows="rows"
27
27
  :x-field="xField"
28
28
  :y-field="yField"
29
- :series-name="chartConfig.seriesName"
29
+ :series-name="lineSeriesName"
30
30
  :color="chartConfig.color"
31
31
  :height="chartHeight"
32
32
  />
@@ -69,9 +69,10 @@
69
69
 
70
70
  <StackedBarChart
71
71
  v-else-if="chartConfig?.type === 'stacked_bar'"
72
- :rows="rows"
72
+ :rows="stackedBarRows"
73
73
  :x-field="xField"
74
- :series="stackedBarSeries"
74
+ :y-field="stackedBarYField"
75
+ :series-field="stackedBarSeriesField"
75
76
  :colors="chartConfig.colors"
76
77
  :height="chartHeight"
77
78
  />
@@ -98,7 +99,7 @@ import HistogramChart from './histogram/HistogramChart.vue'
98
99
  import LineChart from './line/LineChart.vue'
99
100
  import PieChart from './pie/PieChart.vue'
100
101
  import StackedBarChart from './stacked-bar/StackedBarChart.vue'
101
- import { formatChartLabel, toFiniteNumber } from './chart.utils.js'
102
+ import { toFiniteNumber } from './chart.utils.js'
102
103
 
103
104
  const DEFAULT_WIDGET_HEIGHT = 500
104
105
 
@@ -128,15 +129,6 @@ const chartData = computed(() => data.value?.data as DashboardWidgetTableData |
128
129
  const rows = computed(() => chartData.value?.rows ?? [])
129
130
  const columns = computed(() => chartData.value?.columns ?? [])
130
131
  const chartConfig = computed(() => normalizeChartWidgetConfig(props.widget.chart))
131
- const aggregateGroupField = computed(() => {
132
- const dataSource = props.widget.dataSource
133
-
134
- if (dataSource?.type !== 'aggregate' || !dataSource.groupBy) {
135
- return undefined
136
- }
137
-
138
- return dataSource.groupBy.field
139
- })
140
132
 
141
133
  function resolveChartDimensionField(field: string | undefined, fallbackField: string | undefined) {
142
134
  const resolvedField = field ?? fallbackField
@@ -149,41 +141,39 @@ function resolveChartDimensionField(field: string | undefined, fallbackField: st
149
141
  return resolvedField
150
142
  }
151
143
 
152
- if (
153
- aggregateGroupField.value
154
- && resolvedField === aggregateGroupField.value
155
- && columns.value.includes('group')
156
- ) {
157
- return 'group'
158
- }
159
-
160
144
  return resolvedField
161
145
  }
162
146
 
163
- const xField = computed(() => resolveChartDimensionField(chartConfig.value?.xField, columns.value[0]))
164
- const yField = computed(() => chartConfig.value?.yField || columns.value[1])
165
- const labelField = computed(() => resolveChartDimensionField(chartConfig.value?.labelField, columns.value[0]))
166
- const valueField = computed(() => chartConfig.value?.valueField || columns.value[1])
167
- const pieRows = computed(() => {
168
- if (chartConfig.value?.valueField) {
147
+ const firstYField = computed(() => {
148
+ const y = chartConfig.value?.y
149
+ return Array.isArray(y) ? y[0]?.field : y?.field
150
+ })
151
+ const xField = computed(() => resolveChartDimensionField(chartConfig.value?.x?.field, columns.value[0]))
152
+ const yField = computed(() => firstYField.value || columns.value[1])
153
+ const labelField = computed(() => resolveChartDimensionField(chartConfig.value?.label?.field, columns.value[0] || 'name'))
154
+ const valueField = computed(() => chartConfig.value?.value?.field || columns.value[1] || 'value')
155
+ const pieRows = computed(() => rows.value)
156
+ const pieLabelField = computed(() => labelField.value)
157
+ const pieValueField = computed(() => valueField.value)
158
+ const stackedBarYItems = computed(() => {
159
+ const y = chartConfig.value?.y
160
+ return Array.isArray(y) ? y : []
161
+ })
162
+ const stackedBarRows = computed(() => {
163
+ if (chartConfig.value?.type !== 'stacked_bar' || !stackedBarYItems.value.length) {
169
164
  return rows.value
170
165
  }
171
166
 
172
- const groupedRows = new Map<string, { label: string, value: number }>()
173
-
174
- for (const row of rows.value) {
175
- const label = formatChartLabel(row[labelField.value])
176
- const item = groupedRows.get(label) ?? { label, value: 0 }
177
- item.value += 1
178
- groupedRows.set(label, item)
179
- }
180
-
181
- return Array.from(groupedRows.values())
167
+ return rows.value.flatMap((row) => stackedBarYItems.value.map((item) => ({
168
+ [xField.value]: row[xField.value],
169
+ __series: item.label ?? item.field,
170
+ __value: row[item.field],
171
+ })))
182
172
  })
183
- const pieLabelField = computed(() => chartConfig.value?.valueField ? labelField.value : 'label')
184
- const pieValueField = computed(() => chartConfig.value?.valueField ? valueField.value : 'value')
185
173
  const barRows = computed(() => {
186
- const bucketField = chartConfig.value?.bucketField
174
+ const bucketField = chartConfig.value?.type === 'histogram'
175
+ ? chartConfig.value.x?.field
176
+ : undefined
187
177
 
188
178
  if (!bucketField) {
189
179
  return rows.value
@@ -200,19 +190,14 @@ const barRows = computed(() => {
200
190
  }).length,
201
191
  }))
202
192
  })
203
- const barLabelField = computed(() => chartConfig.value?.bucketField ? 'label' : labelField.value)
204
- const barValueField = computed(() => chartConfig.value?.bucketField ? 'count' : valueField.value)
205
- const stackedBarSeries = computed(() => {
206
- if (chartConfig.value?.series?.length) {
207
- return chartConfig.value.series
208
- }
209
-
210
- return columns.value
211
- .filter((column) => column !== xField.value)
212
- .map((column) => ({
213
- name: column,
214
- field: column,
215
- }))
193
+ const barLabelField = computed(() => chartConfig.value?.type === 'histogram' && chartConfig.value.buckets ? 'label' : xField.value)
194
+ const barValueField = computed(() => chartConfig.value?.type === 'histogram' && chartConfig.value.buckets ? 'count' : yField.value)
195
+ const seriesField = computed(() => chartConfig.value?.series?.field || columns.value[2] || '')
196
+ const stackedBarYField = computed(() => stackedBarYItems.value.length ? '__value' : yField.value)
197
+ const stackedBarSeriesField = computed(() => stackedBarYItems.value.length ? '__series' : seriesField.value)
198
+ const lineSeriesName = computed(() => {
199
+ const y = chartConfig.value?.y
200
+ return Array.isArray(y) ? y[0]?.label : undefined
216
201
  })
217
202
 
218
203
  const chartHeight = computed(() => {
@@ -83,7 +83,14 @@
83
83
  <script setup lang="ts">
84
84
  import { computed } from 'vue'
85
85
  import { useElementSize } from '../../../composables/useElementSize.js'
86
- import { CHART_COLORS, formatChartAxisLabel, formatChartLabel, formatChartValue, toFiniteNumber } from '../chart.utils.js'
86
+ import {
87
+ CHART_COLORS,
88
+ formatChartAxisLabel,
89
+ formatChartLabel,
90
+ formatChartValue,
91
+ getChartYAxisWidth,
92
+ toFiniteNumber,
93
+ } from '../chart.utils.js'
87
94
 
88
95
  const props = withDefaults(defineProps<{
89
96
  rows: Record<string, unknown>[]
@@ -97,12 +104,6 @@ const props = withDefaults(defineProps<{
97
104
 
98
105
  const { el: rootEl, width: rootWidth, height: rootHeight } = useElementSize<HTMLDivElement>()
99
106
 
100
- const padding = {
101
- top: 20,
102
- right: 6,
103
- bottom: 34,
104
- left: 38,
105
- }
106
107
  const chartWidth = computed(() => Math.max(rootWidth.value, 1))
107
108
  const chartHeight = computed(() => {
108
109
  if (rootHeight.value > 0) {
@@ -115,8 +116,15 @@ const chartHeight = computed(() => {
115
116
  const chartColor = computed(() => props.color || CHART_COLORS[0])
116
117
  const values = computed(() => props.rows.map((row) => toFiniteNumber(row[props.valueField])))
117
118
  const maxValue = computed(() => Math.max(...values.value, 1))
118
- const innerWidth = computed(() => Math.max(chartWidth.value - padding.left - padding.right, 1))
119
- const innerHeight = computed(() => Math.max(chartHeight.value - padding.top - padding.bottom, 1))
119
+ const yTickValues = computed(() => [maxValue.value, maxValue.value * 0.5, 0])
120
+ const padding = computed(() => ({
121
+ top: 20,
122
+ right: 6,
123
+ bottom: 34,
124
+ left: getChartYAxisWidth(yTickValues.value, chartWidth.value),
125
+ }))
126
+ const innerWidth = computed(() => Math.max(chartWidth.value - padding.value.left - padding.value.right, 1))
127
+ const innerHeight = computed(() => Math.max(chartHeight.value - padding.value.top - padding.value.bottom, 1))
120
128
  const barGap = 12
121
129
  const barWidth = computed(() => {
122
130
  const count = Math.max(props.rows.length, 1)
@@ -126,7 +134,7 @@ const totalChartWidth = computed(() => {
126
134
  const count = Math.max(props.rows.length, 1)
127
135
  return count * barWidth.value + (count - 1) * barGap
128
136
  })
129
- const chartStartX = computed(() => padding.left + Math.max((innerWidth.value - totalChartWidth.value) / 2, 0))
137
+ const chartStartX = computed(() => padding.value.left + Math.max((innerWidth.value - totalChartWidth.value) / 2, 0))
130
138
  const visibleLabelIndexes = computed(() => {
131
139
  const count = props.rows.length
132
140
  const approxLabelWidth = 52
@@ -155,13 +163,13 @@ const bars = computed(() => props.rows.map((row, index) => {
155
163
  axisLabel: formatChartAxisLabel(row[props.labelField]),
156
164
  value,
157
165
  x: chartStartX.value + index * (barWidth.value + barGap),
158
- y: padding.top + innerHeight.value - height,
166
+ y: padding.value.top + innerHeight.value - height,
159
167
  height,
160
168
  }
161
169
  }))
162
170
 
163
171
  const yTicks = computed(() => [0, 0.5, 1].map((ratio) => ({
164
172
  value: maxValue.value * (1 - ratio),
165
- y: padding.top + innerHeight.value * ratio,
173
+ y: padding.value.top + innerHeight.value * ratio,
166
174
  })))
167
175
  </script>
@@ -1,3 +1,5 @@
1
+ import type { ValueFormat } from '../../model/dashboard.types.js'
2
+
1
3
  export type ChartWidgetType =
2
4
  | 'line'
3
5
  | 'pie'
@@ -12,81 +14,39 @@ export type ChartWidgetBucketConfig = {
12
14
  max?: number
13
15
  }
14
16
 
17
+ export type ChartFieldRef = {
18
+ field: string
19
+ label?: string
20
+ format?: ValueFormat
21
+ }
22
+
15
23
  export type ChartWidgetSeriesConfig = {
16
- name: string
17
24
  field: string
25
+ label?: string
18
26
  color?: string
19
27
  }
20
28
 
21
29
  export type ChartWidgetConfig = {
22
30
  type: ChartWidgetType
23
31
  title?: string
24
- x_field?: string
25
- y_field?: string
26
- label_field?: string
27
- value_field?: string
28
- bucket_field?: string
32
+ x?: ChartFieldRef
33
+ y?: ChartFieldRef | ChartFieldRef[]
34
+ label?: ChartFieldRef
35
+ value?: ChartFieldRef
29
36
  buckets?: ChartWidgetBucketConfig[]
30
- series?: ChartWidgetSeriesConfig[]
31
- series_name?: string
37
+ series?: ChartWidgetSeriesConfig
32
38
  color?: string
33
39
  colors?: string[]
34
40
  }
35
41
 
36
- export type NormalizedChartWidgetConfig = {
37
- type: ChartWidgetType
38
- title?: string
39
- xField?: string
40
- yField?: string
41
- labelField?: string
42
- valueField?: string
43
- bucketField?: string
44
- buckets?: ChartWidgetBucketConfig[]
45
- series?: ChartWidgetSeriesConfig[]
46
- seriesName?: string
47
- color?: string
48
- colors?: string[]
49
- }
42
+ export type NormalizedChartWidgetConfig = ChartWidgetConfig
50
43
 
51
44
  export function normalizeChartWidgetConfig(value: unknown): NormalizedChartWidgetConfig | undefined {
52
- const config = asChartWidgetConfigRecord(value)
53
-
54
- if (!config) {
45
+ if (!isRecord(value) || !normalizeChartWidgetType(value.type)) {
55
46
  return undefined
56
47
  }
57
48
 
58
- const type = normalizeChartWidgetType(config.type)
59
-
60
- if (!type) {
61
- return undefined
62
- }
63
-
64
- const xField = getStringField(config, 'x_field')
65
- const yField = getStringField(config, 'y_field')
66
- const labelField = getStringField(config, 'label_field')
67
- const valueField = getStringField(config, 'value_field')
68
- const bucketField = getStringField(config, 'bucket_field')
69
- const seriesName = getStringField(config, 'series_name')
70
- const title = getStringField(config, 'title')
71
- const color = getStringField(config, 'color')
72
- const colors = Array.isArray(config.colors) ? config.colors as string[] : undefined
73
- const buckets = Array.isArray(config.buckets) ? config.buckets as ChartWidgetBucketConfig[] : undefined
74
- const series = Array.isArray(config.series) ? config.series as ChartWidgetSeriesConfig[] : undefined
75
-
76
- return {
77
- type,
78
- ...(title !== undefined ? { title } : {}),
79
- ...(xField !== undefined ? { xField } : {}),
80
- ...(yField !== undefined ? { yField } : {}),
81
- ...(labelField !== undefined ? { labelField } : {}),
82
- ...(valueField !== undefined ? { valueField } : {}),
83
- ...(bucketField !== undefined ? { bucketField } : {}),
84
- ...(buckets !== undefined ? { buckets } : {}),
85
- ...(series !== undefined ? { series } : {}),
86
- ...(seriesName !== undefined ? { seriesName } : {}),
87
- ...(color !== undefined ? { color } : {}),
88
- ...(colors !== undefined ? { colors } : {}),
89
- }
49
+ return value as ChartWidgetConfig
90
50
  }
91
51
 
92
52
  function normalizeChartWidgetType(value: unknown): ChartWidgetType | undefined {
@@ -103,15 +63,6 @@ function normalizeChartWidgetType(value: unknown): ChartWidgetType | undefined {
103
63
  }
104
64
  }
105
65
 
106
- function asChartWidgetConfigRecord(value: unknown): Record<string, unknown> | undefined {
107
- return isRecord(value) ? value : undefined
108
- }
109
-
110
- function getStringField(record: Record<string, unknown>, key: string) {
111
- const value = record[key]
112
- return typeof value === 'string' ? value : undefined
113
- }
114
-
115
66
  function isRecord(value: unknown): value is Record<string, unknown> {
116
67
  return typeof value === 'object' && value !== null
117
68
  }
@@ -18,6 +18,17 @@ export function formatChartValue(value: number, options: Intl.NumberFormatOption
18
18
  return new Intl.NumberFormat(undefined, options).format(value)
19
19
  }
20
20
 
21
+ export function getChartYAxisWidth(values: number[], chartWidth: number) {
22
+ const maxLabelLength = Math.max(
23
+ ...values.map((value) => formatChartValue(value).length),
24
+ 1,
25
+ )
26
+ const estimatedWidth = Math.ceil(maxLabelLength * 6.5) + 18
27
+ const responsiveMaxWidth = Math.max(Math.floor(chartWidth * 0.36), 38)
28
+
29
+ return Math.min(Math.max(estimatedWidth, 38), responsiveMaxWidth, 120)
30
+ }
31
+
21
32
  export function formatChartLabel(value: unknown) {
22
33
  if (typeof value !== 'string') {
23
34
  return String(value)
@@ -48,9 +48,11 @@ const funnelRows = computed(() => {
48
48
  groupedRows.set(label, item)
49
49
  }
50
50
 
51
- return Array.from(groupedRows.values())
52
- .filter((row) => row.value > 0)
53
- .sort((left, right) => right.value - left.value)
51
+ const rows = Array.from(groupedRows.values()).filter((row) => row.value > 0)
52
+
53
+ return props.labelField === 'name'
54
+ ? rows
55
+ : rows.sort((left, right) => right.value - left.value)
54
56
  })
55
57
 
56
58
  const maxValue = computed(() => {
@@ -194,4 +196,4 @@ const segments = computed(() => funnelRows.value.map((row, index) => {
194
196
  </div>
195
197
  </div>
196
198
  </div>
197
- </template>
199
+ </template>
@@ -85,7 +85,14 @@
85
85
  <script setup lang="ts">
86
86
  import { computed } from 'vue'
87
87
  import { useElementSize } from '../../../composables/useElementSize.js'
88
- import { CHART_COLORS, formatChartAxisLabel, formatChartLabel, formatChartValue, toFiniteNumber } from '../chart.utils.js'
88
+ import {
89
+ CHART_COLORS,
90
+ formatChartAxisLabel,
91
+ formatChartLabel,
92
+ formatChartValue,
93
+ getChartYAxisWidth,
94
+ toFiniteNumber,
95
+ } from '../chart.utils.js'
89
96
 
90
97
  const props = withDefaults(defineProps<{
91
98
  rows: Record<string, unknown>[]
@@ -100,12 +107,6 @@ const props = withDefaults(defineProps<{
100
107
 
101
108
  const { el: rootEl, width: rootWidth, height: rootHeight } = useElementSize<HTMLDivElement>()
102
109
 
103
- const padding = {
104
- top: 12,
105
- right: 6,
106
- bottom: 24,
107
- left: 38,
108
- }
109
110
  const chartWidth = computed(() => Math.max(rootWidth.value, 1))
110
111
  const chartHeight = computed(() => {
111
112
  if (rootHeight.value > 0) {
@@ -118,14 +119,21 @@ const chartHeight = computed(() => {
118
119
  const chartColor = computed(() => props.color || CHART_COLORS[0])
119
120
  const values = computed(() => props.rows.map((row) => toFiniteNumber(row[props.yField])))
120
121
  const maxValue = computed(() => Math.max(...values.value, 1))
121
- const innerWidth = computed(() => Math.max(chartWidth.value - padding.left - padding.right, 1))
122
- const innerHeight = computed(() => Math.max(chartHeight.value - padding.top - padding.bottom, 1))
122
+ const yTickValues = computed(() => [maxValue.value, maxValue.value * 0.5, 0])
123
+ const padding = computed(() => ({
124
+ top: 12,
125
+ right: 6,
126
+ bottom: 24,
127
+ left: getChartYAxisWidth(yTickValues.value, chartWidth.value),
128
+ }))
129
+ const innerWidth = computed(() => Math.max(chartWidth.value - padding.value.left - padding.value.right, 1))
130
+ const innerHeight = computed(() => Math.max(chartHeight.value - padding.value.top - padding.value.bottom, 1))
123
131
 
124
132
  const points = computed(() => {
125
133
  if (props.rows.length === 1) {
126
134
  return [{
127
- x: padding.left + innerWidth.value / 2,
128
- y: padding.top + innerHeight.value - (values.value[0] / maxValue.value) * innerHeight.value,
135
+ x: padding.value.left + innerWidth.value / 2,
136
+ y: padding.value.top + innerHeight.value - (values.value[0] / maxValue.value) * innerHeight.value,
129
137
  label: formatChartLabel(props.rows[0][props.xField]),
130
138
  axisLabel: formatChartAxisLabel(props.rows[0][props.xField]),
131
139
  value: values.value[0],
@@ -133,8 +141,8 @@ const points = computed(() => {
133
141
  }
134
142
 
135
143
  return props.rows.map((row, index) => ({
136
- x: padding.left + (index / (props.rows.length - 1)) * innerWidth.value,
137
- y: padding.top + innerHeight.value - (values.value[index] / maxValue.value) * innerHeight.value,
144
+ x: padding.value.left + (index / (props.rows.length - 1)) * innerWidth.value,
145
+ y: padding.value.top + innerHeight.value - (values.value[index] / maxValue.value) * innerHeight.value,
138
146
  label: formatChartLabel(row[props.xField]),
139
147
  axisLabel: formatChartAxisLabel(row[props.xField]),
140
148
  value: values.value[index],
@@ -152,12 +160,12 @@ const fillPath = computed(() => {
152
160
 
153
161
  const first = points.value[0]
154
162
  const last = points.value[points.value.length - 1]
155
- return `${linePath.value} L ${last.x} ${padding.top + innerHeight.value} L ${first.x} ${padding.top + innerHeight.value} Z`
163
+ return `${linePath.value} L ${last.x} ${padding.value.top + innerHeight.value} L ${first.x} ${padding.value.top + innerHeight.value} Z`
156
164
  })
157
165
 
158
166
  const yTicks = computed(() => [0, 0.5, 1].map((ratio) => ({
159
167
  value: maxValue.value * (1 - ratio),
160
- y: padding.top + innerHeight.value * ratio,
168
+ y: padding.value.top + innerHeight.value * ratio,
161
169
  })))
162
170
 
163
171
  const xLabels = computed(() => {