@adminforth/dashboard 1.1.0 → 1.3.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 (55) hide show
  1. package/README.md +43 -52
  2. package/custom/composables/useElementSize.ts +17 -2
  3. package/custom/model/dashboard.types.ts +385 -98
  4. package/custom/runtime/DashboardRuntime.vue +2 -1
  5. package/custom/runtime/WidgetRenderer.vue +2 -1
  6. package/custom/skills/adminforth-dashboard/SKILL.md +8 -4
  7. package/custom/widgets/chart/ChartWidget.vue +36 -35
  8. package/custom/widgets/chart/bar/BarChart.vue +20 -12
  9. package/custom/widgets/chart/chart.types.ts +42 -8
  10. package/custom/widgets/chart/chart.utils.ts +11 -0
  11. package/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
  12. package/custom/widgets/chart/line/LineChart.vue +23 -15
  13. package/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
  14. package/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -43
  15. package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -10
  16. package/custom/widgets/pivot-table/PivotTableWidget.vue +10 -11
  17. package/custom/widgets/table/TableWidget.vue +9 -4
  18. package/dist/custom/composables/useElementSize.js +14 -2
  19. package/dist/custom/composables/useElementSize.ts +17 -2
  20. package/dist/custom/model/dashboard.types.d.ts +179 -38
  21. package/dist/custom/model/dashboard.types.js +108 -42
  22. package/dist/custom/model/dashboard.types.ts +385 -98
  23. package/dist/custom/queries/useDashboardConfig.d.ts +832 -68
  24. package/dist/custom/queries/useWidgetData.d.ts +828 -64
  25. package/dist/custom/runtime/DashboardRuntime.vue +2 -1
  26. package/dist/custom/runtime/WidgetRenderer.vue +2 -1
  27. package/dist/custom/skills/adminforth-dashboard/SKILL.md +8 -4
  28. package/dist/custom/widgets/chart/ChartWidget.vue +36 -35
  29. package/dist/custom/widgets/chart/bar/BarChart.vue +20 -12
  30. package/dist/custom/widgets/chart/chart.types.d.ts +14 -8
  31. package/dist/custom/widgets/chart/chart.types.js +23 -0
  32. package/dist/custom/widgets/chart/chart.types.ts +42 -8
  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 -43
  40. package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -10
  41. package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +10 -11
  42. package/dist/custom/widgets/table/TableWidget.vue +9 -4
  43. package/dist/endpoint/widgets.js +23 -3
  44. package/dist/schema/api.d.ts +2637 -933
  45. package/dist/schema/widget.d.ts +1562 -582
  46. package/dist/schema/widget.js +207 -127
  47. package/dist/services/widgetConfigValidator.js +16 -80
  48. package/dist/services/widgetDataService.d.ts +0 -9
  49. package/dist/services/widgetDataService.js +356 -97
  50. package/endpoint/dashboard.ts +1 -1
  51. package/endpoint/widgets.ts +29 -3
  52. package/package.json +1 -1
  53. package/schema/widget.ts +221 -121
  54. package/services/widgetConfigValidator.ts +29 -100
  55. package/services/widgetDataService.ts +478 -129
@@ -211,6 +211,7 @@ import type {
211
211
  DashboardWidgetConfig,
212
212
  DashboardWidgetMoveDirection,
213
213
  } from '../model/dashboard.types.js'
214
+ import { serializeDashboardWidgetConfigForEditor } from '../model/dashboard.types.js'
214
215
 
