@protobase/ui 0.22.6 → 0.22.7
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.
- package/dist/app/charts/chart-groups.js +1 -1
- package/dist/app/charts/chart-view.js +30 -5
- package/dist/app/charts/group-donut.js +15 -25
- package/dist/app/charts/legend-item.js +33 -0
- package/dist/app/charts/period-chart.js +69 -22
- package/dist/app/charts/split-points.js +52 -0
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-bQTchBn5.js +69 -0
- package/dist/shell/assets/{index-q2Lohp8c.js → index-Bo6QYFeS.js} +1 -1
- package/dist/shell/assets/{maplibre-L-NOV9Ww.js → maplibre-D6Uec36I.js} +1 -1
- package/dist/shell/assets/{markdown-content-CM_VWSfv.js → markdown-content-BJX69D8o.js} +1 -1
- package/dist/shell/assets/{record-map-CElcOkuO.js → record-map-BkrYRpgN.js} +1 -1
- package/dist/shell/assets/{records-map-pk9uGJBc.js → records-map-KUezCU26.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-DvT94uLD.js → shared-protobase-ui-BD0VhagU.js} +1 -1
- package/dist/shell/index.html +3 -3
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +4 -4
- package/dist/shell/assets/app-Bp_H43C6.js +0 -69
|
@@ -49,4 +49,4 @@ const measureText = (value, measure) => {
|
|
|
49
49
|
return `${measure.hints?.prefix ?? ""}${text}${measure.hints?.suffix ?? ""}`;
|
|
50
50
|
};
|
|
51
51
|
//#endregion
|
|
52
|
-
export { groupFilter, measureText, periodPoints, valueSlices };
|
|
52
|
+
export { colorOf, groupFilter, labelOf, measureText, periodPoints, valueSlices };
|
|
@@ -7,6 +7,7 @@ import { periodPoints, valueSlices } from "./chart-groups.js";
|
|
|
7
7
|
import { GroupBars } from "./group-bars.js";
|
|
8
8
|
import { GroupDonut } from "./group-donut.js";
|
|
9
9
|
import { PeriodChart } from "./period-chart.js";
|
|
10
|
+
import { splitPoints } from "./split-points.js";
|
|
10
11
|
import { jsx } from "react/jsx-runtime";
|
|
11
12
|
//#region src/app/charts/chart-view.tsx
|
|
12
13
|
const measureOf = (model, view, sum, average) => {
|
|
@@ -19,20 +20,31 @@ const measureOf = (model, view, sum, average) => {
|
|
|
19
20
|
hints: view?.fields[name]
|
|
20
21
|
};
|
|
21
22
|
};
|
|
23
|
+
const labelOf = (field, resources, views) => {
|
|
24
|
+
const target = field?.type === "relation" && field.relation ? resources[field.relation.resource] : void 0;
|
|
25
|
+
return target && target.primaryKey.length === 1 ? labelFieldOf(target, views[target.name]) : void 0;
|
|
26
|
+
};
|
|
22
27
|
/**
|
|
23
28
|
* Loads a chart's groups through `:aggregate` and draws them: a date field's buckets as bars or a line, a field's
|
|
24
29
|
* values as ranked bars or a donut, each value linking to its records. The server checked the chart at startup.
|
|
25
30
|
*/
|
|
26
|
-
const ChartView = ({ model, view, groupBy, type = "bar", sum, average, filter = "", granularity = "day", range = "30d", limit = 10, empty = "Nothing to chart yet." }) => {
|
|
31
|
+
const ChartView = ({ model, view, groupBy, splitBy, type = "bar", sum, average, filter = "", granularity = "day", range = "30d", limit = 10, empty = "Nothing to chart yet." }) => {
|
|
27
32
|
const { resources, views } = useAdminMeta();
|
|
28
33
|
const field = model.fields[groupBy];
|
|
29
34
|
const dated = field?.type === "date" || field?.type === "timestamp";
|
|
30
|
-
const
|
|
31
|
-
const
|
|
35
|
+
const label = labelOf(field, resources, views);
|
|
36
|
+
const splitField = dated && splitBy !== void 0 ? model.fields[splitBy] : void 0;
|
|
37
|
+
const splitLabel = labelOf(splitField, resources, views);
|
|
38
|
+
const split = splitField ? {
|
|
39
|
+
splitBy: splitField.name,
|
|
40
|
+
splitLimit: 8,
|
|
41
|
+
...splitLabel && { splitLabel }
|
|
42
|
+
} : {};
|
|
32
43
|
const params = dated ? {
|
|
33
44
|
granularity,
|
|
34
45
|
range,
|
|
35
|
-
timeZone: viewerZone()
|
|
46
|
+
timeZone: viewerZone(),
|
|
47
|
+
...split
|
|
36
48
|
} : {
|
|
37
49
|
limit,
|
|
38
50
|
...label && { label }
|
|
@@ -56,11 +68,24 @@ const ChartView = ({ model, view, groupBy, type = "bar", sum, average, filter =
|
|
|
56
68
|
className: "text-muted-foreground",
|
|
57
69
|
children: empty
|
|
58
70
|
});
|
|
71
|
+
const chartType = type === "line" ? "line" : "bar";
|
|
72
|
+
if (dated && splitField && result.data.splits) {
|
|
73
|
+
const { series, points } = splitPoints(groups, result.data.splits, splitField, view?.fields[splitField.name], measure, filter);
|
|
74
|
+
return /* @__PURE__ */ jsx(PeriodChart, {
|
|
75
|
+
points,
|
|
76
|
+
measure,
|
|
77
|
+
bucket: granularity,
|
|
78
|
+
type: chartType,
|
|
79
|
+
series,
|
|
80
|
+
resource: model.name,
|
|
81
|
+
view
|
|
82
|
+
});
|
|
83
|
+
}
|
|
59
84
|
if (dated) return /* @__PURE__ */ jsx(PeriodChart, {
|
|
60
85
|
points: periodPoints(groups, measure),
|
|
61
86
|
measure,
|
|
62
87
|
bucket: granularity,
|
|
63
|
-
type:
|
|
88
|
+
type: chartType
|
|
64
89
|
});
|
|
65
90
|
const slices = valueSlices(groups, field, view?.fields[groupBy], measure, filter);
|
|
66
91
|
return type === "donut" ? /* @__PURE__ */ jsx(GroupDonut, {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { AppLink } from "../use-app-address.js";
|
|
2
1
|
import { measureText } from "./chart-groups.js";
|
|
3
|
-
import {
|
|
2
|
+
import { LegendItem } from "./legend-item.js";
|
|
4
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
4
|
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts";
|
|
6
5
|
//#region src/app/charts/group-donut.tsx
|
|
@@ -51,29 +50,20 @@ const GroupDonut = ({ resource, view, slices, measure }) => {
|
|
|
51
50
|
})
|
|
52
51
|
}), /* @__PURE__ */ jsx("ul", {
|
|
53
52
|
className: "flex w-full min-w-0 flex-col gap-0.5",
|
|
54
|
-
children: slices.map((slice) => /* @__PURE__ */
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
className: "font-medium tabular-nums",
|
|
69
|
-
children: measureText(slice.value, measure)
|
|
70
|
-
}),
|
|
71
|
-
/* @__PURE__ */ jsx("span", {
|
|
72
|
-
className: "w-9 text-right text-xs text-muted-foreground tabular-nums",
|
|
73
|
-
children: percent(slice.value ?? 0, whole)
|
|
74
|
-
})
|
|
75
|
-
]
|
|
76
|
-
}) }, slice.key))
|
|
53
|
+
children: slices.map((slice) => /* @__PURE__ */ jsxs(LegendItem, {
|
|
54
|
+
resource,
|
|
55
|
+
view,
|
|
56
|
+
filter: slice.filter,
|
|
57
|
+
color: slice.color,
|
|
58
|
+
label: slice.label,
|
|
59
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
60
|
+
className: "font-medium tabular-nums",
|
|
61
|
+
children: measureText(slice.value, measure)
|
|
62
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
63
|
+
className: "w-9 text-right text-xs text-muted-foreground tabular-nums",
|
|
64
|
+
children: percent(slice.value ?? 0, whole)
|
|
65
|
+
})]
|
|
66
|
+
}, slice.key))
|
|
77
67
|
})]
|
|
78
68
|
});
|
|
79
69
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { AppLink } from "../use-app-address.js";
|
|
2
|
+
import { filteredListAddress } from "../list-link.js";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/app/charts/legend-item.tsx
|
|
5
|
+
const row = "flex items-center gap-2 rounded-md px-1 py-0.5";
|
|
6
|
+
/**
|
|
7
|
+
* One row of a chart legend: a colour dot and the value's name, with `children` after it. With a `filter` the row links to
|
|
8
|
+
* that value's records; without, as for the rest of a split, it is plain text.
|
|
9
|
+
*/
|
|
10
|
+
const LegendItem = ({ resource, view, filter, color, label, children }) => {
|
|
11
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
12
|
+
/* @__PURE__ */ jsx("span", {
|
|
13
|
+
className: "size-2.5 shrink-0 rounded-full",
|
|
14
|
+
style: { background: color },
|
|
15
|
+
"aria-hidden": true
|
|
16
|
+
}),
|
|
17
|
+
/* @__PURE__ */ jsx("span", {
|
|
18
|
+
className: "min-w-0 flex-1 truncate group-hover:text-primary-text",
|
|
19
|
+
children: label
|
|
20
|
+
}),
|
|
21
|
+
children
|
|
22
|
+
] });
|
|
23
|
+
return /* @__PURE__ */ jsx("li", { children: filter === void 0 ? /* @__PURE__ */ jsx("div", {
|
|
24
|
+
className: row,
|
|
25
|
+
children: content
|
|
26
|
+
}) : /* @__PURE__ */ jsx(AppLink, {
|
|
27
|
+
to: filteredListAddress(view, resource, filter),
|
|
28
|
+
className: `group ${row} hover:bg-muted`,
|
|
29
|
+
children: content
|
|
30
|
+
}) });
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
export { LegendItem };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useMediaQuery } from "../../lib/use-media-query.js";
|
|
2
2
|
import { measureText } from "./chart-groups.js";
|
|
3
|
+
import { LegendItem } from "./legend-item.js";
|
|
3
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
5
|
import { Bar, BarChart, CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
|
5
6
|
//#region src/app/charts/period-chart.tsx
|
|
@@ -58,22 +59,47 @@ const axisTick = {
|
|
|
58
59
|
fontSize: 11,
|
|
59
60
|
fill: "var(--muted-foreground)"
|
|
60
61
|
};
|
|
61
|
-
const PointTooltip = ({ active, payload, measure, bucket }) => {
|
|
62
|
+
const PointTooltip = ({ active, payload, measure, bucket, series }) => {
|
|
62
63
|
const point = payload?.[0]?.payload;
|
|
63
64
|
if (!active || !point) return null;
|
|
64
65
|
return /* @__PURE__ */ jsxs("div", {
|
|
65
66
|
className: "rounded-md border bg-background px-2.5 py-1.5 text-xs shadow-pop",
|
|
66
|
-
children: [
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
children: [
|
|
68
|
+
/* @__PURE__ */ jsx("div", {
|
|
69
|
+
className: "text-muted-foreground",
|
|
70
|
+
children: (bucket === "week" ? "Week of " : "") + formatterOf(tooltipFormats[bucket])(point.at)
|
|
71
|
+
}),
|
|
72
|
+
series?.map((part) => /* @__PURE__ */ jsxs("div", {
|
|
73
|
+
className: "flex items-center gap-2",
|
|
74
|
+
children: [
|
|
75
|
+
/* @__PURE__ */ jsx("span", {
|
|
76
|
+
className: "size-2 shrink-0 rounded-full",
|
|
77
|
+
style: { background: part.color },
|
|
78
|
+
"aria-hidden": true
|
|
79
|
+
}),
|
|
80
|
+
/* @__PURE__ */ jsx("span", {
|
|
81
|
+
className: "flex-1",
|
|
82
|
+
children: part.label
|
|
83
|
+
}),
|
|
84
|
+
/* @__PURE__ */ jsx("span", {
|
|
85
|
+
className: "font-semibold tabular-nums",
|
|
86
|
+
children: measureText(point[part.key] ?? null, measure)
|
|
87
|
+
})
|
|
88
|
+
]
|
|
89
|
+
}, part.key)),
|
|
90
|
+
series && measure.kind !== "average" && /* @__PURE__ */ jsx("div", {
|
|
91
|
+
className: "mt-0.5 border-t pt-0.5 font-semibold tabular-nums",
|
|
92
|
+
children: measureText(point.value, measure)
|
|
93
|
+
}),
|
|
94
|
+
!series && /* @__PURE__ */ jsx("div", {
|
|
95
|
+
className: "font-semibold tabular-nums",
|
|
96
|
+
children: measureText(point.value, measure)
|
|
97
|
+
})
|
|
98
|
+
]
|
|
73
99
|
});
|
|
74
100
|
};
|
|
75
101
|
/** A date field's buckets over time, as bars or a line; a bucket without a value to add up is a gap. */
|
|
76
|
-
const PeriodChart = ({ points, measure, bucket, type }) => {
|
|
102
|
+
const PeriodChart = ({ points, measure, bucket, type, series, resource, view }) => {
|
|
77
103
|
const wide = useMediaQuery("(min-width: 640px)");
|
|
78
104
|
const tick = formatterOf(formats[bucket]);
|
|
79
105
|
const grid = /* @__PURE__ */ jsx(CartesianGrid, {
|
|
@@ -101,11 +127,17 @@ const PeriodChart = ({ points, measure, bucket, type }) => {
|
|
|
101
127
|
cursor: type === "bar" ? { fill: "var(--muted)" } : { stroke: "var(--border-strong)" },
|
|
102
128
|
content: /* @__PURE__ */ jsx(PointTooltip, {
|
|
103
129
|
measure,
|
|
104
|
-
bucket
|
|
130
|
+
bucket,
|
|
131
|
+
...series && { series }
|
|
105
132
|
}),
|
|
106
133
|
isAnimationActive: false
|
|
107
134
|
});
|
|
108
|
-
|
|
135
|
+
const parts = series ?? [{
|
|
136
|
+
key: "value",
|
|
137
|
+
label: "",
|
|
138
|
+
color: "var(--chart-1)"
|
|
139
|
+
}];
|
|
140
|
+
const chart = /* @__PURE__ */ jsx("div", {
|
|
109
141
|
className: "h-40",
|
|
110
142
|
children: /* @__PURE__ */ jsx(ResponsiveContainer, {
|
|
111
143
|
width: "100%",
|
|
@@ -123,13 +155,13 @@ const PeriodChart = ({ points, measure, bucket, type }) => {
|
|
|
123
155
|
x,
|
|
124
156
|
y,
|
|
125
157
|
tooltip,
|
|
126
|
-
/* @__PURE__ */ jsx(Line, {
|
|
127
|
-
dataKey:
|
|
128
|
-
stroke:
|
|
158
|
+
parts.map((part) => /* @__PURE__ */ jsx(Line, {
|
|
159
|
+
dataKey: part.key,
|
|
160
|
+
stroke: part.color,
|
|
129
161
|
strokeWidth: 2,
|
|
130
162
|
dot: points.length <= 31 ? {
|
|
131
163
|
r: 3,
|
|
132
|
-
fill:
|
|
164
|
+
fill: part.color,
|
|
133
165
|
strokeWidth: 0
|
|
134
166
|
} : false,
|
|
135
167
|
activeDot: {
|
|
@@ -138,7 +170,7 @@ const PeriodChart = ({ points, measure, bucket, type }) => {
|
|
|
138
170
|
strokeWidth: 2
|
|
139
171
|
},
|
|
140
172
|
isAnimationActive: false
|
|
141
|
-
})
|
|
173
|
+
}, part.key))
|
|
142
174
|
]
|
|
143
175
|
}) : /* @__PURE__ */ jsxs(BarChart, {
|
|
144
176
|
data: points,
|
|
@@ -154,21 +186,36 @@ const PeriodChart = ({ points, measure, bucket, type }) => {
|
|
|
154
186
|
x,
|
|
155
187
|
y,
|
|
156
188
|
tooltip,
|
|
157
|
-
/* @__PURE__ */ jsx(Bar, {
|
|
158
|
-
dataKey:
|
|
159
|
-
fill:
|
|
160
|
-
|
|
189
|
+
parts.map((part, index) => /* @__PURE__ */ jsx(Bar, {
|
|
190
|
+
dataKey: part.key,
|
|
191
|
+
fill: part.color,
|
|
192
|
+
...series && { stackId: "split" },
|
|
193
|
+
radius: index === parts.length - 1 ? [
|
|
161
194
|
4,
|
|
162
195
|
4,
|
|
163
196
|
0,
|
|
164
197
|
0
|
|
165
|
-
],
|
|
198
|
+
] : 0,
|
|
166
199
|
isAnimationActive: false
|
|
167
|
-
})
|
|
200
|
+
}, part.key))
|
|
168
201
|
]
|
|
169
202
|
})
|
|
170
203
|
})
|
|
171
204
|
});
|
|
205
|
+
if (!series || resource === void 0) return chart;
|
|
206
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
207
|
+
className: "flex flex-col gap-2",
|
|
208
|
+
children: [chart, /* @__PURE__ */ jsx("ul", {
|
|
209
|
+
className: "flex flex-wrap gap-x-3 gap-y-0.5 text-sm",
|
|
210
|
+
children: series.map((part) => /* @__PURE__ */ jsx(LegendItem, {
|
|
211
|
+
resource,
|
|
212
|
+
view,
|
|
213
|
+
color: part.color,
|
|
214
|
+
label: part.label,
|
|
215
|
+
...part.filter !== void 0 && { filter: part.filter }
|
|
216
|
+
}, part.key))
|
|
217
|
+
})]
|
|
218
|
+
});
|
|
172
219
|
};
|
|
173
220
|
//#endregion
|
|
174
221
|
export { PeriodChart };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { colorOf, groupFilter, labelOf, periodPoints } from "./chart-groups.js";
|
|
2
|
+
//#region src/app/charts/split-points.ts
|
|
3
|
+
const noise = 1e-6;
|
|
4
|
+
/**
|
|
5
|
+
* A date field's buckets with each split into one part per value of another field, in the order the API ranks them.
|
|
6
|
+
* A bucket where a value has no records holds 0 of a count and no value of a sum or average. For a count or a sum, what
|
|
7
|
+
* the kept values leave of the bucket's total is "Other", a series of its own when some bucket has any; an average
|
|
8
|
+
* has no remainder.
|
|
9
|
+
*/
|
|
10
|
+
const splitPoints = (groups, splits, field, hints, measure, filter = "") => {
|
|
11
|
+
const series = splits.map((split, index) => ({
|
|
12
|
+
key: `s${index}`,
|
|
13
|
+
label: labelOf(field, hints, split),
|
|
14
|
+
color: colorOf(field, hints, split.value, index),
|
|
15
|
+
filter: groupFilter(field, split.value, filter)
|
|
16
|
+
}));
|
|
17
|
+
const missing = measure.kind === "count" ? 0 : null;
|
|
18
|
+
const partOf = (group, value) => {
|
|
19
|
+
const part = group.splits?.find((candidate) => candidate.value === value);
|
|
20
|
+
if (!part) return missing;
|
|
21
|
+
return measure.kind === "count" ? part.count : part[measure.kind] ?? null;
|
|
22
|
+
};
|
|
23
|
+
const points = periodPoints(groups, measure).map((point, at) => {
|
|
24
|
+
const group = groups[at];
|
|
25
|
+
const parts = splits.map((split) => partOf(group, split.value));
|
|
26
|
+
const entries = Object.fromEntries(parts.map((part, index) => [`s${index}`, part]));
|
|
27
|
+
if (measure.kind === "average") return {
|
|
28
|
+
...point,
|
|
29
|
+
...entries
|
|
30
|
+
};
|
|
31
|
+
const rest = point.value === null ? null : point.value - parts.reduce((all, part) => all + (part ?? 0), 0);
|
|
32
|
+
return {
|
|
33
|
+
...point,
|
|
34
|
+
...entries,
|
|
35
|
+
other: rest === null ? null : rest < noise ? 0 : rest
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
if (!(measure.kind !== "average" && points.some((point) => (point.other ?? 0) > 0))) return {
|
|
39
|
+
series,
|
|
40
|
+
points: points.map(({ other, ...point }) => point)
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
series: [...series, {
|
|
44
|
+
key: "other",
|
|
45
|
+
label: "Other",
|
|
46
|
+
color: "var(--border-strong)"
|
|
47
|
+
}],
|
|
48
|
+
points
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
//#endregion
|
|
52
|
+
export { splitPoints };
|
package/dist/package.js
CHANGED