@bluemarble/bi 0.6.3 → 0.6.5

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,143 +1,164 @@
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-CBzYDoqE.js";
2
+ import { S as layoutItemKey, _ as useClickFilter, a as formatCurrency, c as measureFormat, d as OTHER_COLOR, f as SERIES_COLORS, g as seriesColor, h as numberOf, i as fieldLabel, l as measureFormatter, m as chartPlaceholder, n as INSPECT_ATTRIBUTE, o as formatMeasure, p as TooltipBox, r as useModelQuery, s as formatNumber, t as ModelDictionary, u as Legend, v as DashboardProvider, x as LAYOUT_ITEM_ATTRIBUTE, y as useDashboard } from "../ModelDictionary-CyeYU6ZJ.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";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
- //#region src/client/chartKit.tsx
7
+ //#region src/client/seriesPivot.ts
8
+ const EMPTY_SERIES_LABEL = "(em branco)";
9
+ const OTHERS$1 = "Outros";
10
+ const keyOf = (value) => value === null ? "\0" : String(value);
8
11
  /**
9
- * Peças comuns dos gráficos ligados ao modelo: paleta, filtro por clique, legenda e os estados de
10
- * carregando/erro/vazio.
12
+ * Séries a partir do domínio (série × medida sem filtros): ordenadas pelo total, maior primeiro, e
13
+ * a cor segue essa ordem — como o domínio não depende dos filtros, cada valor tem sempre a mesma
14
+ * cor. Além de `max`, as menores viram "Outros" (a última vaga), em cinza.
11
15
  */
16
+ function seriesItems(domain, series, measure, max, format = String) {
17
+ const ranked = domain.map((row) => ({
18
+ value: row[series] ?? null,
19
+ amount: Math.abs(numberOf(row, measure) ?? 0)
20
+ })).sort((a, b) => b.amount - a.amount);
21
+ const limit = Math.min(Math.max(max, 2), 8);
22
+ const kept = ranked.length > limit ? ranked.slice(0, limit - 1) : ranked;
23
+ const items = kept.map((item, index) => ({
24
+ dataKey: `s${index}`,
25
+ value: item.value,
26
+ label: item.value === null ? EMPTY_SERIES_LABEL : format(item.value),
27
+ color: seriesColor(index)
28
+ }));
29
+ if (ranked.length > kept.length) items.push(othersItem(ranked.length - kept.length));
30
+ return items;
31
+ }
32
+ const othersItem = (count) => ({
33
+ dataKey: "outros",
34
+ value: null,
35
+ label: count ? `${OTHERS$1} (${count})` : OTHERS$1,
36
+ color: OTHER_COLOR,
37
+ others: true
38
+ });
12
39
  /**
13
- * Paleta categórica, sempre nesta ordem (a série N tem sempre a mesma cor). Laranja Rocha primeiro;
14
- * a ordem passa no validador de daltonismo (CVD ΔE ≥ 9 entre vizinhas) e de visão normal (≥ 19).
15
- */
16
- const SERIES_COLORS = [
17
- "#F58220",
18
- "#2a78d6",
19
- "#1baf7a",
20
- "#eda100",
21
- "#e87ba4",
22
- "#008300",
23
- "#6250d6",
24
- "#e34948"
25
- ];
26
- /** "Outros" (fatias agrupadas) fica neutro, fora da paleta. */
27
- const OTHER_COLOR = "#9e9e9e";
28
- const seriesColor = (index) => SERIES_COLORS[index % SERIES_COLORS.length];
29
- /** Valor numérico de uma coluna da linha (`null` quando vazio). */
30
- const numberOf = (row, key) => key && row[key] !== null && row[key] !== void 0 ? Number(row[key]) : null;
31
- /**
32
- * Filtro cruzado a partir do gráfico: clicar num valor filtra o dashboard por ele (Ctrl/Cmd+clique
33
- * acumula; clicar de novo no único selecionado limpa). O gráfico deve ignorar o próprio filtro na
34
- * consulta (`ignoreFilters`) e só destacar a seleção.
40
+ * Linhas (categoria, série, medida) → um ponto por categoria, na ordem da consulta, com o valor de
41
+ * cada série em `dataKey`. Valor fora das séries soma em "Outros" (criado se o domínio não tinha).
42
+ * Devolve só as séries com algum valor nos pontos (um filtro pode ter tirado uma delas).
35
43
  */
