@bluemarble/bi 0.5.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, h as orderedColumns, o as chartCardStyle, u as customChartProps } from "../layout-QuOYFmp1.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-D3JzCd6x.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";
@@ -705,8 +705,7 @@ function DashboardCanvas({ layout, children }) {
705
705
  //#endregion
706
706
  //#region src/client/DataTable.tsx
707
707
  const EMPTY = [];
708
- /** Chave da coluna de percentual de uma medida (ordenação). */
709
- const PERCENT = "%:";
708
+ const NO_RULES = [];
710
709
  function compare(a, b) {
711
710
  if (typeof a === "number" && typeof b === "number") return a - b;
712
711
  return String(a).localeCompare(String(b), "pt-BR", { numeric: true });
@@ -715,17 +714,25 @@ function compare(a, b) {
715
714
  * Tabela de medidas por dimensões, com total e ordenação pelo cabeçalho. Ex.:
716
715
  * `<DataTable dimensions={['navio']} measures={['volTotal', 'custoTotal', 'custoTon']} />`.
717
716
  */
718
- function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDimensions = {}, showTotal = true, sortBy, sortDirection = "desc", clickToFilter = false, height = 400, labels, formats: formatsByMeasure, percentDecimals = 1, columnOrder, 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 }) {
719
718
  const { meta } = useDashboard();
720
719
  const name = queryLabel ?? `DataTable · ${dimensions.join(", ") || "total"}`;
721
720
  const firstDimension = dimensions[0];
722
721
  const ignoreFilters = clickToFilter && firstDimension ? [firstDimension] : void 0;
723
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])];
724
726
  const { rows, isLoading, error, inspectProps } = useModelQuery({
725
- dimensions,
726
- measures: [...measures, ...extraMeasures],
727
+ dimensions: queryDimensions,
728
+ measures: [
729
+ ...measures,
730
+ ...extraMeasures,
731
+ ...outside.filter((field) => meta?.measures[field] && !meta.dimensions[field])
732
+ ],
727
733
  ignoreFilters,
728
- label: name
734
+ label: name,
735
+ skip: waitingMeta
729
736
  });
730
737
  const totals = useModelQuery({
731
738
  measures,
@@ -746,7 +753,7 @@ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDime
746
753
  direction: sortDirection
747
754
  });
748
755
  }
749
- const sortKey = sort.by?.startsWith(PERCENT) ? sort.by.slice(2) : sort.by;
756
+ const sortKey = sort.by?.startsWith("%:") ? sort.by.slice(2) : sort.by;
750
757
  const sorted = sortKey ? [...rows].sort((a, b) => {
751
758
  const [x, y] = [a[sortKey], b[sortKey]];
752
759
  if (x === null || x === void 0) return 1;
@@ -758,7 +765,7 @@ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDime
758
765
  const dimensionText = (dimension, value) => value === null || value === void 0 ? "(vazio)" : (formatDimensions[dimension] ?? String)(value);
759
766
  const labelOf = (column) => fieldLabel(meta, column, labels);
760
767
  const headerOf = (column) => {
761
- if (!column.startsWith(PERCENT)) return labelOf(column);
768
+ if (!column.startsWith("%:")) return labelOf(column);
762
769
  const measure = column.slice(2);
763
770
  return measures.includes(measure) ? "%" : `% ${labelOf(measure)}`;
764
771
  };
@@ -768,10 +775,25 @@ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDime
768
775
  const total = sums[measure];
769
776
  return value === null || value === void 0 || !total ? null : Number(value) / total;
770
777
  };
771
- const columns = [...orderedColumns([...dimensions, ...measures], columnOrder).flatMap((column) => percentOf.includes(column) && !dimensions.includes(column) ? [column, `${PERCENT}${column}`] : [column]), ...extraMeasures.map((measure) => `${PERCENT}${measure}`)];
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
+ };
772
792
  const isDimension = (column) => dimensions.includes(column);
793
+ const alignOf = (column) => align?.[column] ?? (isDimension(column) ? "left" : "right");
794
+ const verticalOf = (column) => verticalAlign?.[column] ?? "middle";
773
795
  const totalLabelColumn = columns.find(isDimension);
774
- const cellText = (row, column) => column.startsWith(PERCENT) ? formatPercent(percentOfRow(row, column.slice(2))) : formats[column]?.(row[column]);
796
+ const cellText = (row, column) => column.startsWith("%:") ? formatPercent(percentOfRow(row, column.slice(2))) : formats[column]?.(row[column]);
775
797
  const toggleSort = (column) => setSort((current) => current.by === column ? {
776
798
  by: column,
777
799
  direction: current.direction === "asc" ? "desc" : "asc"
@@ -780,19 +802,21 @@ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDime
780
802
  direction: dimensions.includes(column) ? "asc" : "desc"
781
803
  });
