@bluemarble/bi 0.4.0 → 0.6.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.
@@ -1,5 +1,5 @@
1
- import { f as isBuiltinChart, o as chartCardStyle, u as customChartProps } from "../layout-676sEvvb.js";
2
- import { a as useDashboard, c as layoutItemKey, i as DashboardProvider, n as INSPECT_ATTRIBUTE, r as useModelQuery, s as LAYOUT_ITEM_ATTRIBUTE, t as ModelDictionary } from "../ModelDictionary-Y7W9oK1J.js";
1
+ import { T as rowRuleStyles, _ as tableColumns, f as isBuiltinChart, h as percentColumn, o as chartCardStyle, u as customChartProps } from "../layout-BS51SQcJ.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-C2tzlOY8.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,75 +121,6 @@ function TooltipBox({ title, caption, children }) {
121
121
  });
122
122
  }
123
123
  //#endregion
124
- //#region src/client/format.ts
125
- const formatters = /* @__PURE__ */ new Map();
126
- /** `compact`: notação curta do pt-BR (1,2 mil / 3,4 mi / 5,6 bi). */
127
- function formatNumber(value, decimals = 2, compact = false) {
128
- if (value === null || value === void 0 || value === "") return "";
129
- const number = Number(value);
130
- if (Number.isNaN(number)) return String(value);
131
- const key = `${decimals}:${compact}`;
132
- let formatter = formatters.get(key);
133
- if (!formatter) {
134
- formatter = new Intl.NumberFormat("pt-BR", {
135
- minimumFractionDigits: decimals,
136
- maximumFractionDigits: decimals,
137
- ...compact && { notation: "compact" }
138
- });
139
- formatters.set(key, formatter);
140
- }
141
- return formatter.format(number);
142
- }
143
- const formatCurrency = (value, decimals = 2, compact = false) => {
144
- const text = formatNumber(value, decimals, compact);
145
- return text && `R$ ${text}`;
146
- };
147
- /**
148
- * Formata pelo dicionário do modelo (`format`/`decimals` da medida) ou pelo formato do gráfico.
149
- * `fallbackDecimals` é o padrão do componente quando nenhum dos dois define `decimals`.
150
- */
151
- function formatMeasure(value, meta, fallbackDecimals = 2) {
152
- if (value === null || value === void 0 || value === "") return "";
153
- const decimals = meta?.decimals ?? (meta?.format === "integer" ? 0 : fallbackDecimals);
154
- const compact = meta?.compact ?? false;
155
- let text;
156
- switch (meta?.format) {
157
- case "currency":
158
- text = formatCurrency(value, decimals, compact);
159
- break;
160
- case "percent": {
161
- const number = formatNumber(Number(value) * 100, decimals, compact);
162
- text = number && `${number}%`;
163
- break;
164
- }
165
- default: text = formatNumber(value, decimals, compact);
166
- }
167
- return text && `${meta?.prefix ?? ""}${text}${meta?.suffix ?? ""}`;
168
- }
169
- /** Dicionário do modelo com o formato do gráfico por cima. */
170
- function measureFormat(meta, measure, formats) {
171
- const fromModel = meta?.measures[measure];
172
- const fromChart = formats?.[measure];
173
- if (!fromChart) return fromModel;
174
- return {
175
- ...fromModel?.format !== void 0 && { format: fromModel.format },
176
- ...fromModel?.decimals !== void 0 && { decimals: fromModel.decimals },
177
- ...fromChart
178
- };
179
- }
180
- /**
181
- * Formatter de uma medida: o `formats` do gráfico (se houver) por cima do dicionário do modelo; sem
182
- * `format`, número com `fallbackDecimals`.
183
- */
184
- function measureFormatter(meta, measure, fallbackDecimals = 2, formats) {
185
- const format = measureFormat(meta, measure, formats);
186
- return (value) => formatMeasure(value, format, fallbackDecimals);
187
- }
188
- /** Rótulo de uma dimensão/medida: o `labels` do gráfico, o dicionário do modelo ou o nome. */
189
- function fieldLabel(meta, name, labels) {
190
- return labels?.[name] || meta?.dimensions[name]?.label || meta?.measures[name]?.label || name;
191
- }
192
- //#endregion
193
124
  //#region src/client/BarChart.tsx
