@bicharts/chart-host 0.5.31 → 0.5.33
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.
- package/dist/index.mjs +166 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/selectionCard.d.ts +53 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -569,6 +569,170 @@ function qualifyGroupHeadingFor(row, state) {
|
|
|
569
569
|
return null;
|
|
570
570
|
}
|
|
571
571
|
|
|
572
|
+
// src/selectionCard.ts
|
|
573
|
+
var SYNTHETIC_PREFIX = "__";
|
|
574
|
+
var NUMERIC_DATATYPE = /int|double|decimal|single|float|number|currency|money/i;
|
|
575
|
+
function normaliseAggregation(raw) {
|
|
576
|
+
const s = String(raw ?? "").trim().toLowerCase();
|
|
577
|
+
if (!s) return "sum";
|
|
578
|
+
if (/^(avg|average|mean)$/.test(s)) return "average";
|
|
579
|
+
if (/^(min|minimum)$/.test(s)) return "min";
|
|
580
|
+
if (/^(max|maximum)$/.test(s)) return "max";
|
|
581
|
+
if (/^(count|countrows)$/.test(s)) return "count";
|
|
582
|
+
if (/^(median|med)$/.test(s)) return "median";
|
|
583
|
+
if (/^(distinctcount|dcount|countdistinct|uniques?)$/.test(s)) return "distinctcount";
|
|
584
|
+
return "sum";
|
|
585
|
+
}
|
|
586
|
+
function isAdditive(agg) {
|
|
587
|
+
return agg === "sum" || agg === "count";
|
|
588
|
+
}
|
|
589
|
+
function aggLabel(agg) {
|
|
590
|
+
switch (agg) {
|
|
591
|
+
case "average":
|
|
592
|
+
return "Average";
|
|
593
|
+
case "min":
|
|
594
|
+
return "Min";
|
|
595
|
+
case "max":
|
|
596
|
+
return "Max";
|
|
597
|
+
case "count":
|
|
598
|
+
return "Count";
|
|
599
|
+
case "median":
|
|
600
|
+
return "Median";
|
|
601
|
+
case "distinctcount":
|
|
602
|
+
return "Distinct";
|
|
603
|
+
default:
|
|
604
|
+
return "Sum";
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function isSynthetic(name) {
|
|
608
|
+
return String(name ?? "").startsWith(SYNTHETIC_PREFIX);
|
|
609
|
+
}
|
|
610
|
+
function isMeasureColumn(col) {
|
|
611
|
+
if (!col || isSynthetic(col.name)) return false;
|
|
612
|
+
if (col.isMeasure === true) return true;
|
|
613
|
+
return NUMERIC_DATATYPE.test(String(col.dataType ?? ""));
|
|
614
|
+
}
|
|
615
|
+
function toNumber(v) {
|
|
616
|
+
if (v == null || v === "") return null;
|
|
617
|
+
const n = typeof v === "number" ? v : Number(v);
|
|
618
|
+
return Number.isFinite(n) ? n : null;
|
|
619
|
+
}
|
|
620
|
+
function reduce(values, agg, distinctRaw) {
|
|
621
|
+
if (agg === "count") return values.length;
|
|
622
|
+
if (agg === "distinctcount") return new Set(distinctRaw.map((v) => v == null ? "\0" : String(v))).size;
|
|
623
|
+
if (!values.length) return null;
|
|
624
|
+
switch (agg) {
|
|
625
|
+
case "average":
|
|
626
|
+
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
627
|
+
case "min":
|
|
628
|
+
return Math.min(...values);
|
|
629
|
+
case "max":
|
|
630
|
+
return Math.max(...values);
|
|
631
|
+
case "median": {
|
|
632
|
+
const s = [...values].sort((a, b) => a - b);
|
|
633
|
+
const m = s.length >> 1;
|
|
634
|
+
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
|
635
|
+
}
|
|
636
|
+
default:
|
|
637
|
+
return values.reduce((a, b) => a + b, 0);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function fmtNumber(v, culture) {
|
|
641
|
+
try {
|
|
642
|
+
const abs = Math.abs(v);
|
|
643
|
+
const opts = abs >= 1e6 ? { notation: "compact", maximumFractionDigits: 1 } : { maximumFractionDigits: abs < 10 ? 2 : 0 };
|
|
644
|
+
return new Intl.NumberFormat(culture || void 0, opts).format(v);
|
|
645
|
+
} catch {
|
|
646
|
+
return String(Math.round(v * 100) / 100);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function computeSelectionCard(payload, selectedRowIdxs, opts = {}) {
|
|
650
|
+
const columns = payload?.columns ?? [];
|
|
651
|
+
const rows = payload?.rows ?? [];
|
|
652
|
+
if (!columns.length || !rows.length) return null;
|
|
653
|
+
const wanted = Array.from(new Set((selectedRowIdxs ?? []).filter((n) => Number.isInteger(n))));
|
|
654
|
+
if (!wanted.length) return null;
|
|
655
|
+
const rowIdxCol = columns.findIndex((c) => c?.name === "__rowIdx__");
|
|
656
|
+
let byIdx = null;
|
|
657
|
+
if (rowIdxCol >= 0) {
|
|
658
|
+
byIdx = /* @__PURE__ */ new Map();
|
|
659
|
+
for (let r = 0; r < rows.length; r++) {
|
|
660
|
+
const v = toNumber(rows[r]?.[rowIdxCol]);
|
|
661
|
+
if (v != null) byIdx.set(v, r);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
const positions = [];
|
|
665
|
+
for (const idx of wanted) {
|
|
666
|
+
const pos = byIdx ? byIdx.get(idx) : idx;
|
|
667
|
+
if (pos != null && pos >= 0 && pos < rows.length) positions.push(pos);
|
|
668
|
+
}
|
|
669
|
+
if (!positions.length) return null;
|
|
670
|
+
const agg = normaliseAggregation(opts.aggregation);
|
|
671
|
+
const maxMeasures = opts.maxMeasures ?? 4;
|
|
672
|
+
const maxHeaderValues = opts.maxHeaderValues ?? 3;
|
|
673
|
+
const dimIdx = columns.findIndex((c) => c && !isSynthetic(c.name) && !isMeasureColumn(c));
|
|
674
|
+
let header;
|
|
675
|
+
if (dimIdx >= 0) {
|
|
676
|
+
const seen = [];
|
|
677
|
+
const set = /* @__PURE__ */ new Set();
|
|
678
|
+
for (const p of positions) {
|
|
679
|
+
const raw = rows[p]?.[dimIdx];
|
|
680
|
+
const s = raw == null || raw === "" ? "(blank)" : String(raw);
|
|
681
|
+
if (!set.has(s)) {
|
|
682
|
+
set.add(s);
|
|
683
|
+
seen.push(s);
|
|
684
|
+
}
|
|
685
|
+
if (seen.length > maxHeaderValues) break;
|
|
686
|
+
}
|
|
687
|
+
header = seen.length > maxHeaderValues ? `${positions.length} marks` : seen.join(", ");
|
|
688
|
+
} else {
|
|
689
|
+
header = `${positions.length} mark${positions.length === 1 ? "" : "s"}`;
|
|
690
|
+
}
|
|
691
|
+
const measureCols = [];
|
|
692
|
+
for (let c = 0; c < columns.length; c++) if (isMeasureColumn(columns[c])) measureCols.push(c);
|
|
693
|
+
const shown = measureCols.slice(0, maxMeasures);
|
|
694
|
+
const lines = [];
|
|
695
|
+
for (const c of shown) {
|
|
696
|
+
const selVals = [];
|
|
697
|
+
const selRaw = [];
|
|
698
|
+
for (const p of positions) {
|
|
699
|
+
const n = toNumber(rows[p]?.[c]);
|
|
700
|
+
selRaw.push(rows[p]?.[c]);
|
|
701
|
+
if (n != null) selVals.push(n);
|
|
702
|
+
}
|
|
703
|
+
const value = reduce(selVals, agg, selRaw);
|
|
704
|
+
let sharePct = null;
|
|
705
|
+
if (isAdditive(agg) && value != null) {
|
|
706
|
+
const allVals = [];
|
|
707
|
+
const allRaw = [];
|
|
708
|
+
for (let r = 0; r < rows.length; r++) {
|
|
709
|
+
const n = toNumber(rows[r]?.[c]);
|
|
710
|
+
allRaw.push(rows[r]?.[c]);
|
|
711
|
+
if (n != null) allVals.push(n);
|
|
712
|
+
}
|
|
713
|
+
const total = reduce(allVals, agg, allRaw);
|
|
714
|
+
if (total != null && total > 0) sharePct = value / total * 100;
|
|
715
|
+
}
|
|
716
|
+
lines.push({
|
|
717
|
+
column: String(columns[c]?.name ?? ""),
|
|
718
|
+
label: `${aggLabel(agg)} of ${columns[c]?.name ?? ""}`,
|
|
719
|
+
value,
|
|
720
|
+
valueText: value == null ? "\u2014" : fmtNumber(value, opts.cultureCode),
|
|
721
|
+
sharePct,
|
|
722
|
+
shareText: sharePct == null ? "" : `${sharePct < 0.1 ? "<0.1" : sharePct.toFixed(1)}% of total`
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
return {
|
|
726
|
+
header,
|
|
727
|
+
lines,
|
|
728
|
+
rowsText: `${positions.length} of ${rows.length} rows`,
|
|
729
|
+
selectedRows: positions.length,
|
|
730
|
+
totalRows: rows.length,
|
|
731
|
+
hiddenMeasures: Math.max(0, measureCols.length - shown.length),
|
|
732
|
+
aggregation: agg
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
572
736
|
// src/index.ts
|
|
573
737
|
function registerCityTable(packed) {
|
|
574
738
|
L3(packed);
|
|
@@ -610,6 +774,7 @@ export {
|
|
|
610
774
|
clearGeoCache,
|
|
611
775
|
compileRenderFn,
|
|
612
776
|
compileTrivialSource,
|
|
777
|
+
computeSelectionCard,
|
|
613
778
|
createChartHost,
|
|
614
779
|
createMarkResolver,
|
|
615
780
|
explainRenderFailure,
|
|
@@ -617,6 +782,7 @@ export {
|
|
|
617
782
|
geoFromCache,
|
|
618
783
|
loadGeo,
|
|
619
784
|
newQualifyGroupState,
|
|
785
|
+
normaliseAggregation,
|
|
620
786
|
planTrivialChart,
|
|
621
787
|
qualifyGroupHeadingFor,
|
|
622
788
|
rasterizeSvgToPngDataUrl,
|
package/dist/types/index.d.ts
CHANGED
|
@@ -12,3 +12,4 @@ export { captureSvgSnapshot, svgToDataUrl, svgNaturalSize, rasterizeSvgToPngData
|
|
|
12
12
|
export { shouldReview, buildReviewWire, bareBase64, actionFor, type ReviewGate, type ReviewWire, type ReviewVerdict, type ReviewAction } from "./review";
|
|
13
13
|
export { askApplyImprovements, type ReviewDialogOptions, type ReviewDialogText } from "./reviewDialog";
|
|
14
14
|
export { qualifyGroupHeadingFor, newQualifyGroupState, type QualifyGroupRow, type QualifyGroupState, type QualifyGroupHeading, } from "./qualifyGroups";
|
|
15
|
+
export { computeSelectionCard, normaliseAggregation, type SelectionCardModel, type SelectionCardLine, type SelectionCardOptions, } from "./selectionCard";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { RenderPayload } from "./payload";
|
|
2
|
+
export interface SelectionCardOptions {
|
|
3
|
+
/** The chart's own aggregation, so the card AGREES with the picture it sits on. A card
|
|
4
|
+
* reading "Sum" beside a chart drawn from averages is two answers to one question. */
|
|
5
|
+
aggregation?: string;
|
|
6
|
+
cultureCode?: string;
|
|
7
|
+
/** Cards are capped at roughly a third of the tile, so the measure list is capped too.
|
|
8
|
+
* `hiddenMeasures` reports what was dropped rather than truncating silently. */
|
|
9
|
+
maxMeasures?: number;
|
|
10
|
+
/** Past this many distinct dimension values the header becomes "N marks" — naming twelve
|
|
11
|
+
* cities in a header is not a header. */
|
|
12
|
+
maxHeaderValues?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface SelectionCardLine {
|
|
15
|
+
column: string;
|
|
16
|
+
/** "Sum of Revenue" — the aggregation is named because it is not always sum. */
|
|
17
|
+
label: string;
|
|
18
|
+
value: number | null;
|
|
19
|
+
valueText: string;
|
|
20
|
+
/** Share of the same aggregate over ALL rows. Null whenever a share would be a lie — see
|
|
21
|
+
* isAdditive: the average of a subset is not a percentage of the average of the whole. */
|
|
22
|
+
sharePct: number | null;
|
|
23
|
+
shareText: string;
|
|
24
|
+
}
|
|
25
|
+
export interface SelectionCardModel {
|
|
26
|
+
header: string;
|
|
27
|
+
lines: SelectionCardLine[];
|
|
28
|
+
/** "6 of 24 rows" — the one line that is true regardless of what the columns contain. */
|
|
29
|
+
rowsText: string;
|
|
30
|
+
selectedRows: number;
|
|
31
|
+
totalRows: number;
|
|
32
|
+
/** Measures the cap left out, so a host can say "+2 more" instead of implying there were none. */
|
|
33
|
+
hiddenMeasures: number;
|
|
34
|
+
aggregation: string;
|
|
35
|
+
}
|
|
36
|
+
type Agg = "sum" | "average" | "min" | "max" | "count" | "median" | "distinctcount";
|
|
37
|
+
/** Normalise the many spellings the hosts use into the set this file reduces over. Unknown
|
|
38
|
+
* spellings fall back to sum rather than throwing: a card is a courtesy, and refusing to draw
|
|
39
|
+
* one because a host wrote "Total" is a worse outcome than summing. */
|
|
40
|
+
export declare function normaliseAggregation(raw: string | undefined | null): Agg;
|
|
41
|
+
/**
|
|
42
|
+
* The card for a selection, or NULL when there is nothing to say.
|
|
43
|
+
*
|
|
44
|
+
* `selectedRowIdxs` are the values a mark carries in `data-row-idx` — which ARE row positions,
|
|
45
|
+
* because `buildRenderPayload` writes `__rowIdx__` as the row's own index. The mapping is
|
|
46
|
+
* resolved through that column when it is present rather than assumed, so a payload that ever
|
|
47
|
+
* reorders rows keeps working.
|
|
48
|
+
*
|
|
49
|
+
* Null (rather than an empty model) when the selection is empty, because "no selection" and "a
|
|
50
|
+
* selection that sums to nothing" are different states and only the first should dismiss the card.
|
|
51
|
+
*/
|
|
52
|
+
export declare function computeSelectionCard(payload: RenderPayload | null | undefined, selectedRowIdxs: number[] | null | undefined, opts?: SelectionCardOptions): SelectionCardModel | null;
|
|
53
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bicharts/chart-host",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.33",
|
|
4
4
|
"description": "Run a BIC-generated D3 chart in any web host: compiles the generated render() function, applies the shared option defaults, resolves mark clicks (through tooltip overlays), owns the selection affordance, and translates row indices between cross-filtered charts. The same contract the BIC Power BI visual implements, minus Power BI. React bindings at @bicharts/chart-host/react.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|