36
- function useClickFilter(dimension, enabled) {
37
- const { filters, setFilter } = useDashboard();
38
- const selected = enabled && dimension ? filters[dimension] ?? [] : [];
39
- const toggle = (value, event) => {
40
- if (!enabled || !dimension) return;
41
- if (event?.ctrlKey || event?.metaKey) setFilter(dimension, selected.includes(value) ? selected.filter((item) => item !== value) : [...selected, value]);
42
- else setFilter(dimension, selected.length === 1 && selected[0] === value ? [] : [value]);
43
- };
44
- return {
45
- selected,
46
- /** Há seleção e este valor não está nela. */
47
- isDimmed: (value) => selected.length > 0 && !selected.includes(value),
48
- toggle,
49
- clear: () => {
50
- if (enabled && dimension && selected.length) setFilter(dimension, []);
44
+ function pivotSeries(rows, dimension, series, measure, items, format = String) {
45
+ const all = [...items];
46
+ const byValue = new Map(all.filter((item) => !item.others).map((item) => [keyOf(item.value), item]));
47
+ const points = /* @__PURE__ */ new Map();
48
+ const used = /* @__PURE__ */ new Set();
49
+ for (const row of rows) {
50
+ const value = row[dimension] ?? "";
51
+ const key = String(value);
52
+ let point = points.get(key);
53
+ if (!point) {
54
+ point = {
55
+ value,
56
+ label: format(value)
57
+ };
58
+ for (const item of all) point[item.dataKey] = 0;
59
+ points.set(key, point);
51
60
  }
61
+ let item = byValue.get(keyOf(row[series] ?? null));
62
+ if (!item) {
63
+ item = all.find((other) => other.others);
64
+ if (!item) {
65
+ item = othersItem();
66
+ all.push(item);
67
+ for (const other of points.values()) other[item.dataKey] = 0;
68
+ }
69
+ }
70
+ const amount = numberOf(row, measure);
71
+ if (amount) {
72
+ point[item.dataKey] = Number(point[item.dataKey] ?? 0) + amount;
73
+ used.add(item.dataKey);
74
+ }
75
+ }
76
+ return {
77
+ points: [...points.values()],
78
+ items: all.filter((item) => used.has(item.dataKey))
52
79
  };
53
80
  }
54
- function Legend({ items, loading }) {
55
- return /* @__PURE__ */ jsxs(Box, {
56
- sx: {
57
- display: "flex",
58
- flexWrap: "wrap",
59
- columnGap: 1.5,
60
- rowGap: .5,
61
- mb: 1,
62
- fontSize: 14,
63
- color: "#555",
64
- minHeight: 21
65
- },
66
- children: [items.map((item) => /* @__PURE__ */ jsxs(Box, {
67
- sx: {
68
- display: "flex",
69
- alignItems: "center",
70
- gap: .5
71
- },
72
- children: [/* @__PURE__ */ jsx(Box, { sx: {
73
- width: 9,
74
- height: 9,
75
- borderRadius: "50%",
76
- bgcolor: item.color
77
- } }), item.label]
78
- }, item.label)), loading && /* @__PURE__ */ jsx(CircularProgress, {
79
- size: 14,
80
- sx: { ml: "auto" }
81
- })]
82
- });
83
- }
84
- /** Erro / carregando / sem dados no lugar do gráfico; `null` quando há o que desenhar (função, não componente). */
85
- function chartPlaceholder({ height, error, isLoading, empty }) {
86
- if (!error && !empty) return null;
87
- return /* @__PURE__ */ jsx(Box, {
88
- sx: {
89
- height,
90
- display: "flex",
91
- alignItems: "center",
92
- justifyContent: "center",
93
- color: error ? "error.main" : "#999"
94
- },
95
- children: error ? "Erro ao carregar o gráfico" : isLoading ? "Carregando…" : "Sem dados para os filtros selecionados"
96
- });
97
- }
98
- /** `caption`: o que é o título (ex.: o rótulo da dimensão), em cinza acima dele. */
99
- function TooltipBox({ title, caption, children }) {
100
- return /* @__PURE__ */ jsxs(Box, {
101
- sx: {
102
- bgcolor: "#fff",
103
- border: "1px solid #ccc",
104
- p: 1,
105
- fontSize: 13
106
- },
107
- children: [
108
- caption && /* @__PURE__ */ jsx(Box, {
109
- sx: {
110
- fontSize: 11,
111
- color: "#777"
112
- },
113
- children: caption
114
- }),
115
- /* @__PURE__ */ jsx(Box, {
116
- sx: { fontWeight: 700 },
117
- children: title
118
- }),
119
- children
120
- ]
121
- });
81
+ /** Soma das séries de um ponto (a altura da barra empilhada). */
82
+ const pointTotal = (point, items) => items.reduce((total, item) => total + Number(point[item.dataKey] ?? 0), 0);
83
+ /**
84
+ * Todas as categorias da dimensão (consulta só da dimensão, sem medidas), na ordem dela, com os
85
+ * valores dos pontos que vieram da consulta e zero nas que não têm dado (o "Mostrar itens sem
86
+ * dados" do Power BI). Ponto fora da lista (não deveria haver) vai no fim.
87
+ */
88
+ function fillCategories(points, categories, dimension, dataKeys, format = String) {
89
+ const byValue = new Map(points.map((point) => [String(point.value), point]));
90
+ const seen = /* @__PURE__ */ new Set();
91
+ const filled = [];
92
+ for (const row of categories) {
93
+ const value = row[dimension] ?? "";
94
+ const key = String(value);
95
+ if (seen.has(key)) continue;
96
+ seen.add(key);
97
+ const point = byValue.get(key);
98
+ if (point) filled.push(point);
99
+ else filled.push({
100
+ value,
101
+ label: format(value),
102
+ ...Object.fromEntries(dataKeys.map((dataKey) => [dataKey, 0]))
103
+ });
104
+ }
105
+ return [...filled, ...points.filter((point) => !seen.has(String(point.value)))];
122
106
  }
123
107
  //#endregion
124
108
  //#region src/client/BarChart.tsx
125
109
  const MAX_VALUE_LABELS = 30;
110
+ /** Coluna do total da pilha no ponto (para o rótulo do `showTotal`). */
111
+ const TOTAL_KEY = "\0total";
112
+ /** Posição do valor no Recharts, conforme a orientação. */
113
+ const LABEL_POSITION = {
114
+ vertical: {
115
+ outside: "top",
116
+ insideEnd: "insideTop",
117
+ center: "center",
118
+ insideBase: "insideBottom"
119
+ },
120
+ horizontal: {
121
+ outside: "right",
122
+ insideEnd: "insideRight",
123
+ center: "center",
124
+ insideBase: "insideLeft"
125
+ }
126
+ };
127
+ /** Chave de uma série em `colors`: o valor, "(em branco)" ou "Outros". */
128
+ const seriesColorKey = (item) => item.others ? "Outros" : item.value === null ? EMPTY_SERIES_LABEL : String(item.value);
126
129
  /** Altura de cada categoria no modo horizontal (barras finas, com respiro). */
127
130
  const ROW_HEIGHT = 28;
128
131
  /**
129
132
  * Barras de uma ou mais medidas por uma dimensão. Ex.:
130
133
  * `<BarChart dimension="navio" measures={['custoTotal']} orientation="horizontal" sort="desc" limit={10} />`.
134
+ * Com `series`, uma medida quebrada pelos valores de outra dimensão:
135
+ * `<BarChart dimension="mes" series="tipo_descarga" measures={['volume']} stacked />`.
131
136
  */
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();
137
+ function BarChart({ dimension, measures: measureList, series, formatSeries = String, maxSeries = 6, orientation = "vertical", stacked = false, sort = "dimension", limit, formatDimension = String, hideEmpty = true, showEmpty = false, showValues = true, valuePosition = "outside", valueColor, valueSize = 12, showTotal = false, colors, clickToFilter = false, height = 360, labels, formats: formatsByMeasure, queryLabel }) {
138
+ const { meta, filters, setFilter } = useDashboard();
139
+ const measures = series ? measureList.slice(0, 1) : measureList;
134
140
  const { rows, isLoading, error, inspectProps } = useModelQuery({
135
- dimensions: [dimension],
141
+ dimensions: series ? [dimension, series] : [dimension],
136
142
  measures,
137
- ignoreFilters: clickToFilter ? [dimension] : void 0,
143
+ ignoreFilters: clickToFilter ? series ? [dimension, series] : [dimension] : void 0,
138
144
  label: queryLabel ?? `BarChart · ${dimension}`
139
145
  });
146
+ const domain = useModelQuery({
147
+ dimensions: series ? [series] : [],
148
+ measures,
149
+ ignoreFilters: Object.keys(filters),
150
+ label: `${queryLabel ?? `BarChart · ${dimension}`} · séries`,
151
+ skip: !series
152
+ });
153
+ const table = meta?.dimensions[dimension]?.table;
154
+ const categories = useModelQuery({
155
+ dimensions: [dimension],
156
+ ignoreFilters: Object.keys(filters).filter((name) => clickToFilter && name === dimension || !table || meta?.dimensions[name]?.table !== table),
157
+ label: `${queryLabel ?? `BarChart · ${dimension}`} · categorias`,
158
+ skip: !showEmpty || !meta
159
+ });
140
160
  const filter = useClickFilter(dimension, clickToFilter);
161
+ const seriesFilter = useClickFilter(series, clickToFilter);
141
162
  const formats = measures.map((measure) => measureFormatter(meta, measure, 0, formatsByMeasure));
142
163
  const labelOf = (name) => fieldLabel(meta, name, labels);
143
164
  const axisFormat = measures[0] ? measureFormat(meta, measures[0], formatsByMeasure) : void 0;
@@ -145,61 +166,143 @@ function BarChart({ dimension, measures, orientation = "vertical", stacked = fal
145
166
  ...axisFormat,
146
167
  decimals: axisFormat?.compact ? 1 : 0
147
168
  }, 0);
169
+ let data;
170
+ let bars;
148
171
  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]));
172
+ if (series && first) {
173
+ const format = formats[0] ?? String;
174
+ const pivot = pivotSeries(rows, dimension, series, first, seriesItems(domain.rows, series, first, maxSeries, formatSeries), formatDimension);
175
+ bars = pivot.items.map((item) => ({
176
+ ...item,
177
+ color: colors?.[seriesColorKey(item)] ?? item.color,
178
+ item,
179
+ format
180
+ }));
181
+ data = pivot.points;
182
+ } else {
183
+ bars = measures.map((measure, index) => ({
184
+ dataKey: measure,
185
+ label: labelOf(measure),
186
+ color: colors?.[measure] ?? seriesColor(index),
187
+ format: formats[index] ?? String
188
+ }));
189
+ data = rows.map((row) => {
190
+ const value = row[dimension] ?? "";
191
+ const point = {
192
+ value,
193
+ label: formatDimension(value)
194
+ };
195
+ for (const measure of measures) point[measure] = numberOf(row, measure) ?? 0;
196
+ return point;
197
+ });
198
+ }
199
+ if (showEmpty) data = fillCategories(data, categories.rows, dimension, bars.map((bar) => bar.dataKey), formatDimension);
200
+ else if (hideEmpty) data = data.filter((point) => bars.some((bar) => Number(point[bar.dataKey])));
201
+ const sortValue = (point) => series ? pointTotal(point, bars.map((bar) => bar.item).filter((item) => !!item)) : Number(point[first ?? ""]);
202
+ if (sort !== "dimension" && first) data = [...data].sort((a, b) => sort === "desc" ? sortValue(b) - sortValue(a) : sortValue(a) - sortValue(b));
159
203
  if (limit) data = data.slice(0, limit);
160
204
  const horizontal = orientation === "horizontal";
161
205
  const showLabels = showValues && data.length <= MAX_VALUE_LABELS;
206
+ const segmentLabels = showLabels && (!stacked || valuePosition !== "outside");
207
+ const totalLabel = showLabels && stacked && showTotal && bars.length > 0;
208
+ const labelsOutside = segmentLabels && valuePosition === "outside" || totalLabel;
209
+ if (totalLabel) data = data.map((point) => ({
210
+ ...point,
211
+ [TOTAL_KEY]: bars.reduce((total, bar) => total + Number(point[bar.dataKey] ?? 0), 0)
212
+ }));
213
+ const labelStyle = (outside) => ({
214
+ fontSize: valueSize,
215
+ fill: valueColor ?? (outside ? "#333" : "#fff"),
216
+ fontWeight: 600
217
+ });
218
+ const position = LABEL_POSITION[orientation === "horizontal" ? "horizontal" : "vertical"];
162
219
  const chartHeight = horizontal ? Math.max(height, data.length * ROW_HEIGHT + 24) : height;
163
220
  const longest = Math.max(0, ...data.map((point) => point.label.length));
221
+ const loading = isLoading || domain.isLoading || categories.isLoading || showEmpty && !meta;
164
222
  const state = chartPlaceholder({
165
223
  height,
166
- error,
167
- isLoading,
224
+ error: error ?? domain.error ?? categories.error,
225
+ isLoading: loading,
168
226
  empty: !data.length
169
227
  });
170
228
  const ChartTooltip = ({ active, payload }) => {
171
229
  if (!active || !payload?.length) return null;
172
230
  const point = payload[0].payload;
173
- return /* @__PURE__ */ jsx(TooltipBox, {
231
+ const shown = series ? bars.filter((bar) => Number(point[bar.dataKey])) : bars;
232
+ const format = bars[0]?.format ?? String;
233
+ return /* @__PURE__ */ jsxs(TooltipBox, {
174
234
  title: point.label,
175
235
  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))
236
+ children: [
237
+ series && first && /* @__PURE__ */ jsxs(Box, {
238
+ sx: {
239
+ fontSize: 11,
240
+ color: "#777",
241
+ mt: .5
242
+ },
243
+ children: [
244
+ labelOf(first),
245
+ " por ",
246
+ labelOf(series).toLocaleLowerCase("pt-BR")
247
+ ]
248
+ }),
249
+ shown.map((bar) => /* @__PURE__ */ jsxs(Box, {
250
+ sx: {
251
+ display: "flex",
252
+ alignItems: "center",
253
+ gap: .75
254
+ },
255
+ children: [
256
+ /* @__PURE__ */ jsx(Box, { sx: {
257
+ width: 8,
258
+ height: 8,
259
+ borderRadius: "50%",
260
+ bgcolor: bar.color
261
+ } }),
262
+ bar.label,
263
+ ": ",
264
+ bar.format(Number(point[bar.dataKey]))
265
+ ]
266
+ }, bar.dataKey)),
267
+ series && stacked && shown.length > 1 && /* @__PURE__ */ jsxs(Box, {
268
+ sx: {
269
+ fontWeight: 700,
270
+ mt: .5
271
+ },
272
+ children: ["Total: ", format(shown.reduce((total, bar) => total + Number(point[bar.dataKey]), 0))]
273
+ })
274
+ ]
194
275
  });
195
276
  };
196
- const handleClick = (index, event) => {
277
+ const handleClick = (index, bar, event) => {
197
278
  event.stopPropagation();
198
279
  const point = data[index];
199
- if (point) filter.toggle(point.value, event);
280
+ if (!point) return;
281
+ const value = bar.item?.value;
282
+ if (!series || value === null || value === void 0) {
283
+ filter.toggle(point.value, event);
284
+ seriesFilter.clear();
285
+ } else if (event.ctrlKey || event.metaKey) {
286
+ filter.toggle(point.value, event);
287
+ seriesFilter.toggle(value, event);
288
+ } else {
289
+ const sole = (selected, item) => selected.length === 1 && selected[0] === item;
290
+ const again = sole(filter.selected, point.value) && sole(seriesFilter.selected, value);
291
+ setFilter(dimension, again ? [] : [point.value]);
292
+ setFilter(series, again ? [] : [value]);
293
+ }
294
+ };
295
+ const isDimmed = (point, bar) => {
296
+ if (filter.isDimmed(point.value)) return true;
297
+ if (!bar.item || !seriesFilter.selected.length) return false;
298
+ return bar.item.value === null || seriesFilter.isDimmed(bar.item.value);
299
+ };
300
+ const clearFilters = () => {
301
+ filter.clear();
302
+ seriesFilter.clear();
200
303
  };
201
304
  const radius = (index) => {
202
- if (stacked && index < measures.length - 1) return [
305
+ if (stacked && index < bars.length - 1) return [
203
306
  0,
204
307
  0,
205
308
  0,
@@ -221,17 +324,17 @@ function BarChart({ dimension, measures, orientation = "vertical", stacked = fal
221
324
  ...inspectProps,
222
325
  sx: { minWidth: 0 },
223
326
  children: [/* @__PURE__ */ jsx(Legend, {
224
- items: measures.length > 1 ? measures.map((measure, index) => ({
225
- label: labelOf(measure),
226
- color: seriesColor(index)
327
+ items: series || bars.length > 1 ? bars.map(({ label, color }) => ({
328
+ label,
329
+ color
227
330
  })) : [],
228
- loading: isLoading
331
+ loading
229
332
  }), state ?? /* @__PURE__ */ jsx(Box, {
230
- onClick: filter.clear,
333
+ onClick: clearFilters,
231
334
  sx: {
232
335
  height,
233
336
  overflowY: horizontal ? "auto" : "hidden",
234
- opacity: isLoading ? .5 : 1,
337
+ opacity: loading ? .5 : 1,
235
338
  transition: "opacity .2s"
236
339
  },
237
340
  children: /* @__PURE__ */ jsx(Box, {
@@ -246,11 +349,11 @@ function BarChart({ dimension, measures, orientation = "vertical", stacked = fal
246
349
  barCategoryGap: "20%",
247
350
  margin: horizontal ? {
248
351
  top: 4,
249
- right: showLabels ? 96 : 16,
352
+ right: labelsOutside ? 96 : 16,
250
353
  left: 8,
251
354
  bottom: 4
252
355
  } : {
253
- top: showLabels ? 24 : 8,
356
+ top: labelsOutside ? valueSize + 12 : 8,
254
357
  right: 8,
255
358
  left: 8,
256
359
  bottom: 0
@@ -302,27 +405,36 @@ function BarChart({ dimension, measures, orientation = "vertical", stacked = fal
302
405
  content: /* @__PURE__ */ jsx(ChartTooltip, {}),
303
406
  cursor: { fill: "rgba(0,0,0,.04)" }
304
407
  }),
305
- measures.map((measure, index) => /* @__PURE__ */ jsxs(Bar, {
306
- dataKey: measure,
307
- name: labelOf(measure),
308
- fill: seriesColor(index),
408
+ bars.map((bar, index) => /* @__PURE__ */ jsxs(Bar, {
409
+ dataKey: bar.dataKey,
410
+ name: bar.label,
411
+ fill: bar.color,
309
412
  stackId: stacked ? "stack" : void 0,
310
413
  radius: radius(index),
311
414
  maxBarSize: horizontal ? 18 : 56,
312
415
  isAnimationActive: false,
313
- onClick: clickToFilter ? (_bar, barIndex, event) => handleClick(barIndex, event) : void 0,
416
+ onClick: clickToFilter ? (_bar, barIndex, event) => handleClick(barIndex, bar, event) : void 0,
314
417
  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,
317
- position: horizontal ? "right" : "top",
318
- formatter: (value) => formats[index]?.(value) ?? "",
319
- style: {
320
- fontSize: 12,
321
- fill: "#333",
322
- fontWeight: 600
323
- }
324
- })]
325
- }, measure))
418
+ children: [
419
+ clickToFilter && data.map((point) => /* @__PURE__ */ jsx(Cell, { fillOpacity: isDimmed(point, bar) ? .35 : 1 }, String(point.value))),
420
+ segmentLabels && /* @__PURE__ */ jsx(LabelList, {
421
+ dataKey: bar.dataKey,
422
+ position: position[valuePosition],
423
+ formatter: (value) => stacked && !Number(value) ? "" : bar.format(value),
424
+ style: labelStyle(valuePosition === "outside")
425
+ }),
426
+ totalLabel && index === bars.length - 1 && /* @__PURE__ */ jsx(LabelList, {
427
+ dataKey: TOTAL_KEY,
428
+ position: position.outside,
429
+ formatter: (value) => (bars[0]?.format ?? String)(value),
430
+ style: {
431
+ ...labelStyle(true),
432
+ ...valuePosition !== "outside" && { fill: "#333" },
433
+ fontWeight: 700
434
+ }
435
+ })
436
+ ]
437
+ }, bar.dataKey))
326
438
  ]
327
439
  })
328
440
  })
@@ -1279,11 +1391,12 @@ function Chart({ chart, formatters }) {
1279
1391
  });
1280
1392
  }
1281
1393
  case "bar": {
1282
- const { id, type, title, span, card, formatDimension, ...props } = chart;
1394
+ const { id, type, title, span, card, formatDimension, formatSeries, ...props } = chart;
1283
1395
  return /* @__PURE__ */ jsx(BarChart, {
1284
1396
  ...props,
1285
1397
  queryLabel,
1286
- formatDimension: formatter(formatDimension)
1398
+ formatDimension: formatter(formatDimension),
1399
+ formatSeries: formatter(formatSeries)
1287
1400
  });
1288
1401
  }
1289
1402
  case "donut": {