@bluemarble/bi 0.5.0 → 0.6.1

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,4 +1,4 @@
1
- import { C as ChartTypeSpec, T as ChartsLayout, U as ModelDef, W as ModelMeta, a as DashboardResult, b as ChartDef, i as DashboardQueryDebug, l as FilterValue, n as DashboardFilters$1, o as DashboardRow, r as DashboardQuery, v as CardStyle, w as ChartTypes, x as ChartNumberFormat } from "../types-FdrY0FOZ.js";
1
+ import { A as DashboardRow, C as ColumnVerticalAlign, D as DashboardQuery, E as DashboardFilters$1, K as ModelDef, N as FilterValue, O as DashboardQueryDebug, S as ColumnAlign, d as ChartsLayout, i as CardStyle, k as DashboardResult, l as ChartTypeSpec, o as ChartDef, q as ModelMeta, s as ChartNumberFormat, u as ChartTypes, w as FormatRule } from "../layout-B7eYgNyR.js";
2
2
  import { ComponentType, MouseEvent, ReactNode } from "react";
3
3
  //#region src/client/BarChart.d.ts
4
4
  type BarChartProps = {
@@ -370,7 +370,11 @@ type DataTableProps = {
370
370
  * linhas.
371
371
  */
372
372
  showTotal?: boolean;
373
- /** Coluna da ordem inicial (dimensão ou medida); quem usa reordena clicando no cabeçalho. */
373
+ /**
374
+ * Coluna da ordem inicial (dimensão, medida ou `%:medida`); quem usa reordena clicando no
375
+ * cabeçalho. Dimensão/medida fora da tabela entra na consulta sem coluna (ex.: número do mês para
376
+ * ordenar o nome do mês) — uma dimensão de fora divide as linhas se tiver mais de um valor por linha.
377
+ */
374
378
  sortBy?: string;
375
379
  sortDirection?: SortDirection;
376
380
  /** Clicar numa linha filtra o dashboard pela primeira dimensão. */
@@ -384,10 +388,27 @@ type DataTableProps = {
384
388
  /** Casas decimais das colunas de percentual. Padrão: 1. */
385
389
  percentDecimals?: number;
386
390
  /**
387
- * Ordem das colunas misturando dimensões e medidas (ex.: `['custoTotal', 'navio']`); o que não
388
- * estiver nela vai depois. Padrão: dimensões e depois medidas.
391
+ * Ordem das colunas misturando dimensões, medidas e percentuais (ex.: `['custoTotal',
392
+ * '%:custoTotal', 'navio']`); o que não estiver nela vai depois. Padrão: dimensões e depois
393
+ * medidas, cada percentual logo depois da medida.
389
394
  */
390
395
  columnOrder?: string[];
396
+ /**
397
+ * Formatação condicional: cor do texto/fundo e negrito da célula ou da linha conforme o valor de
398
+ * uma dimensão/medida (da tabela, de fora dela ou `%:medida`).
399
+ */
400
+ formatRules?: FormatRule[];
401
+ /** Bordas entre as células (grade). Padrão: só a linha embaixo de cada linha. */
402
+ cellBorders?: boolean;
403
+ /** Cor das bordas da grade. Padrão: `#e0e0e0`. */
404
+ borderColor?: string;
405
+ /**
406
+ * Alinhamento horizontal por coluna (`%:medida` para o percentual): cabeçalho, células e total.
407
+ * Padrão: dimensão à esquerda, número à direita.
408
+ */
409
+ align?: Record<string, ColumnAlign>;
410
+ /** Alinhamento vertical por coluna (`top`, `middle`, `bottom`). Padrão: `middle`. */
411
+ verticalAlign?: Record<string, ColumnVerticalAlign>;
391
412
  /** Nome da consulta no devtools. */
392
413
  queryLabel?: string;
393
414
  };
@@ -395,7 +416,7 @@ type DataTableProps = {
395
416
  * Tabela de medidas por dimensões, com total e ordenação pelo cabeçalho. Ex.:
396
417
  * `<DataTable dimensions={['navio']} measures={['volTotal', 'custoTotal', 'custoTon']} />`.
397
418
  */
398
- declare function DataTable({ dimensions, measures, percentOf, formatDimensions, showTotal, sortBy, sortDirection, clickToFilter, height, labels, formats: formatsByMeasure, percentDecimals, columnOrder, queryLabel }: DataTableProps): import("react").JSX.Element;
419
+ declare function DataTable({ dimensions, measures, percentOf, formatDimensions, showTotal, sortBy, sortDirection, clickToFilter, height, labels, formats: formatsByMeasure, percentDecimals, columnOrder, formatRules, cellBorders, borderColor, align, verticalAlign, queryLabel }: DataTableProps): import("react").JSX.Element;
399
420
  //#endregion
400
421
  //#region src/client/DonutChart.d.ts
401
422
  type DonutChartProps = {
@@ -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-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-B26oLtPH.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
  })