215
216
  const props = defineProps<{
216
217
  dashboardSlug: string
@@ -388,7 +389,7 @@ async function removeWidget(widgetId: string) {
388
389
 
389
390
  function editWidget(widget: DashboardWidgetConfig) {
390
391
  editingWidgetId.value = widget.id
391
- widgetConfigCode.value = stringifyYaml(widget)
392
+ widgetConfigCode.value = stringifyYaml(serializeDashboardWidgetConfigForEditor(widget))
392
393
  widgetConfigError.value = ''
393
394
  widgetConfigFieldErrors.value = []
394
395
  }
@@ -26,6 +26,7 @@
26
26
  <script setup lang="ts">
27
27
  import { computed } from 'vue'
28
28
  import type { DashboardWidgetConfig } from '../model/dashboard.types.js'
29
+ import { normalizeChartWidgetConfig } from '../widgets/chart/chart.types.js'
29
30
  import { getWidgetLabel, getWidgetRegistration } from '../widgets/registry.js'
30
31
 
31
32
  const props = defineProps<{
@@ -52,7 +53,7 @@ const widgetTitle = computed(() => {
52
53
  }
53
54
 
54
55
  if (props.widget.target === 'chart') {
55
- return props.widget.chart?.title || 'Untitled chart'
56
+ return normalizeChartWidgetConfig(props.widget.chart)?.title || 'Untitled chart'
56
57
  }
57
58
 
58
59
  return getWidgetLabel(props.widget.target)
@@ -119,7 +119,11 @@ Use the current schema keys exactly:
119
119
 
120
120
  - Use `target`, not `type`.
121
121
  - Use `label`, not `title`.
122
- - Use `query.resource`, not `resourceId`.
123
- - Use `query.select`, not `columns`.
124
- - Use `query.order`, not `sort`.
125
- - Use `query.limit` for row count.
122
+ - Use `query`, not `data_source`.
123
+ - Use `resource`, not `resource_id`.
124
+ - Use `group_by`, not `groupBy`.
125
+ - Use `order_by`, not `orderBy`.
126
+ - Use `page_size`, not `pageSize`.
127
+ - For funnel charts, use `query.steps` as an ordered array of `{ name, resource, metric, filters }` steps.
128
+ - Use `card` for KPI and gauge widget view config.
129
+ - Use `pivot` for pivot table view config.
@@ -26,7 +26,7 @@
26
26
  :rows="rows"
27
27
  :x-field="xField"
28
28
  :y-field="yField"
29
- :series-name="chartConfig.series_name"
29
+ :series-name="lineSeriesName"
30
30
  :color="chartConfig.color"
31
31
  :height="chartHeight"
32
32
  />
@@ -71,7 +71,8 @@
71
71
  v-else-if="chartConfig?.type === 'stacked_bar'"
72
72
  :rows="rows"
73
73
  :x-field="xField"
74
- :series="stackedBarSeries"
74
+ :y-field="yField"
75
+ :series-field="seriesField"
75
76
  :colors="chartConfig.colors"
76
77
  :height="chartHeight"
77
78
  />
@@ -91,13 +92,14 @@
91
92
  import { computed, watch } from 'vue'
92
93
  import { useWidgetData } from '../../queries/useWidgetData.js'
93
94
  import type { DashboardWidgetConfig, DashboardWidgetTableData } from '../../model/dashboard.types.js'
95
+ import { normalizeChartWidgetConfig } from './chart.types.js'
94
96
  import BarChart from './bar/BarChart.vue'
95
97
  import FunnelChart from './funnel/FunnelChart.vue'
96
98
  import HistogramChart from './histogram/HistogramChart.vue'
97
99
  import LineChart from './line/LineChart.vue'
98
100
  import PieChart from './pie/PieChart.vue'
99
101
  import StackedBarChart from './stacked-bar/StackedBarChart.vue'
100
- import { formatChartLabel, toFiniteNumber } from './chart.utils.js'
102
+ import { toFiniteNumber } from './chart.utils.js'
101
103
 
102
104
  const DEFAULT_WIDGET_HEIGHT = 500
103
105
 
@@ -126,31 +128,37 @@ watch(
126
128
  const chartData = computed(() => data.value?.data as DashboardWidgetTableData | null)
127
129
  const rows = computed(() => chartData.value?.rows ?? [])
128
130
  const columns = computed(() => chartData.value?.columns ?? [])
129
- const chartConfig = computed(() => props.widget.chart)
130
- const xField = computed(() => chartConfig.value?.x_field || columns.value[0])
131
- const yField = computed(() => chartConfig.value?.y_field || columns.value[1])
132
- const labelField = computed(() => chartConfig.value?.label_field || columns.value[0])
133
- const valueField = computed(() => chartConfig.value?.value_field || columns.value[1])
134
- const pieRows = computed(() => {
135
- if (chartConfig.value?.value_field) {
136
- return rows.value
137
- }
131
+ const chartConfig = computed(() => normalizeChartWidgetConfig(props.widget.chart))
132
+
133
+ function resolveChartDimensionField(field: string | undefined, fallbackField: string | undefined) {
134
+ const resolvedField = field ?? fallbackField
138
135
 
139
- const groupedRows = new Map<string, { label: string, value: number }>()
136
+ if (!resolvedField) {
137
+ return ''
138
+ }
140
139
 
141
- for (const row of rows.value) {
142
- const label = formatChartLabel(row[labelField.value])
143
- const item = groupedRows.get(label) ?? { label, value: 0 }
144
- item.value += 1
145
- groupedRows.set(label, item)
140
+ if (columns.value.includes(resolvedField)) {
141
+ return resolvedField
146
142
  }
147
143
 
148
- return Array.from(groupedRows.values())
144
+ return resolvedField
145
+ }
146
+
147
+ const firstYField = computed(() => {
148
+ const y = chartConfig.value?.y
149
+ return Array.isArray(y) ? y[0]?.field : y?.field
149
150
  })
150
- const pieLabelField = computed(() => chartConfig.value?.value_field ? labelField.value : 'label')
151
- const pieValueField = computed(() => chartConfig.value?.value_field ? valueField.value : 'value')
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)
152
158
  const barRows = computed(() => {
153
- const bucketField = chartConfig.value?.bucket_field
159
+ const bucketField = chartConfig.value?.type === 'histogram'
160
+ ? chartConfig.value.x?.field
161
+ : undefined
154
162
 
155
163
  if (!bucketField) {
156
164
  return rows.value
@@ -167,19 +175,12 @@ const barRows = computed(() => {
167
175
  }).length,
168
176
  }))
169
177
  })
170
- const barLabelField = computed(() => chartConfig.value?.bucket_field ? 'label' : labelField.value)
171
- const barValueField = computed(() => chartConfig.value?.bucket_field ? 'count' : valueField.value)
172
- const stackedBarSeries = computed(() => {
173
- if (chartConfig.value?.series?.length) {
174
- return chartConfig.value.series
175
- }
176
-
177
- return columns.value
178
- .filter((column) => column !== xField.value)
179
- .map((column) => ({
180
- name: column,
181
- field: column,
182
- }))
178
+ const barLabelField = computed(() => chartConfig.value?.type === 'histogram' && chartConfig.value.buckets ? 'label' : xField.value)
179
+ const barValueField = computed(() => chartConfig.value?.type === 'histogram' && chartConfig.value.buckets ? 'count' : yField.value)
180
+ const seriesField = computed(() => chartConfig.value?.series?.field || columns.value[2] || '')
181
+ const lineSeriesName = computed(() => {
182
+ const y = chartConfig.value?.y
183
+ return Array.isArray(y) ? y[0]?.label : undefined
183
184
  })
184
185
 
185
186
  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,25 +1,31 @@
1
+ import type { ValueFormat } from '../../model/dashboard.types.js';
1
2
  export type ChartWidgetType = 'line' | 'pie' | 'bar' | 'stacked_bar' | 'funnel' | 'histogram';
2
3
  export type ChartWidgetBucketConfig = {
3
4
  label: string;
4
5
  min?: number;
5
6
  max?: number;
6
7
  };
8
+ export type ChartFieldRef = {
9
+ field: string;
10
+ label?: string;
11
+ format?: ValueFormat;
12
+ };
7
13
  export type ChartWidgetSeriesConfig = {
8
- name: string;
9
14
  field: string;
15
+ label?: string;
10
16
  color?: string;
11
17
  };
12
18
  export type ChartWidgetConfig = {
13
19
  type: ChartWidgetType;
14
20
  title?: string;
15
- x_field?: string;
16
- y_field?: string;
17
- label_field?: string;
18
- value_field?: string;
19
- bucket_field?: string;
21
+ x?: ChartFieldRef;
22
+ y?: ChartFieldRef | ChartFieldRef[];
23
+ label?: ChartFieldRef;
24
+ value?: ChartFieldRef;
20
25
  buckets?: ChartWidgetBucketConfig[];
21
- series?: ChartWidgetSeriesConfig[];
22
- series_name?: string;
26
+ series?: ChartWidgetSeriesConfig;
23
27
  color?: string;
24
28
  colors?: string[];
25
29
  };
30
+ export type NormalizedChartWidgetConfig = ChartWidgetConfig;
31
+ export declare function normalizeChartWidgetConfig(value: unknown): NormalizedChartWidgetConfig | undefined;
@@ -1,2 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeChartWidgetConfig = normalizeChartWidgetConfig;
4
+ function normalizeChartWidgetConfig(value) {
5
+ if (!isRecord(value) || !normalizeChartWidgetType(value.type)) {
6
+ return undefined;
7
+ }
8
+ return value;
9
+ }
10
+ function normalizeChartWidgetType(value) {
11
+ switch (value) {
12
+ case 'line':
13
+ case 'pie':
14
+ case 'bar':
15
+ case 'stacked_bar':
16
+ case 'funnel':
17
+ case 'histogram':
18
+ return value;
19
+ default:
20
+ return undefined;
21
+ }
22
+ }
23
+ function isRecord(value) {
24
+ return typeof value === 'object' && value !== null;
25
+ }
@@ -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,23 +14,55 @@ 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
  }
41
+
42
+ export type NormalizedChartWidgetConfig = ChartWidgetConfig
43
+
44
+ export function normalizeChartWidgetConfig(value: unknown): NormalizedChartWidgetConfig | undefined {
45
+ if (!isRecord(value) || !normalizeChartWidgetType(value.type)) {
46
+ return undefined
47
+ }
48
+
49
+ return value as ChartWidgetConfig
50
+ }
51
+
52
+ function normalizeChartWidgetType(value: unknown): ChartWidgetType | undefined {
53
+ switch (value) {
54
+ case 'line':
55
+ case 'pie':
56
+ case 'bar':
57
+ case 'stacked_bar':
58
+ case 'funnel':
59
+ case 'histogram':
60
+ return value
61
+ default:
62
+ return undefined
63
+ }
64
+ }
65
+
66
+ function isRecord(value: unknown): value is Record<string, unknown> {
67
+ return typeof value === 'object' && value !== null
68
+ }
@@ -1,5 +1,6 @@
1
1
  export declare const CHART_COLORS: string[];
2
2
  export declare function toFiniteNumber(value: unknown): number;
3
3
  export declare function formatChartValue(value: number, options?: Intl.NumberFormatOptions): string;
4
+ export declare function getChartYAxisWidth(values: number[], chartWidth: number): number;
4
5
  export declare function formatChartLabel(value: unknown): string;
5
6
  export declare function formatChartAxisLabel(value: unknown, maxLength?: number): string;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CHART_COLORS = void 0;
4
4
  exports.toFiniteNumber = toFiniteNumber;
5
5
  exports.formatChartValue = formatChartValue;
6
+ exports.getChartYAxisWidth = getChartYAxisWidth;
6
7
  exports.formatChartLabel = formatChartLabel;
7
8
  exports.formatChartAxisLabel = formatChartAxisLabel;
8
9
  exports.CHART_COLORS = [
@@ -22,6 +23,12 @@ function toFiniteNumber(value) {
22
23
  function formatChartValue(value, options = {}) {
23
24
  return new Intl.NumberFormat(undefined, options).format(value);
24
25
  }
26
+ function getChartYAxisWidth(values, chartWidth) {
27
+ const maxLabelLength = Math.max(...values.map((value) => formatChartValue(value).length), 1);
28
+ const estimatedWidth = Math.ceil(maxLabelLength * 6.5) + 18;
29
+ const responsiveMaxWidth = Math.max(Math.floor(chartWidth * 0.36), 38);
30
+ return Math.min(Math.max(estimatedWidth, 38), responsiveMaxWidth, 120);
31
+ }
25
32
  function formatChartLabel(value) {
26
33
  if (typeof value !== 'string') {
27
34
  return String(value);
@@ -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(() => {
@@ -1,13 +1,20 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from 'vue'
3
3
  import { useElementSize } from '../../../composables/useElementSize.js'
4
- import type { ChartWidgetSeriesConfig } from '../chart.types.js'
5
- import { CHART_COLORS, formatChartAxisLabel, formatChartLabel, formatChartValue, toFiniteNumber } from '../chart.utils.js'
4
+ import {
5
+ CHART_COLORS,
6
+ formatChartAxisLabel,
7
+ formatChartLabel,
8
+ formatChartValue,
9
+ getChartYAxisWidth,
10
+ toFiniteNumber,
11
+ } from '../chart.utils.js'
6
12
 
7
13
  const props = withDefaults(defineProps<{
8
14
  rows: Record<string, unknown>[]
9
15
  xField: string
10
- series: ChartWidgetSeriesConfig[]
16
+ yField: string
17
+ seriesField: string
11
18
  colors?: string[]
12
19
  height?: number
13
20
  }>(), {
@@ -17,16 +24,11 @@ const props = withDefaults(defineProps<{
17
24
  const { el: rootEl, width: rootWidth } = useElementSize<HTMLDivElement>()
18
25
  const { el: svgEl, width: svgWidth, height: svgHeight } = useElementSize<HTMLDivElement>()
19
26
 
20
- const padding = {
21
- top: 24,
22
- right: 6,
23
- bottom: 34,
24
- left: 38,
25
- }
26
27
  const barGap = 10
27
- const normalizedSeries = computed(() => props.series.map((series, index) => ({
28
- ...series,
29
- color: series.color || props.colors?.[index] || CHART_COLORS[index % CHART_COLORS.length],
28
+ const seriesNames = computed(() => Array.from(new Set(props.rows.map((row) => formatChartLabel(row[props.seriesField])))))
29
+ const normalizedSeries = computed(() => seriesNames.value.map((name, index) => ({
30
+ name,
31
+ color: props.colors?.[index] || CHART_COLORS[index % CHART_COLORS.length],
30
32
  })))
31
33
  const showLegend = computed(() => normalizedSeries.value.length > 0)
32
34
  const isCompact = computed(() => rootWidth.value > 0 && rootWidth.value < 420)
@@ -38,19 +40,15 @@ const chartHeight = computed(() => {
38
40
 
39
41
  return Math.max(props.height - (showLegend.value ? 28 : 0), 96)
40
42
  })
41
- const innerWidth = computed(() => Math.max(chartWidth.value - padding.left - padding.right, 1))
42
- const innerHeight = computed(() => Math.max(chartHeight.value - padding.top - padding.bottom, 1))
43
43
  const groupedRows = computed(() => {
44
44
  const grouped = new Map<string, Record<string, unknown>>()
45
45
 
46
46
  for (const row of props.rows) {
47
47
  const label = formatChartLabel(row[props.xField])
48
48
  const item = grouped.get(label) ?? { [props.xField]: label }
49
+ const seriesName = formatChartLabel(row[props.seriesField])
49
50
 
50
- for (const series of normalizedSeries.value) {
51
- item[series.name] = toFiniteNumber(item[series.name])
52
- + getSeriesContribution(row[series.field], series.name)
53
- }
51
+ item[seriesName] = toFiniteNumber(item[seriesName]) + toFiniteNumber(row[props.yField])
54
52
 
55
53
  grouped.set(label, item)
56
54
  }
@@ -65,15 +63,24 @@ const totalChartWidth = computed(() => {
65
63
  const count = Math.max(groupedRows.value.length, 1)
66
64
  return count * barWidth.value + (count - 1) * barGap
67
65
  })
68
- const chartStartX = computed(() => padding.left + Math.max((innerWidth.value - totalChartWidth.value) / 2, 0))
66
+ const chartStartX = computed(() => padding.value.left + Math.max((innerWidth.value - totalChartWidth.value) / 2, 0))
69
67
  const totals = computed(() => groupedRows.value.map((row) => normalizedSeries.value.reduce(
70
68
  (sum, series) => sum + toFiniteNumber(row[series.name]),
71
69
  0,
72
70
  )))
73
71
  const maxTotal = computed(() => Math.max(...totals.value, 1))
72
+ const yTickValues = computed(() => [maxTotal.value, maxTotal.value * 0.5, 0])
73
+ const padding = computed(() => ({
74
+ top: 24,
75
+ right: 6,
76
+ bottom: 34,
77
+ left: getChartYAxisWidth(yTickValues.value, chartWidth.value),
78
+ }))
79
+ const innerWidth = computed(() => Math.max(chartWidth.value - padding.value.left - padding.value.right, 1))
80
+ const innerHeight = computed(() => Math.max(chartHeight.value - padding.value.top - padding.value.bottom, 1))
74
81
 
75
82
  const bars = computed(() => groupedRows.value.map((row, rowIndex) => {
76
- let y = padding.top + innerHeight.value
83
+ let y = padding.value.top + innerHeight.value
77
84
 
78
85
  return {
79
86
  label: String(row[props.xField]),
@@ -118,31 +125,9 @@ const visibleLabelIndexes = computed(() => {
118
125
 
119
126
  const yTicks = computed(() => [0, 0.5, 1].map((ratio) => ({
120
127
  value: maxTotal.value * (1 - ratio),
121
- y: padding.top + innerHeight.value * ratio,
128
+ y: padding.value.top + innerHeight.value * ratio,
122
129
  })))
123
130
 
124
- function getSeriesContribution(value: unknown, seriesName: string) {
125
- if (typeof value === 'boolean') {
126
- return value === getBooleanSeriesValue(seriesName) ? 1 : 0
127
- }
128
-
129
- if (typeof value === 'string' && ['true', 'false'].includes(value.toLowerCase())) {
130
- return (value.toLowerCase() === 'true') === getBooleanSeriesValue(seriesName) ? 1 : 0
131
- }
132
-
133
- return toFiniteNumber(value)
134
- }
135
-
136
- function getBooleanSeriesValue(seriesName: string) {
137
- const normalizedName = seriesName.toLowerCase()
138
-
139
- if (normalizedName.includes('unlisted') || normalizedName.includes('false')) {
140
- return false
141
- }
142
-
143
- return true
144
- }
145
-
146
131
  function getBarTooltip(bar: { label: string, total: number, segments: Array<{ name: string, value: number }> }) {
147
132
  const percentFormatter = new Intl.NumberFormat(undefined, { maximumFractionDigits: 1 })
148
133