@bluemarble/bi 0.6.2 → 0.6.4
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/{DashboardDevtools-BT9mJrmJ.js → DashboardDevtools-CSRt9lib.js} +16 -6
- package/dist/DashboardDevtools-CSRt9lib.js.map +1 -0
- package/dist/{ModelDictionary-BlDeDyAE.js → ModelDictionary-Dib4Vk3i.js} +63 -9
- package/dist/ModelDictionary-Dib4Vk3i.js.map +1 -0
- package/dist/client/index.d.ts +24 -6
- package/dist/client/index.js +213 -57
- package/dist/client/index.js.map +1 -1
- package/dist/{layout-B7eYgNyR.d.ts → layout-CoGyGWYM.d.ts} +46 -5
- package/dist/{layout-DAFeHIuP.js → layout-nViz3MdU.js} +31 -3
- package/dist/layout-nViz3MdU.js.map +1 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +162 -5
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
- package/dist/DashboardDevtools-BT9mJrmJ.js.map +0 -1
- package/dist/ModelDictionary-BlDeDyAE.js.map +0 -1
- package/dist/layout-DAFeHIuP.js.map +0 -1
package/dist/client/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { T as rowRuleStyles, _ as tableColumns, f as isBuiltinChart, h as percentColumn, o as chartCardStyle, u as customChartProps } from "../layout-
|
|
2
|
-
import { a as formatCurrency, c as measureFormat, d as useDashboard, i as fieldLabel, l as measureFormatter, m as layoutItemKey, n as INSPECT_ATTRIBUTE, o as formatMeasure, p as LAYOUT_ITEM_ATTRIBUTE, r as useModelQuery, s as formatNumber, t as ModelDictionary, u as DashboardProvider } from "../ModelDictionary-
|
|
1
|
+
import { T as rowRuleStyles, _ as tableColumns, f as isBuiltinChart, h as percentColumn, o as chartCardStyle, u as customChartProps } from "../layout-nViz3MdU.js";
|
|
2
|
+
import { a as formatCurrency, c as measureFormat, d as useDashboard, i as fieldLabel, l as measureFormatter, m as layoutItemKey, n as INSPECT_ATTRIBUTE, o as formatMeasure, p as LAYOUT_ITEM_ATTRIBUTE, r as useModelQuery, s as formatNumber, t as ModelDictionary, u as DashboardProvider } from "../ModelDictionary-Dib4Vk3i.js";
|
|
3
3
|
import { Box, Checkbox, CircularProgress, FormControl, InputLabel, ListItemText, MenuItem, OutlinedInput, Paper, Select, Table, TableBody, TableCell, TableHead, TableRow, TableSortLabel, Typography } from "@mui/material";
|
|
4
4
|
import { Bar, BarChart as BarChart$1, CartesianGrid, Cell, ComposedChart, LabelList, Line, Pie, PieChart, ResponsiveContainer, Tooltip as Tooltip$1, XAxis, YAxis } from "recharts";
|
|
5
5
|
import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
@@ -121,6 +121,83 @@ function TooltipBox({ title, caption, children }) {
|
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
123
|
//#endregion
|
|
124
|
+
//#region src/client/seriesPivot.ts
|
|
125
|
+
const EMPTY_SERIES_LABEL = "(em branco)";
|
|
126
|
+
const OTHERS$1 = "Outros";
|
|
127
|
+
const keyOf = (value) => value === null ? "\0" : String(value);
|
|
128
|
+
/**
|
|
129
|
+
* Séries a partir do domínio (série × medida sem filtros): ordenadas pelo total, maior primeiro, e
|
|
130
|
+
* a cor segue essa ordem — como o domínio não depende dos filtros, cada valor tem sempre a mesma
|
|
131
|
+
* cor. Além de `max`, as menores viram "Outros" (a última vaga), em cinza.
|
|
132
|
+
*/
|
|
133
|
+
function seriesItems(domain, series, measure, max, format = String) {
|
|
134
|
+
const ranked = domain.map((row) => ({
|
|
135
|
+
value: row[series] ?? null,
|
|
136
|
+
amount: Math.abs(numberOf(row, measure) ?? 0)
|
|
137
|
+
})).sort((a, b) => b.amount - a.amount);
|
|
138
|
+
const limit = Math.min(Math.max(max, 2), 8);
|
|
139
|
+
const kept = ranked.length > limit ? ranked.slice(0, limit - 1) : ranked;
|
|
140
|
+
const items = kept.map((item, index) => ({
|
|
141
|
+
dataKey: `s${index}`,
|
|
142
|
+
value: item.value,
|
|
143
|
+
label: item.value === null ? EMPTY_SERIES_LABEL : format(item.value),
|
|
144
|
+
color: seriesColor(index)
|
|
145
|
+
}));
|
|
146
|
+
if (ranked.length > kept.length) items.push(othersItem(ranked.length - kept.length));
|
|
147
|
+
return items;
|
|
148
|
+
}
|
|
149
|
+
const othersItem = (count) => ({
|
|
150
|
+
dataKey: "outros",
|
|
151
|
+
value: null,
|
|
152
|
+
label: count ? `${OTHERS$1} (${count})` : OTHERS$1,
|
|
153
|
+
color: OTHER_COLOR,
|
|
154
|
+
others: true
|
|
155
|
+
});
|
|
156
|
+
/**
|
|
157
|
+
* Linhas (categoria, série, medida) → um ponto por categoria, na ordem da consulta, com o valor de
|
|
158
|
+
* cada série em `dataKey`. Valor fora das séries soma em "Outros" (criado se o domínio não tinha).
|
|
159
|
+
* Devolve só as séries com algum valor nos pontos (um filtro pode ter tirado uma delas).
|
|
160
|
+
*/
|
|
161
|
+
function pivotSeries(rows, dimension, series, measure, items, format = String) {
|
|
162
|
+
const all = [...items];
|
|
163
|
+
const byValue = new Map(all.filter((item) => !item.others).map((item) => [keyOf(item.value), item]));
|
|
164
|
+
const points = /* @__PURE__ */ new Map();
|
|
165
|
+
const used = /* @__PURE__ */ new Set();
|
|
166
|
+
for (const row of rows) {
|
|
167
|
+
const value = row[dimension] ?? "";
|
|
168
|
+
const key = String(value);
|
|
169
|
+
let point = points.get(key);
|
|
170
|
+
if (!point) {
|
|
171
|
+
point = {
|
|
172
|
+
value,
|
|
173
|
+
label: format(value)
|
|
174
|
+
};
|
|
175
|
+
for (const item of all) point[item.dataKey] = 0;
|
|
176
|
+
points.set(key, point);
|
|
177
|
+
}
|
|
178
|
+
let item = byValue.get(keyOf(row[series] ?? null));
|
|
179
|
+
if (!item) {
|
|
180
|
+
item = all.find((other) => other.others);
|
|
181
|
+
if (!item) {
|
|
182
|
+
item = othersItem();
|
|
183
|
+
all.push(item);
|
|
184
|
+
for (const other of points.values()) other[item.dataKey] = 0;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const amount = numberOf(row, measure);
|
|
188
|
+
if (amount) {
|
|
189
|
+
point[item.dataKey] = Number(point[item.dataKey] ?? 0) + amount;
|
|
190
|
+
used.add(item.dataKey);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
points: [...points.values()],
|
|
195
|
+
items: all.filter((item) => used.has(item.dataKey))
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/** Soma das séries de um ponto (a altura da barra empilhada). */
|
|
199
|
+
const pointTotal = (point, items) => items.reduce((total, item) => total + Number(point[item.dataKey] ?? 0), 0);
|
|
200
|
+
//#endregion
|
|
124
201
|
//#region src/client/BarChart.tsx
|
|
125
202
|
const MAX_VALUE_LABELS = 30;
|
|
126
203
|
/** Altura de cada categoria no modo horizontal (barras finas, com respiro). */
|
|
@@ -128,16 +205,27 @@ const ROW_HEIGHT = 28;
|
|
|
128
205
|
/**
|
|
129
206
|
* Barras de uma ou mais medidas por uma dimensão. Ex.:
|
|
130
207
|
* `<BarChart dimension="navio" measures={['custoTotal']} orientation="horizontal" sort="desc" limit={10} />`.
|
|
208
|
+
* Com `series`, uma medida quebrada pelos valores de outra dimensão:
|
|
209
|
+
* `<BarChart dimension="mes" series="tipo_descarga" measures={['volume']} stacked />`.
|
|
131
210
|
*/
|
|
132
|
-
function BarChart({ dimension, measures, orientation = "vertical", stacked = false, sort = "dimension", limit, formatDimension = String, hideEmpty = true, showValues = true, clickToFilter = false, height = 360, labels, formats: formatsByMeasure, queryLabel }) {
|
|
133
|
-
const { meta } = useDashboard();
|
|
211
|
+
function BarChart({ dimension, measures: measureList, series, formatSeries = String, maxSeries = 6, orientation = "vertical", stacked = false, sort = "dimension", limit, formatDimension = String, hideEmpty = true, showValues = true, clickToFilter = false, height = 360, labels, formats: formatsByMeasure, queryLabel }) {
|
|
212
|
+
const { meta, filters, setFilter } = useDashboard();
|
|
213
|
+
const measures = series ? measureList.slice(0, 1) : measureList;
|
|
134
214
|
const { rows, isLoading, error, inspectProps } = useModelQuery({
|
|
135
|
-
dimensions: [dimension],
|
|
215
|
+
dimensions: series ? [dimension, series] : [dimension],
|
|
136
216
|
measures,
|
|
137
|
-
ignoreFilters: clickToFilter ? [dimension] : void 0,
|
|
217
|
+
ignoreFilters: clickToFilter ? series ? [dimension, series] : [dimension] : void 0,
|
|
138
218
|
label: queryLabel ?? `BarChart · ${dimension}`
|
|
139
219
|
});
|
|
220
|
+
const domain = useModelQuery({
|
|
221
|
+
dimensions: series ? [series] : [],
|
|
222
|
+
measures,
|
|
223
|
+
ignoreFilters: Object.keys(filters),
|
|
224
|
+
label: `${queryLabel ?? `BarChart · ${dimension}`} · séries`,
|
|
225
|
+
skip: !series
|
|
226
|
+
});
|
|
140
227
|
const filter = useClickFilter(dimension, clickToFilter);
|
|
228
|
+
const seriesFilter = useClickFilter(series, clickToFilter);
|
|
141
229
|
const formats = measures.map((measure) => measureFormatter(meta, measure, 0, formatsByMeasure));
|
|
142
230
|
const labelOf = (name) => fieldLabel(meta, name, labels);
|
|
143
231
|
const axisFormat = measures[0] ? measureFormat(meta, measures[0], formatsByMeasure) : void 0;
|
|
@@ -145,61 +233,128 @@ function BarChart({ dimension, measures, orientation = "vertical", stacked = fal
|
|
|
145
233
|
...axisFormat,
|
|
146
234
|
decimals: axisFormat?.compact ? 1 : 0
|
|
147
235
|
}, 0);
|
|
236
|
+
let data;
|
|
237
|
+
let bars;
|
|
148
238
|
const first = measures[0];
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
239
|
+
if (series && first) {
|
|
240
|
+
const format = formats[0] ?? String;
|
|
241
|
+
const pivot = pivotSeries(rows, dimension, series, first, seriesItems(domain.rows, series, first, maxSeries, formatSeries), formatDimension);
|
|
242
|
+
bars = pivot.items.map((item) => ({
|
|
243
|
+
...item,
|
|
244
|
+
item,
|
|
245
|
+
format
|
|
246
|
+
}));
|
|
247
|
+
data = pivot.points;
|
|
248
|
+
} else {
|
|
249
|
+
bars = measures.map((measure, index) => ({
|
|
250
|
+
dataKey: measure,
|
|
251
|
+
label: labelOf(measure),
|
|
252
|
+
color: seriesColor(index),
|
|
253
|
+
format: formats[index] ?? String
|
|
254
|
+
}));
|
|
255
|
+
data = rows.map((row) => {
|
|
256
|
+
const value = row[dimension] ?? "";
|
|
257
|
+
const point = {
|
|
258
|
+
value,
|
|
259
|
+
label: formatDimension(value)
|
|
260
|
+
};
|
|
261
|
+
for (const measure of measures) point[measure] = numberOf(row, measure) ?? 0;
|
|
262
|
+
return point;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
if (hideEmpty) data = data.filter((point) => bars.some((bar) => Number(point[bar.dataKey])));
|
|
266
|
+
const sortValue = (point) => series ? pointTotal(point, bars.map((bar) => bar.item).filter((item) => !!item)) : Number(point[first ?? ""]);
|
|
267
|
+
if (sort !== "dimension" && first) data = [...data].sort((a, b) => sort === "desc" ? sortValue(b) - sortValue(a) : sortValue(a) - sortValue(b));
|
|
159
268
|
if (limit) data = data.slice(0, limit);
|
|
160
269
|
const horizontal = orientation === "horizontal";
|
|
161
270
|
const showLabels = showValues && data.length <= MAX_VALUE_LABELS;
|
|
162
271
|
const chartHeight = horizontal ? Math.max(height, data.length * ROW_HEIGHT + 24) : height;
|
|
163
272
|
const longest = Math.max(0, ...data.map((point) => point.label.length));
|
|
273
|
+
const loading = isLoading || domain.isLoading;
|
|
164
274
|
const state = chartPlaceholder({
|
|
165
275
|
height,
|
|
166
|
-
error,
|
|
167
|
-
isLoading,
|
|
276
|
+
error: error ?? domain.error,
|
|
277
|
+
isLoading: loading,
|
|
168
278
|
empty: !data.length
|
|
169
279
|
});
|
|
170
280
|
const ChartTooltip = ({ active, payload }) => {
|
|
171
281
|
if (!active || !payload?.length) return null;
|
|
172
282
|
const point = payload[0].payload;
|
|
173
|
-
|
|
283
|
+
const shown = series ? bars.filter((bar) => Number(point[bar.dataKey])) : bars;
|
|
284
|
+
const format = bars[0]?.format ?? String;
|
|
285
|
+
return /* @__PURE__ */ jsxs(TooltipBox, {
|
|
174
286
|
title: point.label,
|
|
175
287
|
caption: labelOf(dimension),
|
|
176
|
-
children:
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
288
|
+
children: [
|
|
289
|
+
series && first && /* @__PURE__ */ jsxs(Box, {
|
|
290
|
+
sx: {
|
|
291
|
+
fontSize: 11,
|
|
292
|
+
color: "#777",
|
|
293
|
+
mt: .5
|
|
294
|
+
},
|
|
295
|
+
children: [
|
|
296
|
+
labelOf(first),
|
|
297
|
+
" por ",
|
|
298
|
+
labelOf(series).toLocaleLowerCase("pt-BR")
|
|
299
|
+
]
|
|
300
|
+
}),
|
|
301
|
+
shown.map((bar) => /* @__PURE__ */ jsxs(Box, {
|
|
302
|
+
sx: {
|
|
303
|
+
display: "flex",
|
|
304
|
+
alignItems: "center",
|
|
305
|
+
gap: .75
|
|
306
|
+
},
|
|
307
|
+
children: [
|
|
308
|
+
/* @__PURE__ */ jsx(Box, { sx: {
|
|
309
|
+
width: 8,
|
|
310
|
+
height: 8,
|
|
311
|
+
borderRadius: "50%",
|
|
312
|
+
bgcolor: bar.color
|
|
313
|
+
} }),
|
|
314
|
+
bar.label,
|
|
315
|
+
": ",
|
|
316
|
+
bar.format(Number(point[bar.dataKey]))
|
|
317
|
+
]
|
|
318
|
+
}, bar.dataKey)),
|
|
319
|
+
series && stacked && shown.length > 1 && /* @__PURE__ */ jsxs(Box, {
|
|
320
|
+
sx: {
|
|
321
|
+
fontWeight: 700,
|
|
322
|
+
mt: .5
|
|
323
|
+
},
|
|
324
|
+
children: ["Total: ", format(shown.reduce((total, bar) => total + Number(point[bar.dataKey]), 0))]
|
|
325
|
+
})
|
|
326
|
+
]
|
|
194
327
|
});
|
|
195
328
|
};
|
|
196
|
-
const handleClick = (index, event) => {
|
|
329
|
+
const handleClick = (index, bar, event) => {
|
|
197
330
|
event.stopPropagation();
|
|
198
331
|
const point = data[index];
|
|
199
|
-
if (point)
|
|
332
|
+
if (!point) return;
|
|
333
|
+
const value = bar.item?.value;
|
|
334
|
+
if (!series || value === null || value === void 0) {
|
|
335
|
+
filter.toggle(point.value, event);
|
|
336
|
+
seriesFilter.clear();
|
|
337
|
+
} else if (event.ctrlKey || event.metaKey) {
|
|
338
|
+
filter.toggle(point.value, event);
|
|
339
|
+
seriesFilter.toggle(value, event);
|
|
340
|
+
} else {
|
|
341
|
+
const sole = (selected, item) => selected.length === 1 && selected[0] === item;
|
|
342
|
+
const again = sole(filter.selected, point.value) && sole(seriesFilter.selected, value);
|
|
343
|
+
setFilter(dimension, again ? [] : [point.value]);
|
|
344
|
+
setFilter(series, again ? [] : [value]);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
const isDimmed = (point, bar) => {
|
|
348
|
+
if (filter.isDimmed(point.value)) return true;
|
|
349
|
+
if (!bar.item || !seriesFilter.selected.length) return false;
|
|
350
|
+
return bar.item.value === null || seriesFilter.isDimmed(bar.item.value);
|
|
351
|
+
};
|
|
352
|
+
const clearFilters = () => {
|
|
353
|
+
filter.clear();
|
|
354
|
+
seriesFilter.clear();
|
|
200
355
|
};
|
|
201
356
|
const radius = (index) => {
|
|
202
|
-
if (stacked && index <
|
|
357
|
+
if (stacked && index < bars.length - 1) return [
|
|
203
358
|
0,
|
|
204
359
|
0,
|
|
205
360
|
0,
|
|
@@ -221,17 +376,17 @@ function BarChart({ dimension, measures, orientation = "vertical", stacked = fal
|
|
|
221
376
|
...inspectProps,
|
|
222
377
|
sx: { minWidth: 0 },
|
|
223
378
|
children: [/* @__PURE__ */ jsx(Legend, {
|
|
224
|
-
items:
|
|
225
|
-
label
|
|
226
|
-
color
|
|
379
|
+
items: series || bars.length > 1 ? bars.map(({ label, color }) => ({
|
|
380
|
+
label,
|
|
381
|
+
color
|
|
227
382
|
})) : [],
|
|
228
|
-
loading
|
|
383
|
+
loading
|
|
229
384
|
}), state ?? /* @__PURE__ */ jsx(Box, {
|
|
230
|
-
onClick:
|
|
385
|
+
onClick: clearFilters,
|
|
231
386
|
sx: {
|
|
232
387
|
height,
|
|
233
388
|
overflowY: horizontal ? "auto" : "hidden",
|
|
234
|
-
opacity:
|
|
389
|
+
opacity: loading ? .5 : 1,
|
|
235
390
|
transition: "opacity .2s"
|
|
236
391
|
},
|
|
237
392
|
children: /* @__PURE__ */ jsx(Box, {
|
|
@@ -302,27 +457,27 @@ function BarChart({ dimension, measures, orientation = "vertical", stacked = fal
|
|
|
302
457
|
content: /* @__PURE__ */ jsx(ChartTooltip, {}),
|
|
303
458
|
cursor: { fill: "rgba(0,0,0,.04)" }
|
|
304
459
|
}),
|
|
305
|
-
|
|
306
|
-
dataKey:
|
|
307
|
-
name:
|
|
308
|
-
fill:
|
|
460
|
+
bars.map((bar, index) => /* @__PURE__ */ jsxs(Bar, {
|
|
461
|
+
dataKey: bar.dataKey,
|
|
462
|
+
name: bar.label,
|
|
463
|
+
fill: bar.color,
|
|
309
464
|
stackId: stacked ? "stack" : void 0,
|
|
310
465
|
radius: radius(index),
|
|
311
466
|
maxBarSize: horizontal ? 18 : 56,
|
|
312
467
|
isAnimationActive: false,
|
|
313
|
-
onClick: clickToFilter ? (_bar, barIndex, event) => handleClick(barIndex, event) : void 0,
|
|
468
|
+
onClick: clickToFilter ? (_bar, barIndex, event) => handleClick(barIndex, bar, event) : void 0,
|
|
314
469
|
cursor: clickToFilter ? "pointer" : void 0,
|
|
315
|
-
children: [clickToFilter && data.map((point) => /* @__PURE__ */ jsx(Cell, { fillOpacity:
|
|
316
|
-
dataKey:
|
|
470
|
+
children: [clickToFilter && data.map((point) => /* @__PURE__ */ jsx(Cell, { fillOpacity: isDimmed(point, bar) ? .35 : 1 }, String(point.value))), showLabels && !stacked && /* @__PURE__ */ jsx(LabelList, {
|
|
471
|
+
dataKey: bar.dataKey,
|
|
317
472
|
position: horizontal ? "right" : "top",
|
|
318
|
-
formatter: (value) =>
|
|
473
|
+
formatter: (value) => bar.format(value),
|
|
319
474
|
style: {
|
|
320
475
|
fontSize: 12,
|
|
321
476
|
fill: "#333",
|
|
322
477
|
fontWeight: 600
|
|
323
478
|
}
|
|
324
479
|
})]
|
|
325
|
-
},
|
|
480
|
+
}, bar.dataKey))
|
|
326
481
|
]
|
|
327
482
|
})
|
|
328
483
|
})
|
|
@@ -1279,11 +1434,12 @@ function Chart({ chart, formatters }) {
|
|
|
1279
1434
|
});
|
|
1280
1435
|
}
|
|
1281
1436
|
case "bar": {
|
|
1282
|
-
const { id, type, title, span, card, formatDimension, ...props } = chart;
|
|
1437
|
+
const { id, type, title, span, card, formatDimension, formatSeries, ...props } = chart;
|
|
1283
1438
|
return /* @__PURE__ */ jsx(BarChart, {
|
|
1284
1439
|
...props,
|
|
1285
1440
|
queryLabel,
|
|
1286
|
-
formatDimension: formatter(formatDimension)
|
|
1441
|
+
formatDimension: formatter(formatDimension),
|
|
1442
|
+
formatSeries: formatter(formatSeries)
|
|
1287
1443
|
});
|
|
1288
1444
|
}
|
|
1289
1445
|
case "donut": {
|