194
125
  const MAX_VALUE_LABELS = 30;
195
126
  /** Altura de cada categoria no modo horizontal (barras finas, com respiro). */
@@ -774,8 +705,7 @@ function DashboardCanvas({ layout, children }) {
774
705
  //#endregion
775
706
  //#region src/client/DataTable.tsx
776
707
  const EMPTY = [];
777
- /** Chave da coluna de percentual de uma medida (ordenação). */
778
- const PERCENT = "%:";
708
+ const NO_RULES = [];
779
709
  function compare(a, b) {
780
710
  if (typeof a === "number" && typeof b === "number") return a - b;
781
711
  return String(a).localeCompare(String(b), "pt-BR", { numeric: true });
@@ -784,17 +714,25 @@ function compare(a, b) {
784
714
  * Tabela de medidas por dimensões, com total e ordenação pelo cabeçalho. Ex.:
785
715
  * `<DataTable dimensions={['navio']} measures={['volTotal', 'custoTotal', 'custoTon']} />`.
786
716
  */
787
- function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDimensions = {}, showTotal = true, sortBy, sortDirection = "desc", clickToFilter = false, height = 400, labels, formats: formatsByMeasure, percentDecimals = 1, queryLabel }) {
717
+ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDimensions = {}, showTotal = true, sortBy, sortDirection = "desc", clickToFilter = false, height = 400, labels, formats: formatsByMeasure, percentDecimals = 1, columnOrder, formatRules = NO_RULES, cellBorders = false, borderColor = "#e0e0e0", align, verticalAlign, queryLabel }) {
788
718
  const { meta } = useDashboard();
789
719
  const name = queryLabel ?? `DataTable · ${dimensions.join(", ") || "total"}`;
790
720
  const firstDimension = dimensions[0];
791
721
  const ignoreFilters = clickToFilter && firstDimension ? [firstDimension] : void 0;
792
722
  const extraMeasures = percentOf.filter((measure) => !measures.includes(measure));
723
+ const outside = [...new Set([sortBy, ...formatRules.map((rule) => rule.field)].filter(Boolean))].filter((field) => !field.startsWith("%:") && !dimensions.includes(field) && !measures.includes(field) && !extraMeasures.includes(field));
724
+ const waitingMeta = outside.length > 0 && !meta;
725
+ const queryDimensions = [...dimensions, ...outside.filter((field) => meta?.dimensions[field])];
793
726
  const { rows, isLoading, error, inspectProps } = useModelQuery({
794
- dimensions,
795
- measures: [...measures, ...extraMeasures],
727
+ dimensions: queryDimensions,
728
+ measures: [
729
+ ...measures,
730
+ ...extraMeasures,
731
+ ...outside.filter((field) => meta?.measures[field] && !meta.dimensions[field])
732
+ ],
796
733
  ignoreFilters,
797
- label: name
734
+ label: name,
735
+ skip: waitingMeta
798
736
  });
799
737
  const totals = useModelQuery({
800
738
  measures,
@@ -815,7 +753,7 @@ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDime
815
753
  direction: sortDirection
816
754
  });
817
755
  }
