@bicharts/chart-host 0.6.51 → 0.6.53

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.
@@ -3518,10 +3518,10 @@ function paintRoot(el, container) {
3518
3518
  }
3519
3519
  return root;
3520
3520
  }
3521
- function drawnBeneath(shape, text, textRoot) {
3521
+ function drawnBeneath(shape, text2, textRoot) {
3522
3522
  if (!shape.root || shape.root !== textRoot) return true;
3523
3523
  if (typeof shape.el.compareDocumentPosition !== "function") return true;
3524
- return (shape.el.compareDocumentPosition(text) & DOC_POSITION_FOLLOWING) !== 0;
3524
+ return (shape.el.compareDocumentPosition(text2) & DOC_POSITION_FOLLOWING) !== 0;
3525
3525
  }
3526
3526
  function sampleBackgrounds(shapes, pts, pageRGB, effAlpha) {
3527
3527
  if (pts.length === 0 || shapes.length === 0) return null;
@@ -3797,13 +3797,13 @@ function applyLabelContrast(container, opts = {}) {
3797
3797
  }
3798
3798
 
3799
3799
  // src/invalidSentinel.ts
3800
- function isInvalidSentinelError(text) {
3801
- if (!text) return false;
3802
- return /\bINVALID\s*:/.test(text);
3800
+ function isInvalidSentinelError(text2) {
3801
+ if (!text2) return false;
3802
+ return /\bINVALID\s*:/.test(text2);
3803
3803
  }
3804
- function invalidSentinelReason(text, max = 240) {
3805
- if (!text) return "";
3806
- const m4 = /\bINVALID\s*:\s*([^\r\n]*)/.exec(text);
3804
+ function invalidSentinelReason(text2, max = 240) {
3805
+ if (!text2) return "";
3806
+ const m4 = /\bINVALID\s*:\s*([^\r\n]*)/.exec(text2);
3807
3807
  if (!m4) return "";
3808
3808
  const reason = m4[1].trim();
3809
3809
  const cap = Math.max(0, Math.floor(max));
@@ -3811,6 +3811,138 @@ function invalidSentinelReason(text, max = 240) {
3811
3811
  return cap <= 3 ? reason.slice(0, cap) : reason.slice(0, cap - 3).trimEnd() + "...";
3812
3812
  }
3813
3813
 
3814
+ // src/annotations.ts
3815
+ var ANNOTATION_LAYER_CLASS = "bic-annotations";
3816
+ var ANNOTATION_CLASS = "bic-annotation";
3817
+ var text = (v) => v == null ? "" : String(v).trim();
3818
+ var parseIdxs = (s2) => (s2 || "").split(",").map((x3) => parseInt(x3, 10)).filter((n) => Number.isFinite(n));
3819
+ function createAnnotationLayer(container, onClick) {
3820
+ let list = [];
3821
+ let report = { shown: 0, notShown: 0 };
3822
+ let last = null;
3823
+ const doc = container.ownerDocument;
3824
+ const win = doc.defaultView;
3825
+ let observer = null;
3826
+ let queued = false;
3827
+ const redrawSoon = () => {
3828
+ if (queued || !last) return;
3829
+ queued = true;
3830
+ const run = () => {
3831
+ queued = false;
3832
+ if (last) paint(last.data, last.theme);
3833
+ };
3834
+ if (win?.requestAnimationFrame) win.requestAnimationFrame(run);
3835
+ else setTimeout(run, 16);
3836
+ };
3837
+ const watch = (on) => {
3838
+ const MO = win?.MutationObserver;
3839
+ if (on && !observer && MO) {
3840
+ observer = new MO((records) => {
3841
+ if (records.some((r2) => !r2.target.closest?.(`.${ANNOTATION_LAYER_CLASS}`))) redrawSoon();
3842
+ });
3843
+ observer.observe(container, { subtree: true, attributes: true, attributeFilter: ["transform", "d", "cx", "cy", "x", "y", "points"] });
3844
+ } else if (!on && observer) {
3845
+ observer.disconnect();
3846
+ observer = null;
3847
+ }
3848
+ };
3849
+ function paint(data, theme) {
3850
+ let layer = container.querySelector(`:scope > .${ANNOTATION_LAYER_CLASS}`);
3851
+ if (!list.length) {
3852
+ layer?.remove();
3853
+ report = { shown: 0, notShown: 0 };
3854
+ return;
3855
+ }
3856
+ if (!layer) {
3857
+ layer = doc.createElement("div");
3858
+ layer.className = ANNOTATION_LAYER_CLASS;
3859
+ layer.setAttribute("aria-hidden", "false");
3860
+ Object.assign(layer.style, { position: "absolute", left: "0", top: "0", width: "0", height: "0", overflow: "visible", zIndex: "2" });
3861
+ if (win?.getComputedStyle(container).position === "static") container.style.position = "relative";
3862
+ container.appendChild(layer);
3863
+ }
3864
+ layer.textContent = "";
3865
+ const names = (data.columns || []).map((c2) => typeof c2 === "string" ? c2 : c2?.name);
3866
+ const marks = Array.from(container.querySelectorAll(`.${MARK_CLASS}[${ROW_IDX_ATTR}]`)).filter((m4) => !m4.closest(`.${ANNOTATION_LAYER_CLASS}`));
3867
+ const marksByRow = /* @__PURE__ */ new Map();
3868
+ for (const m4 of marks) for (const r2 of parseIdxs(m4.getAttribute(ROW_IDX_ATTR))) {
3869
+ const at = marksByRow.get(r2);
3870
+ if (at) at.push(m4);
3871
+ else marksByRow.set(r2, [m4]);
3872
+ }
3873
+ const origin = container.getBoundingClientRect();
3874
+ let shown = 0;
3875
+ for (const a3 of list) {
3876
+ const pairs = (a3.where ? Object.entries(a3.where) : a3.column != null ? [[a3.column, a3.value]] : []).map(([c2, v]) => [names.indexOf(c2), text(v)]);
3877
+ const want = pairs.map((p4) => p4[1]).join(" / ");
3878
+ const hits = /* @__PURE__ */ new Set();
3879
+ if (pairs.length && pairs.every((p4) => p4[0] >= 0)) (data.rows || []).forEach((row, r2) => {
3880
+ if (pairs.every(([ci, v]) => text(row?.[ci]) === v)) for (const m4 of marksByRow.get(r2) ?? []) hits.add(m4);
3881
+ });
3882
+ if (!hits.size) continue;
3883
+ shown++;
3884
+ let best = null;
3885
+ for (const m4 of hits) {
3886
+ const b4 = m4.getBoundingClientRect();
3887
+ if (!best || b4.width * b4.height > best.width * best.height) best = b4;
3888
+ }
3889
+ const badge = doc.createElement("div");
3890
+ badge.className = ANNOTATION_CLASS;
3891
+ badge.textContent = a3.label ?? "\u2022";
3892
+ if (a3.title) badge.setAttribute("title", a3.title);
3893
+ badge.setAttribute("role", "button");
3894
+ badge.setAttribute("aria-label", a3.title ? `Note: ${a3.title}` : `Note on ${want}`);
3895
+ const laidOut = !!best && best.width > 0 && best.height > 0;
3896
+ Object.assign(badge.style, {
3897
+ position: "absolute",
3898
+ left: `${Math.round(laidOut ? best.left - origin.left + best.width / 2 : 0)}px`,
3899
+ top: `${Math.round(laidOut ? best.top - origin.top + best.height / 2 : 0)}px`,
3900
+ transform: "translate(-50%, -50%)",
3901
+ minWidth: "16px",
3902
+ height: "16px",
3903
+ padding: "0 4px",
3904
+ boxSizing: "border-box",
3905
+ borderRadius: "8px",
3906
+ font: "600 10px/16px system-ui, sans-serif",
3907
+ textAlign: "center",
3908
+ color: theme.bg || "#ffffff",
3909
+ background: theme.accent || theme.fg || "#0f6cbd",
3910
+ boxShadow: `0 0 0 1.5px ${theme.bg || "#ffffff"}`,
3911
+ cursor: onClick ? "pointer" : "default",
3912
+ // Never pointer-events (a host failsafe turns it back on); visibility hides a badge whose mark
3913
+ // has no box yet.
3914
+ visibility: laidOut ? "visible" : "hidden"
3915
+ });
3916
+ badge.addEventListener("click", (e) => {
3917
+ e.stopPropagation();
3918
+ onClick?.(a3);
3919
+ });
3920
+ layer.appendChild(badge);
3921
+ }
3922
+ report = { shown, notShown: list.length - shown };
3923
+ }
3924
+ return {
3925
+ set(next) {
3926
+ list = Array.isArray(next) ? next.slice() : [];
3927
+ watch(list.length > 0);
3928
+ if (last) paint(last.data, last.theme);
3929
+ },
3930
+ draw(data, theme) {
3931
+ last = { data, theme };
3932
+ paint(data, theme);
3933
+ },
3934
+ get report() {
3935
+ return report;
3936
+ },
3937
+ destroy() {
3938
+ watch(false);
3939
+ list = [];
3940
+ last = null;
3941
+ container.querySelector(`:scope > .${ANNOTATION_LAYER_CLASS}`)?.remove();
3942
+ }
3943
+ };
3944
+ }
3945
+
3814
3946
  // src/host.ts
3815
3947
  function sessionViewStateProvider(container) {
3816
3948
  const holder = container;
@@ -3971,6 +4103,16 @@ function createChartHost(container, config) {
3971
4103
  let warnedGeo = false;
3972
4104
  let warnedBlank = false;
3973
4105
  let renderSeq = 0;
4106
+ const annotationLayer = createAnnotationLayer(container, (a3) => config.onAnnotationClick?.(a3));
4107
+ const drawAnnotations = () => annotationLayer.draw(
4108
+ { columns: data.columns, rows: data.rows },
4109
+ {
4110
+ fg: resolved.themeFg,
4111
+ accent: resolved.themeAccent,
4112
+ bg: resolved.backgroundColor ?? resolved.themeBg
4113
+ }
4114
+ );
4115
+ annotationLayer.set(config.annotations ?? []);
3974
4116
  let renderedPromise = Promise.resolve();
3975
4117
  const settle = (p4) => {
3976
4118
  renderedPromise = p4;
@@ -4192,6 +4334,10 @@ function createChartHost(container, config) {
4192
4334
  } catch {
4193
4335
  }
4194
4336
  }
4337
+ try {
4338
+ drawAnnotations();
4339
+ } catch {
4340
+ }
4195
4341
  };
4196
4342
  const renderFailure = (err) => {
4197
4343
  const message = String(err?.message ?? err ?? "");
@@ -4290,6 +4436,16 @@ function createChartHost(container, config) {
4290
4436
  get options() {
4291
4437
  return resolved;
4292
4438
  },
4439
+ setAnnotations(list) {
4440
+ annotationLayer.set(list);
4441
+ if (!destroyed) try {
4442
+ drawAnnotations();
4443
+ } catch {
4444
+ }
4445
+ },
4446
+ get annotationReport() {
4447
+ return annotationLayer.report;
4448
+ },
4293
4449
  // A GETTER, not a captured value: `rendered` names the LATEST render, so a host that
4294
4450
  // read it once would otherwise hold the promise of a draw long since replaced.
4295
4451
  get rendered() {
@@ -4328,6 +4484,7 @@ function createChartHost(container, config) {
4328
4484
  container.removeEventListener(XFILTER_REFRESH_EVENT, onXf);
4329
4485
  container.removeEventListener("click", onClick);
4330
4486
  subs.clear();
4487
+ annotationLayer.destroy();
4331
4488
  unpinScrolledAxis(container);
4332
4489
  container.innerHTML = "";
4333
4490
  }
@@ -4629,6 +4786,8 @@ export {
4629
4786
  applyLabelContrast,
4630
4787
  isInvalidSentinelError,
4631
4788
  invalidSentinelReason,
4789
+ ANNOTATION_LAYER_CLASS,
4790
+ ANNOTATION_CLASS,
4632
4791
  sessionViewStateProvider,
4633
4792
  noopViewStateProvider,
4634
4793
  assembleD3,
package/dist/index.mjs CHANGED
@@ -10,6 +10,8 @@ import {
10
10
  ANIM_PLAY_SPEED_DEFAULT,
11
11
  ANIM_PLAY_SPEED_MAX,
12
12
  ANIM_PLAY_SPEED_MIN,
13
+ ANNOTATION_CLASS,
14
+ ANNOTATION_LAYER_CLASS,
13
15
  APPROXIMATE_POSITIONS_DEFAULT,
14
16
  AUTO_RAMP_CLASSES,
15
17
  AUTO_RAMP_DRAWN_OPACITY,
@@ -163,7 +165,7 @@ import {
163
165
  unpinScrolledAxis,
164
166
  valuePlacementFlag,
165
167
  withoutViewOnlyKeys
166
- } from "./chunk-P4FOCNAX.mjs";
168
+ } from "./chunk-TOKJTZNS.mjs";
167
169
  import "./chunk-A2GMXZP7.mjs";
168
170
  import "./chunk-GHODWOAL.mjs";
169
171
 
@@ -939,9 +941,10 @@ function buildProductFeedbackPayload(input) {
939
941
  };
940
942
  }
941
943
  var DEFAULT_TEXT2 = {
942
- label: "Your suggestions",
943
- placeholder: "An idea, a missing chart, or something that got in your way",
944
- hint: "Sent to our team. It is never used to draw a chart.",
944
+ label: "Ideas to improve this product",
945
+ placeholder: "A feature you'd like, or something that got in your way",
946
+ hint: "This sends a note to our team. It won't draw a chart.",
947
+ open: "Suggest an improvement",
945
948
  send: "Send",
946
949
  sending: "Sending...",
947
950
  sent: "Thank you - your suggestion was sent.",
@@ -980,7 +983,26 @@ function mountProductFeedbackBox(container, opts) {
980
983
  status.setAttribute("role", "status");
981
984
  status.setAttribute("aria-live", "polite");
982
985
  row.append(button, status);
983
- wrap.append(label, input, hint, row);
986
+ const opener = doc.createElement("button");
987
+ opener.type = "button";
988
+ opener.className = "bic-product-feedback-open";
989
+ opener.textContent = text.open;
990
+ const panel = doc.createElement("div");
991
+ panel.className = "bic-product-feedback-panel";
992
+ panel.append(label, hint, input, row);
993
+ const setOpen = (open) => {
994
+ opener.hidden = open;
995
+ panel.hidden = !open;
996
+ wrap.setAttribute("data-open", String(open));
997
+ };
998
+ setOpen(opts.collapsed === false);
999
+ const onOpen = () => {
1000
+ if (!panel.hidden) return;
1001
+ setOpen(true);
1002
+ input.focus();
1003
+ safely(() => opts.onOpened?.());
1004
+ };
1005
+ wrap.append(opener, panel);
984
1006
  container.appendChild(wrap);
985
1007
  let sending = false;
986
1008
  const sync = () => {
@@ -1026,6 +1048,7 @@ function mountProductFeedbackBox(container, opts) {
1026
1048
  };
1027
1049
  input.addEventListener("input", onInput);
1028
1050
  button.addEventListener("click", onClick);
1051
+ opener.addEventListener("click", onOpen);
1029
1052
  sync();
1030
1053
  safely(() => opts.onShown?.());
1031
1054
  return {
@@ -1033,6 +1056,7 @@ function mountProductFeedbackBox(container, opts) {
1033
1056
  destroy() {
1034
1057
  input.removeEventListener("input", onInput);
1035
1058
  button.removeEventListener("click", onClick);
1059
+ opener.removeEventListener("click", onOpen);
1036
1060
  wrap.remove();
1037
1061
  }
1038
1062
  };
@@ -2789,6 +2813,8 @@ export {
2789
2813
  ANIM_PLAY_SPEED_DEFAULT,
2790
2814
  ANIM_PLAY_SPEED_MAX,
2791
2815
  ANIM_PLAY_SPEED_MIN,
2816
+ ANNOTATION_CLASS,
2817
+ ANNOTATION_LAYER_CLASS,
2792
2818
  APPROXIMATE_POSITIONS_DEFAULT,
2793
2819
  AUTO_RAMP_CLASSES,
2794
2820
  AUTO_RAMP_DRAWN_OPACITY,
package/dist/react.mjs CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  loadGeo,
8
8
  syncMemberSelection,
9
9
  toSourceRows
10
- } from "./chunk-P4FOCNAX.mjs";
10
+ } from "./chunk-TOKJTZNS.mjs";
11
11
  import "./chunk-A2GMXZP7.mjs";
12
12
  import "./chunk-GHODWOAL.mjs";
13
13
 
@@ -53,6 +53,8 @@ function BicChart(props) {
53
53
  filteredBy,
54
54
  respondsWith,
55
55
  onSelect,
56
+ annotations,
57
+ onAnnotationClick,
56
58
  className,
57
59
  style
58
60
  } = props;
@@ -78,6 +80,11 @@ function BicChart(props) {
78
80
  onLabelContrastRef.current = onLabelContrast;
79
81
  const onInvalidSentinelRef = useRef(onInvalidSentinel);
80
82
  onInvalidSentinelRef.current = onInvalidSentinel;
83
+ const onAnnotationClickRef = useRef(onAnnotationClick);
84
+ onAnnotationClickRef.current = onAnnotationClick;
85
+ const annKey = useMemo(() => JSON.stringify(annotations ?? []), [annotations]);
86
+ const annotationsRef = useRef(annotations);
87
+ annotationsRef.current = annotations;
81
88
  const [geoTick, setGeoTick] = useState(0);
82
89
  useEffect(() => {
83
90
  if (!geoKind || geoFromCache(geoKind)) return;
@@ -112,9 +119,12 @@ function BicChart(props) {
112
119
  // newest prop wins without a rebuild.
113
120
  get onInvalidSentinel() {
114
121
  return onInvalidSentinelRef.current ? (info) => onInvalidSentinelRef.current?.(info) : void 0;
115
- }
122
+ },
123
+ annotations: annotationsRef.current ?? [],
124
+ onAnnotationClick: (a) => onAnnotationClickRef.current?.(a)
116
125
  });
117
126
  hostRef.current = host;
127
+ builtAnnKeyRef.current = annKey;
118
128
  const off = host.selection.onChange((payloadIdxs, source) => {
119
129
  if (source === "host") return;
120
130
  const sourceIdxs = toSourceRows(rowMapRef.current, payloadIdxs);
@@ -135,6 +145,12 @@ function BicChart(props) {
135
145
  builtWithRef.current = { ...builtWithRef.current, optKey };
136
146
  hostRef.current.setOptions(options);
137
147
  }, [optKey]);
148
+ const builtAnnKeyRef = useRef(null);
149
+ useEffect(() => {
150
+ if (!hostRef.current || builtAnnKeyRef.current === annKey) return;
151
+ builtAnnKeyRef.current = annKey;
152
+ hostRef.current.setAnnotations(annotationsRef.current ?? []);
153
+ }, [annKey]);
138
154
  useEffect(() => {
139
155
  if (!hostRef.current || !data) return;
140
156
  if (builtWithRef.current?.data === data) return;
@@ -0,0 +1,40 @@
1
+ export interface MarkAnnotation {
2
+ /** The column holding the key (with `value`). */
3
+ column?: string;
4
+ /** The key's value; matched as text, trimmed, so a number column and a string key agree. */
5
+ value?: string | number;
6
+ /**
7
+ * A key over several columns - a route by both of its ends, `{ Origin: "BRA", Dest: "CHL" }` - so no key
8
+ * column has to be added to the query or the chart. Every named column must match. Used instead of column/value.
9
+ */
10
+ where?: Record<string, string | number>;
11
+ /** Badge text (a count, a letter). Default: a dot. */
12
+ label?: string;
13
+ /** Tooltip text - the note, or a summary of several. */
14
+ title?: string;
15
+ /** Anything the host wants back on click. */
16
+ id?: string;
17
+ }
18
+ export interface AnnotationReport {
19
+ /** Annotations drawn on a mark. */
20
+ shown: number;
21
+ /** Annotations with no mark in this render (filtered out, off the map, no such key). */
22
+ notShown: number;
23
+ }
24
+ export declare const ANNOTATION_LAYER_CLASS = "bic-annotations";
25
+ export declare const ANNOTATION_CLASS = "bic-annotation";
26
+ export interface AnnotationLayer {
27
+ set(list: MarkAnnotation[]): void;
28
+ /** Redraw against the container's current marks and the given data. */
29
+ draw(data: {
30
+ columns: any[];
31
+ rows: any[][];
32
+ }, theme: {
33
+ fg?: string;
34
+ accent?: string;
35
+ bg?: string;
36
+ }): void;
37
+ readonly report: AnnotationReport;
38
+ destroy(): void;
39
+ }
40
+ export declare function createAnnotationLayer(container: HTMLElement, onClick?: (a: MarkAnnotation) => void): AnnotationLayer;
@@ -6,6 +6,7 @@ import { type ColourSpreadCensus } from "./colourSpread";
6
6
  import { type ValuePlacementCensus } from "./valuePlacement";
7
7
  import { type FitRenderedChartOptions, type FitRenderedChartResult } from "./fitDom";
8
8
  import { type LabelContrastOptions, type LabelContrastReport } from "./labelContrastDom";
9
+ import { type MarkAnnotation, type AnnotationReport } from "./annotations";
9
10
  export type RenderFn = (container: HTMLElement, data: any, options: RenderOptions) => void;
10
11
  /**
11
12
  * THE SESSION PROVIDER — what every host got hard-coded before contract 1.6.0, now named.
@@ -181,8 +182,21 @@ export interface ChartHostConfig {
181
182
  reason: string;
182
183
  message: string;
183
184
  }) => void;
185
+ /**
186
+ * NOTES ON MARKS. Each annotation names a key - a value in a column of this chart's data, such as a
187
+ * country code - and the host draws a badge on that key's mark after every render, wherever a filter,
188
+ * a re-query or a zoom has put it. Keyed by value, never by row position. Change them with
189
+ * setAnnotations; none (the default) draws nothing.
190
+ */
191
+ annotations?: MarkAnnotation[];
192
+ /** A badge was clicked. The mark beneath is not selected. */
193
+ onAnnotationClick?: (annotation: MarkAnnotation) => void;
184
194
  }
185
195
  export interface ChartHost {
196
+ /** Replace the notes drawn on marks (see ChartHostConfig.annotations); redraws them at once. */
197
+ setAnnotations(list: MarkAnnotation[]): void;
198
+ /** How many annotations the last draw placed on a mark, and how many had none in this render. */
199
+ readonly annotationReport: AnnotationReport;
186
200
  render(): void;
187
201
  /**
188
202
  * THE LAST RENDER, SETTLED. render() stays synchronous — it returns nothing, exactly as it
@@ -8,6 +8,7 @@ export declare function registerCityTable(packed: string): void;
8
8
  export { buildRenderPayload, type RenderPayload, type GeoPointBinding } from "./payload";
9
9
  export { codeReadsDatesInLocalTime, vegaSpecReadsDatesInLocalTime } from "./dateCells";
10
10
  export { createChartHost, compileRenderFn, stripEsmExports, requiredD3Plugins, explainRenderFailure, sessionViewStateProvider, noopViewStateProvider, assembleD3, D3_PLUGIN_PACKAGES, type ChartHost, type ChartHostConfig, type RenderFn } from "./host";
11
+ export { ANNOTATION_CLASS, ANNOTATION_LAYER_CLASS, type MarkAnnotation, type AnnotationReport } from "./annotations";
11
12
  export { createChartGroup, toSourceRows, syncMemberSelection, type ChartGroup, type ChartGroupMember, type ChartGroupSelection, type ChartGroupChange, type ChartGroupMemberOptions, type ChartGroupSourceOptions, type ChartGroupPayload } from "./group";
12
13
  export { createMarkResolver, isInsideControl, rowIdxsFromMark, nextSelection, collectD3GlyphCenters, clearSelectionGlyphs, paintSelectionGlyphs, SELECTION_GLYPH_CLASS, type MarkResolver, type MarkResolverEnv, type GlyphCenter, type SelectionGlyphStyle, type SelectionModifiers, } from "./selection";
13
14
  export { planTrivialChart, compileTrivialSource, type TrivialPlan, type TrivialShapeKind } from "./trivial";
@@ -60,6 +60,8 @@ export interface ProductFeedbackText {
60
60
  sending?: string;
61
61
  sent?: string;
62
62
  failed?: string;
63
+ /** The link the closed box shows. */
64
+ open?: string;
63
65
  }
64
66
  export interface ProductFeedbackBoxOptions {
65
67
  /** Delivers the cleaned text. Resolve { ok: false } (or throw) on failure; the box keeps the text either way. */
@@ -68,8 +70,13 @@ export interface ProductFeedbackBoxOptions {
68
70
  /** Added to the wrapper so the host can skin it; the structure stays the same. */
69
71
  className?: string;
70
72
  maxChars?: number;
73
+ /** Start as a link that opens the box (the default). A closed affordance cannot be mistaken for a chart prompt;
74
+ * pass false only where the box is the page's only purpose. Once opened it stays open, so a draft is never hidden. */
75
+ collapsed?: boolean;
71
76
  /** Called once, when the box is mounted - the host's "shown" counter. */
72
77
  onShown?: () => void;
78
+ /** Called once, when the reader opens a closed box. */
79
+ onOpened?: () => void;
73
80
  /** Called after a successful send - the host's "sent" counter. */
74
81
  onSent?: () => void;
75
82
  /** Called after a failed send, with what failed, so the host can log it. */
@@ -82,7 +89,9 @@ export interface ProductFeedbackBox {
82
89
  }
83
90
  /**
84
91
  * Build the box inside `container`. The wrapper carries `bic-product-feedback` (plus the host's
85
- * class); the textarea, button and status line carry `bic-product-feedback-input`, `-send` and
86
- * `-status`, so hosts and tests can find them without depending on the order of elements.
92
+ * class) and `data-open`; the opening link is `bic-product-feedback-open`, and everything it
93
+ * reveals sits in `bic-product-feedback-panel`; the textarea, button and status line carry
94
+ * `bic-product-feedback-input`, `-send` and `-status`, so hosts and tests can find them without
95
+ * depending on the order of elements.
87
96
  */
88
97
  export declare function mountProductFeedbackBox(container: HTMLElement, opts: ProductFeedbackBoxOptions): ProductFeedbackBox;
@@ -61,6 +61,14 @@ export interface BicChartProps {
61
61
  respondsWith?: "filter" | "highlight";
62
62
  /** Selection callback in SOURCE row indices (group) or payload indices (standalone). */
63
63
  onSelect?: (rowIdxs: number[]) => void;
64
+ /**
65
+ * Notes on marks: a badge on each key's mark, redrawn after every render - e.g.
66
+ * `[{ column: "CountryCode", value: "USA", label: "2", title: "..." }]`. Keyed by a value in a column,
67
+ * never a row position, so a filter or a re-query never moves one.
68
+ */
69
+ annotations?: ChartHostConfig["annotations"];
70
+ /** A badge was clicked (the mark beneath is not selected). */
71
+ onAnnotationClick?: ChartHostConfig["onAnnotationClick"];
64
72
  className?: string;
65
73
  style?: React.CSSProperties;
66
74
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bicharts/chart-host",
3
- "version": "0.6.51",
3
+ "version": "0.6.53",
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",