782
804
  const totalRow = dimensions.length ? totals.rows[0] : void 0;
805
+ const loading = isLoading || waitingMeta;
783
806
  const placeholder = chartPlaceholder({
784
807
  height: 160,
785
808
  error,
786
- isLoading,
809
+ isLoading: loading,
787
810
  empty: !rows.length
788
811
  });
789
812
  const header = (column, numeric) => /* @__PURE__ */ jsx(TableCell, {
790
- align: numeric ? "right" : "left",
813
+ align: alignOf(column),
791
814
  sortDirection: sort.by === column ? sort.direction : false,
792
815
  sx: {
793
816
  fontWeight: 700,
794
817
  bgcolor: "#f3f3f3",
795
- whiteSpace: "nowrap"
818
+ whiteSpace: "nowrap",
819
+ verticalAlign: verticalOf(column)
796
820
  },
797
821
  children: /* @__PURE__ */ jsx(TableSortLabel, {
798
822
  active: sort.by === column,
@@ -807,7 +831,7 @@ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDime
807
831
  minWidth: 0,
808
832
  position: "relative"
809
833
  },
810
- children: [isLoading && /* @__PURE__ */ jsx(CircularProgress, {
834
+ children: [loading && /* @__PURE__ */ jsx(CircularProgress, {
811
835
  size: 14,
812
836
  sx: {
813
837
  position: "absolute",
@@ -818,16 +842,20 @@ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDime
818
842
  sx: {
819
843
  maxHeight: height,
820
844
  overflow: "auto",
821
- opacity: isLoading ? .5 : 1,
845
+ opacity: loading ? .5 : 1,
822
846
  transition: "opacity .2s"
823
847
  },
824
848
  children: /* @__PURE__ */ jsxs(Table, {
825
849
  size: "small",
826
850
  stickyHeader: true,
827
- sx: { "& td, & th": { fontSize: 13 } },
851
+ sx: { "& td, & th": {
852
+ fontSize: 13,
853
+ ...cellBorders && { border: `1px solid ${borderColor}` }
854
+ } },
828
855
  children: [/* @__PURE__ */ jsx(TableHead, { children: /* @__PURE__ */ jsx(TableRow, { children: columns.map((column) => header(column, !isDimension(column))) }) }), /* @__PURE__ */ jsxs(TableBody, { children: [sorted.map((row) => {
829
856
  const value = firstDimension ? row[firstDimension] : null;
830
857
  const clickable = clickToFilter && value !== null && value !== void 0;
858
+ const styles = ruleStyles(row);
831
859
  return /* @__PURE__ */ jsx(TableRow, {
832
860
  hover: true,
833
861
  onClick: clickable ? (event) => filter.toggle(value, event) : void 0,
@@ -836,12 +864,23 @@ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDime
836
864
  cursor: clickable ? "pointer" : void 0,
837
865
  opacity: clickable && filter.isDimmed(value) ? .5 : 1
838
866
  },
839
- children: columns.map((column) => isDimension(column) ? /* @__PURE__ */ jsx(TableCell, { children: dimensionText(column, row[column]) }, column) : /* @__PURE__ */ jsx(TableCell, {
840
- align: "right",
841
- 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
+ },
842
881
  children: cellText(row, column)
843
882
  }, column))
844
- }, dimensions.map((dimension) => String(row[dimension])).join("\0"));
883
+ }, queryDimensions.map((dimension) => String(row[dimension])).join("\0"));
845
884
  }), totalRow && /* @__PURE__ */ jsx(TableRow, {
846
885
  sx: { "& td": {
847
886
  fontWeight: 700,
@@ -850,10 +889,17 @@ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDime
850
889
  bgcolor: "#f3f3f3",
851
890
  borderTop: "2px solid #ddd"
852
891
  } },
853
- children: columns.map((column) => isDimension(column) ? /* @__PURE__ */ jsx(TableCell, { children: column === totalLabelColumn ? "Total" : "" }, column) : /* @__PURE__ */ jsx(TableCell, {
854
- align: "right",
855
- sx: { whiteSpace: "nowrap" },
856
- children: column.startsWith(PERCENT) ? formatPercent(sums[column.slice(2)] ? 1 : null) : formats[column]?.(totalRow[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])
857
903
  }, column))
858
904
  })] })]
859
905
  })