818
- const sortKey = sort.by?.startsWith(PERCENT) ? sort.by.slice(2) : sort.by;
756
+ const sortKey = sort.by?.startsWith("%:") ? sort.by.slice(2) : sort.by;
819
757
  const sorted = sortKey ? [...rows].sort((a, b) => {
820
758
  const [x, y] = [a[sortKey], b[sortKey]];
821
759
  if (x === null || x === void 0) return 1;
@@ -827,7 +765,7 @@ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDime
827
765
  const dimensionText = (dimension, value) => value === null || value === void 0 ? "(vazio)" : (formatDimensions[dimension] ?? String)(value);
828
766
  const labelOf = (column) => fieldLabel(meta, column, labels);
829
767
  const headerOf = (column) => {
830
- if (!column.startsWith(PERCENT)) return labelOf(column);
768
+ if (!column.startsWith("%:")) return labelOf(column);
831
769
  const measure = column.slice(2);
832
770
  return measures.includes(measure) ? "%" : `% ${labelOf(measure)}`;
833
771
  };
@@ -837,8 +775,25 @@ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDime
837
775
  const total = sums[measure];
838
776
  return value === null || value === void 0 || !total ? null : Number(value) / total;
839
777
  };
840
- const valueColumns = [...measures.flatMap((measure) => percentOf.includes(measure) ? [measure, `${PERCENT}${measure}`] : [measure]), ...extraMeasures.map((measure) => `${PERCENT}${measure}`)];
841
- const cellText = (row, column) => column.startsWith(PERCENT) ? formatPercent(percentOfRow(row, column.slice(2))) : formats[column]?.(row[column]);
778
+ const columns = tableColumns([...dimensions, ...measures], percentOf.filter((measure) => !dimensions.includes(measure)), columnOrder);
779
+ const ruleStyles = (row) => {
780
+ if (!formatRules.length) return void 0;
781
+ const evaluated = { ...row };
782
+ for (const measure of percentOf) evaluated[percentColumn(measure)] = percentOfRow(row, measure);
783
+ return rowRuleStyles(formatRules, evaluated, columns);
784
+ };
785
+ const ruleSx = (style) => style && {
786
+ ...style.color && { color: style.color },
787
+ ...style.background && { bgcolor: style.background },
788
+ ...style.bold && { fontWeight: 700 },
789
+ ...style.align && { textAlign: style.align },
790
+ ...style.verticalAlign && { verticalAlign: style.verticalAlign }
791
+ };
792
+ const isDimension = (column) => dimensions.includes(column);
793
+ const alignOf = (column) => align?.[column] ?? (isDimension(column) ? "left" : "right");
794
+ const verticalOf = (column) => verticalAlign?.[column] ?? "middle";
795
+ const totalLabelColumn = columns.find(isDimension);
796
+ const cellText = (row, column) => column.startsWith("%:") ? formatPercent(percentOfRow(row, column.slice(2))) : formats[column]?.(row[column]);
842
797
  const toggleSort = (column) => setSort((current) => current.by === column ? {
843
798
  by: column,
844
799
  direction: current.direction === "asc" ? "desc" : "asc"
@@ -847,19 +802,21 @@ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDime
847
802
  direction: dimensions.includes(column) ? "asc" : "desc"
848
803
  });
849
804
  const totalRow = dimensions.length ? totals.rows[0] : void 0;
805
+ const loading = isLoading || waitingMeta;
850
806
  const placeholder = chartPlaceholder({
851
807
  height: 160,
852
808
  error,
853
- isLoading,
809
+ isLoading: loading,
854
810
  empty: !rows.length
855
811
  });
856
812
  const header = (column, numeric) => /* @__PURE__ */ jsx(TableCell, {
857
- align: numeric ? "right" : "left",
813
+ align: alignOf(column),
858
814
  sortDirection: sort.by === column ? sort.direction : false,
859
815
  sx: {
860
816
  fontWeight: 700,
861
817
  bgcolor: "#f3f3f3",
862
- whiteSpace: "nowrap"
818
+ whiteSpace: "nowrap",
819
+ verticalAlign: verticalOf(column)
863
820
  },
864
821
  children: /* @__PURE__ */ jsx(TableSortLabel, {
865
822
  active: sort.by === column,
@@ -874,7 +831,7 @@ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDime
874
831
  minWidth: 0,
875
832
  position: "relative"
876
833
  },
877
- children: [isLoading && /* @__PURE__ */ jsx(CircularProgress, {
834
+ children: [loading && /* @__PURE__ */ jsx(CircularProgress, {
878
835
  size: 14,
879
836
  sx: {
880
837
  position: "absolute",
@@ -885,17 +842,21 @@ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDime
885
842
  sx: {
886
843
  maxHeight: height,
887
844
  overflow: "auto",
888
- opacity: isLoading ? .5 : 1,
845
+ opacity: loading ? .5 : 1,
889
846
  transition: "opacity .2s"
890
847
  },
891
848
  children: /* @__PURE__ */ jsxs(Table, {
892
849
  size: "small",
893
850
  stickyHeader: true,
894
- sx: { "& td, & th": { fontSize: 13 } },
895
- children: [/* @__PURE__ */ jsx(TableHead, { children: /* @__PURE__ */ jsxs(TableRow, { children: [dimensions.map((dimension) => header(dimension, false)), valueColumns.map((column) => header(column, true))] }) }), /* @__PURE__ */ jsxs(TableBody, { children: [sorted.map((row) => {
851
+ sx: { "& td, & th": {
852
+ fontSize: 13,
853
+ ...cellBorders && { border: `1px solid ${borderColor}` }
854
+ } },
855
+ children: [/* @__PURE__ */ jsx(TableHead, { children: /* @__PURE__ */ jsx(TableRow, { children: columns.map((column) => header(column, !isDimension(column))) }) }), /* @__PURE__ */ jsxs(TableBody, { children: [sorted.map((row) => {
896
856
  const value = firstDimension ? row[firstDimension] : null;
897
857
  const clickable = clickToFilter && value !== null && value !== void 0;
898
- return /* @__PURE__ */ jsxs(TableRow, {
858
+ const styles = ruleStyles(row);
859
+ return /* @__PURE__ */ jsx(TableRow, {
899
860
  hover: true,
900
861
  onClick: clickable ? (event) => filter.toggle(value, event) : void 0,
901
862
  selected: clickable && filter.selected.includes(value),
@@ -903,13 +864,24 @@ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDime
903
864
  cursor: clickable ? "pointer" : void 0,
904
865
  opacity: clickable && filter.isDimmed(value) ? .5 : 1
905
866
  },
906
- children: [dimensions.map((dimension) => /* @__PURE__ */ jsx(TableCell, { children: dimensionText(dimension, row[dimension]) }, dimension)), valueColumns.map((column) => /* @__PURE__ */ jsx(TableCell, {
907
- align: "right",
908
- sx: { whiteSpace: "nowrap" },
867
+ children: columns.map((column) => isDimension(column) ? /* @__PURE__ */ jsx(TableCell, {
868
+ align: alignOf(column),
869
+ sx: {
870
+ verticalAlign: verticalOf(column),
871
+ ...ruleSx(styles?.[column])
872
+ },
873
+ children: dimensionText(column, row[column])
874
+ }, column) : /* @__PURE__ */ jsx(TableCell, {
875
+ align: alignOf(column),
876
+ sx: {
877
+ whiteSpace: "nowrap",
878
+ verticalAlign: verticalOf(column),
879
+ ...ruleSx(styles?.[column])
880
+ },
909
881
  children: cellText(row, column)
910
- }, column))]
911
- }, dimensions.map((dimension) => String(row[dimension])).join("\0"));
912
- }), totalRow && /* @__PURE__ */ jsxs(TableRow, {
882
+ }, column))
883
+ }, queryDimensions.map((dimension) => String(row[dimension])).join("\0"));
884
+ }), totalRow && /* @__PURE__ */ jsx(TableRow, {
913
885
  sx: { "& td": {
914
886
  fontWeight: 700,
915
887
  position: "sticky",
@@ -917,11 +889,18 @@ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDime
917
889
  bgcolor: "#f3f3f3",
918
890
  borderTop: "2px solid #ddd"
919
891
  } },
920
- children: [dimensions.map((dimension, index) => /* @__PURE__ */ jsx(TableCell, { children: index === 0 ? "Total" : "" }, dimension)), valueColumns.map((column) => /* @__PURE__ */ jsx(TableCell, {
921
- align: "right",
922
- sx: { whiteSpace: "nowrap" },
923
- children: column.startsWith(PERCENT) ? formatPercent(sums[column.slice(2)] ? 1 : null) : formats[column]?.(totalRow[column])
924
- }, column))]
892
+ children: columns.map((column) => isDimension(column) ? /* @__PURE__ */ jsx(TableCell, {
893
+ align: alignOf(column),
894
+ sx: { verticalAlign: verticalOf(column) },
895
+ children: column === totalLabelColumn ? "Total" : ""
896
+ }, column) : /* @__PURE__ */ jsx(TableCell, {
897
+ align: alignOf(column),
898
+ sx: {
899
+ whiteSpace: "nowrap",
900
+ verticalAlign: verticalOf(column)
901
+ },
902
+ children: column.startsWith("%:") ? formatPercent(sums[column.slice(2)] ? 1 : null) : formats[column]?.(totalRow[column])
903
+ }, column))
925
904
  })] })]
926
905
  })
927
906
  })]