@bluemarble/bi 0.6.3 → 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.
@@ -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-DAFeHIuP.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-Dc3_LKqr.js";
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
- let data = rows.filter((row) => !hideEmpty || measures.some((measure) => numberOf(row, measure))).map((row) => {
150
- const value = row[dimension] ?? "";
151
- const point = {
152
- value,
153
- label: formatDimension(value)
154
- };
155
- for (const measure of measures) point[measure] = numberOf(row, measure) ?? 0;
156
- return point;
157
- });
158
- if (sort !== "dimension" && first) data = [...data].sort((a, b) => sort === "desc" ? Number(b[first]) - Number(a[first]) : Number(a[first]) - Number(b[first]));
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
- return /* @__PURE__ */ jsx(TooltipBox, {
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: measures.map((measure, index) => /* @__PURE__ */ jsxs(Box, {
177
- sx: {
178
- display: "flex",
179
- alignItems: "center",
180
- gap: .75
181
- },
182
- children: [
183
- /* @__PURE__ */ jsx(Box, { sx: {
184
- width: 8,
185
- height: 8,
186
- borderRadius: "50%",
187
- bgcolor: seriesColor(index)
188
- } }),
189
- labelOf(measure),
190
- ": ",
191
- formats[index]?.(point[measure])
192
- ]
193
- }, measure))
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) filter.toggle(point.value, event);
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 < measures.length - 1) return [
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: measures.length > 1 ? measures.map((measure, index) => ({
225
- label: labelOf(measure),
226
- color: seriesColor(index)
379
+ items: series || bars.length > 1 ? bars.map(({ label, color }) => ({
380
+ label,
381
+ color
227
382
  })) : [],
228
- loading: isLoading
383
+ loading
229
384
  }), state ?? /* @__PURE__ */ jsx(Box, {
230
- onClick: filter.clear,
385
+ onClick: clearFilters,
231
386
  sx: {
232
387
  height,
233
388
  overflowY: horizontal ? "auto" : "hidden",
234
- opacity: isLoading ? .5 : 1,
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
- measures.map((measure, index) => /* @__PURE__ */ jsxs(Bar, {
306
- dataKey: measure,
307
- name: labelOf(measure),
308
- fill: seriesColor(index),
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: filter.isDimmed(point.value) ? .35 : 1 }, String(point.value))), showLabels && !stacked && /* @__PURE__ */ jsx(LabelList, {
316
- dataKey: measure,
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) => formats[index]?.(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
- }, measure))
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": {