@eshal-bot/chat-widget 0.2.24 → 0.2.27

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.
@@ -87838,11 +87838,81 @@
87838
87838
  return null;
87839
87839
  }
87840
87840
 
87841
+ // ── Figures side by side ───────────────────────────────────────────────────
87842
+ // A chart that compares figures (pre vs post GRP, the 1+/3+/5+ reach curves)
87843
+ // arrives with `props.series`, [{ key, dataKey, name }] built by A2uiBlock's
87844
+ // chartSeries() from the spec's `ys` and `names`: one bar or one line per
87845
+ // figure, named in a legend, its colour fixed by its place in the order. The
87846
+ // colours: the brand blue (docs/design-system), then amber, pink and cyan from
87847
+ // the frontend chart palette's families, an order checked for colour-blind
87848
+ // separation and 3:1 contrast on the widget's light and dark surfaces (on dark,
87849
+ // blue-500 stands in for the brand blue-700, which falls under 3:1 there).
87850
+ // Mirrors SERIES_PALETTE in the frontend's chartShared.tsx.
87851
+ const SERIES_COLORS = ['#1d4ed8', '#d97706', '#db2777', '#0891b2'];
87852
+ const SERIES_COLORS_DARK = ['#3b82f6', '#d97706', '#db2777', '#0891b2'];
87853
+ function seriesColor(i, isDark = false) {
87854
+ const colors = isDark ? SERIES_COLORS_DARK : SERIES_COLORS;
87855
+ return colors[i % colors.length];
87856
+ }
87857
+
87858
+ // The figures drawn side by side, or [] for a one-figure chart.
87859
+ function sideBySide(props) {
87860
+ return Array.isArray(props.series) && props.series.length > 1 ? props.series : [];
87861
+ }
87862
+
87863
+ // Each figure's money formatting (moneyFormatters by its own key, so spend
87864
+ // reads as money beside GRP), and the one axis's: money only when every
87865
+ // figure on it is.
87866
+ function seriesMoney(series, currency) {
87867
+ const each = series.map(s => moneyFormatters(currency, s.key));
87868
+ return {
87869
+ each,
87870
+ axis: each.length > 0 && each.every(Boolean) ? each[0] : null
87871
+ };
87872
+ }
87873
+
87874
+ // The tooltip value of a figure side by side, formatted as that figure. Its
87875
+ // line names the figure, so it is written in ink (seriesTooltipItem): amber
87876
+ // text on white falls under 4.5:1.
87877
+ function seriesTooltip(series, each, unit) {
87878
+ return (v, name, item) => {
87879
+ const money = each[series.findIndex(s => s.dataKey === (item && item.dataKey))];
87880
+ if (money) return money.full(v);
87881
+ return unit ? `${Number(v).toLocaleString()} ${unit}` : Number(v).toLocaleString();
87882
+ };
87883
+ }
87884
+ function seriesTooltipItem(series, inkColor) {
87885
+ return series.length > 0 ? {
87886
+ color: inkColor
87887
+ } : undefined;
87888
+ }
87889
+
87890
+ // The legend of figures side by side: above the plot, clear of slanted
87891
+ // category labels; its words in ink, the swatch carrying the colour.
87892
+ function seriesLegend(inkColor) {
87893
+ return /*#__PURE__*/jsxRuntimeExports.jsx(Legend, {
87894
+ verticalAlign: "top",
87895
+ iconSize: 10,
87896
+ wrapperStyle: {
87897
+ fontSize: 11,
87898
+ paddingBottom: 6
87899
+ },
87900
+ formatter: value => /*#__PURE__*/jsxRuntimeExports.jsx("span", {
87901
+ style: {
87902
+ color: inkColor
87903
+ },
87904
+ children: value
87905
+ })
87906
+ });
87907
+ }
87908
+
87841
87909
  /**
87842
87910
  * Recharts bar chart - mirrors agent-sdk's eshalRenderers.BarChart.
87843
87911
  *
87844
87912
  * Expects `props.data` to be an array of `{ label, value }` objects
87845
- * (already normalized by A2uiBlock's numericData helper).
87913
+ * (already normalized by A2uiBlock's numericData helper), or, for figures
87914
+ * side by side, `{ label, s0, s1, ... }` points with `props.series`: grouped
87915
+ * bars, one per figure, named in a legend.
87846
87916
  */
87847
87917
  function BarChartRenderer({
87848
87918
  props,
@@ -87855,6 +87925,9 @@
87855
87925
  // Human series name for the tooltip ("Average Salary"), so it no longer
87856
87926
  // reads the generic dataKey "value".
87857
87927
  const seriesName = props.valueLabel || undefined;
87928
+ const series = sideBySide(props);
87929
+ const figures = seriesMoney(series, props.currency);
87930
+ const axisMoney = series.length > 0 ? figures.axis : money;
87858
87931
  const borderColor = isDark ? '#374151' : '#e2e8f0';
87859
87932
  const bgColor = isDark ? '#1f2937' : '#ffffff';
87860
87933
  const labelColor = isDark ? '#9ca3af' : '#64748b';
@@ -87938,11 +88011,12 @@
87938
88011
  },
87939
88012
  angle: xAngle,
87940
88013
  textAnchor: xAnchor,
87941
- interval: 0,
87942
- tickFormatter: v => {
87943
- const s = String(v ?? '');
87944
- return s.replace(/[_\-]+/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
87945
- },
88014
+ interval: 0
88015
+ // Each label arrives formatted by A2uiBlock (formatAxisTick), so the
88016
+ // axis shows it as given, as the line and pie charts do. Capitalising
88017
+ // it again would split "CNBC-TV18" and turn "McDonald's" into
88018
+ // "McDonald'S".
88019
+ ,
87946
88020
  label: props.xAxisLabel ? {
87947
88021
  value: props.xAxisLabel,
87948
88022
  position: 'insideBottom',
@@ -87963,9 +88037,10 @@
87963
88037
  fontSize: 11,
87964
88038
  fill: labelColor
87965
88039
  } : undefined,
87966
- tickFormatter: v => money ? money.compact(v) : props.unit ? `${formatCompactNumber(v)} ${props.unit}` : formatCompactNumber(v)
88040
+ tickFormatter: v => axisMoney ? axisMoney.compact(v) : props.unit ? `${formatCompactNumber(v)} ${props.unit}` : formatCompactNumber(v)
87967
88041
  }), /*#__PURE__*/jsxRuntimeExports.jsx(Tooltip, {
87968
- formatter: v => money ? money.full(v) : props.unit ? `${Number(v).toLocaleString()} ${props.unit}` : Number(v).toLocaleString(),
88042
+ formatter: series.length > 0 ? seriesTooltip(series, figures.each, props.unit) : v => money ? money.full(v) : props.unit ? `${Number(v).toLocaleString()} ${props.unit}` : Number(v).toLocaleString(),
88043
+ itemStyle: seriesTooltipItem(series, titleColor),
87969
88044
  contentStyle: {
87970
88045
  background: tooltipBg,
87971
88046
  border: `1px solid ${borderColor}`,
@@ -87973,7 +88048,12 @@
87973
88048
  fontSize: 12,
87974
88049
  color: titleColor
87975
88050
  }
87976
- }), /*#__PURE__*/jsxRuntimeExports.jsx(Bar, {
88051
+ }), series.length > 0 && seriesLegend(labelColor), series.length > 0 ? series.map((s, i) => /*#__PURE__*/jsxRuntimeExports.jsx(Bar, {
88052
+ dataKey: s.dataKey,
88053
+ name: s.name,
88054
+ fill: seriesColor(i, isDark),
88055
+ radius: [2, 2, 0, 0]
88056
+ }, s.dataKey)) : /*#__PURE__*/jsxRuntimeExports.jsx(Bar, {
87977
88057
  dataKey: "value",
87978
88058
  name: seriesName,
87979
88059
  fill: "#3b82f6",
@@ -88242,7 +88322,9 @@
88242
88322
 
88243
88323
  /**
88244
88324
  * Recharts line / area chart - replaces hand-rolled LineSvg.
88245
- * `props.data` must be an array of `{ label, value }` objects.
88325
+ * `props.data` must be an array of `{ label, value }` objects, or, for
88326
+ * figures side by side, `{ label, s0, s1, ... }` points with `props.series`:
88327
+ * one line each, named in a legend.
88246
88328
  * Set `area={true}` to fill under the line.
88247
88329
  */
88248
88330
  function LineChartRenderer({
@@ -88253,6 +88335,12 @@
88253
88335
  const data = Array.isArray(props.data) ? props.data : [];
88254
88336
  const money = moneyFormatters(props.currency, props.yKey);
88255
88337
  const seriesName = props.valueLabel || undefined;
88338
+ const series = sideBySide(props);
88339
+ const figures = seriesMoney(series, props.currency);
88340
+ const axisMoney = series.length > 0 ? figures.axis : money;
88341
+ // Several lines over a long run of periods (26 weeks of reach): every few
88342
+ // ticks and no dots, so the lines stay legible at the widget's width.
88343
+ const crowded = series.length > 0 && data.length > 12;
88256
88344
  const borderColor = isDark ? '#374151' : '#e2e8f0';
88257
88345
  const bgColor = isDark ? '#1f2937' : '#ffffff';
88258
88346
  const labelColor = isDark ? '#9ca3af' : '#64748b';
@@ -88332,7 +88420,7 @@
88332
88420
  },
88333
88421
  angle: 0,
88334
88422
  textAnchor: "middle",
88335
- interval: 0,
88423
+ interval: crowded ? 'preserveStartEnd' : 0,
88336
88424
  label: props.xAxisLabel ? {
88337
88425
  value: props.xAxisLabel,
88338
88426
  position: 'insideBottom',
@@ -88346,9 +88434,10 @@
88346
88434
  fontSize: 11,
88347
88435
  fill: labelColor
88348
88436
  },
88349
- tickFormatter: v => money ? money.compact(v) : props.unit ? `${formatCompactNumber(v)} ${props.unit}` : formatCompactNumber(v)
88437
+ tickFormatter: v => axisMoney ? axisMoney.compact(v) : props.unit ? `${formatCompactNumber(v)} ${props.unit}` : formatCompactNumber(v)
88350
88438
  }), /*#__PURE__*/jsxRuntimeExports.jsx(Tooltip, {
88351
- formatter: v => money ? money.full(v) : props.unit ? `${Number(v).toLocaleString()} ${props.unit}` : Number(v).toLocaleString(),
88439
+ formatter: series.length > 0 ? seriesTooltip(series, figures.each, props.unit) : v => money ? money.full(v) : props.unit ? `${Number(v).toLocaleString()} ${props.unit}` : Number(v).toLocaleString(),
88440
+ itemStyle: seriesTooltipItem(series, titleColor),
88352
88441
  contentStyle: {
88353
88442
  background: tooltipBg,
88354
88443
  border: `1px solid ${borderColor}`,
@@ -88356,7 +88445,33 @@
88356
88445
  fontSize: 12,
88357
88446
  color: titleColor
88358
88447
  }
88359
- }), area ? /*#__PURE__*/jsxRuntimeExports.jsx(Area, {
88448
+ }), series.length > 0 && seriesLegend(labelColor), series.length > 0 ? series.map((s, i) => {
88449
+ const color = seriesColor(i, isDark);
88450
+ const dot = crowded ? false : {
88451
+ r: 3,
88452
+ fill: color
88453
+ };
88454
+ return area ? /*#__PURE__*/jsxRuntimeExports.jsx(Area, {
88455
+ type: "monotone",
88456
+ dataKey: s.dataKey,
88457
+ name: s.name,
88458
+ stroke: color,
88459
+ fill: color,
88460
+ fillOpacity: 0.15,
88461
+ strokeWidth: 2,
88462
+ dot: dot
88463
+ }, s.dataKey) : /*#__PURE__*/jsxRuntimeExports.jsx(Line, {
88464
+ type: "monotone",
88465
+ dataKey: s.dataKey,
88466
+ name: s.name,
88467
+ stroke: color,
88468
+ strokeWidth: 2,
88469
+ dot: dot,
88470
+ activeDot: {
88471
+ r: 5
88472
+ }
88473
+ }, s.dataKey);
88474
+ }) : area ? /*#__PURE__*/jsxRuntimeExports.jsx(Area, {
88360
88475
  type: "monotone",
88361
88476
  dataKey: "value",
88362
88477
  name: seriesName,
@@ -88738,6 +88853,40 @@
88738
88853
  }).filter(d => Number.isFinite(d.value));
88739
88854
  }
88740
88855
 
88856
+ // The figures a chart compares side by side: the spec's `ys` (the runtime's
88857
+ // agents/data_charts.py writes them for pre vs post GRP, the 1+/3+/5+ reach
88858
+ // curves), each named by `names` or else its prettified key. Two or more that
88859
+ // the rows carry, else none, and a one-figure spec renders as before. Each
88860
+ // figure is read under its own data key (s0, s1, ...), so a figure whose name
88861
+ // matches the category's key ("label") cannot overwrite it.
88862
+ function chartSeries(spec) {
88863
+ const ys = Array.isArray(spec.ys) ? spec.ys.filter(k => typeof k === 'string' && k) : [];
88864
+ const sample = Array.isArray(spec.data) ? spec.data.find(row => row && typeof row === 'object') : null;
88865
+ const keys = sample ? [...new Set(ys)].filter(k => Object.prototype.hasOwnProperty.call(sample, k)) : [];
88866
+ if (keys.length < 2) return [];
88867
+ const names = spec.names && typeof spec.names === 'object' ? spec.names : {};
88868
+ return keys.map((key, i) => ({
88869
+ key,
88870
+ dataKey: `s${i}`,
88871
+ name: typeof names[key] === 'string' && names[key].trim() ? names[key].trim() : prettifyLabel(key)
88872
+ }));
88873
+ }
88874
+
88875
+ // numericData() for figures side by side: the label and each figure under its
88876
+ // series' data key, null where a row has none; a row with none of them goes.
88877
+ function seriesData(data, xKey, series) {
88878
+ return (data || []).filter(row => row && typeof row === 'object').map(row => {
88879
+ const point = {
88880
+ label: formatAxisTick(row[xKey], xKey)
88881
+ };
88882
+ for (const s of series) {
88883
+ const value = row[s.key] == null ? NaN : Number(row[s.key]);
88884
+ point[s.dataKey] = Number.isFinite(value) ? value : null;
88885
+ }
88886
+ return point;
88887
+ }).filter(point => series.some(s => point[s.dataKey] !== null));
88888
+ }
88889
+
88741
88890
  // ── Chart (SVG: line / bar / pie / donut + everything-else fallback) ─────
88742
88891
 
88743
88892
  function Chart({
@@ -88745,12 +88894,16 @@
88745
88894
  type,
88746
88895
  isDark
88747
88896
  }) {
88897
+ const series = chartSeries(spec);
88748
88898
  const {
88749
88899
  x,
88750
88900
  y
88751
- } = inferKeys(spec.data, spec.x, spec.y);
88752
- const rows = numericData(spec.data, x, y);
88901
+ } = inferKeys(spec.data, spec.x, series.length > 0 ? series[0].key : spec.y);
88753
88902
  const t = String(type || '').trim().toLowerCase();
88903
+ // Figures side by side draw as grouped bars or one line each; a pie holds
88904
+ // one figure, so a spec of several typed pie draws its first.
88905
+ const sideBySide = series.length > 0 && t !== 'pie' && t !== 'donut';
88906
+ const rows = sideBySide ? seriesData(spec.data, x, series) : numericData(spec.data, x, y);
88754
88907
  const chartProps = {
88755
88908
  title: spec.title,
88756
88909
  data: rows,
@@ -88765,7 +88918,9 @@
88765
88918
  valueLabel: prettifyLabel(y),
88766
88919
  // The currency the chart states ("USD", "INR", "₹"): the only source of
88767
88920
  // a currency symbol (widgetCatalog moneyFormatters).
88768
- currency: spec.currency
88921
+ currency: spec.currency,
88922
+ // Figures side by side (chartSeries): one bar or line each, in a legend.
88923
+ series: sideBySide ? series : undefined
88769
88924
  };
88770
88925
  if (t === 'pie') return /*#__PURE__*/jsxRuntimeExports.jsx(PieChartRenderer, {
88771
88926
  props: chartProps,
@@ -90383,11 +90538,28 @@
90383
90538
  const MONTH_NAMES = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
90384
90539
  const CURRENCY_KEY_RE = /(^|[_\W])(price|salary|cost|amount|revenue|expense|total|value|budget|spend|income|sale|balance)/i;
90385
90540
  const MONTH_KEY_RE = /(^|[_\W])month/i;
90541
+
90542
+ // Only a column-style value is reworded: one with an underscore
90543
+ // (office_supplies), one without capitals (kebab-case, all-lower words)
90544
+ // or a camelCase key. A value written with capitals is a name and stays as
90545
+ // written ("DD Sports", "Republic TV", "GEC"), as does a word written with
90546
+ // capitals inside a key (GRP_pre -> "GRP Pre"). Lower-casing every word but
90547
+ // its first letter read those channels as "Dd Sports", "Republic Tv" and
90548
+ // "Gec" (Media Prowess, Oct 2026).
90386
90549
  function prettifyLabel(value) {
90387
90550
  if (value === null || value === undefined) return '';
90388
- const s = String(value);
90551
+ const s = String(value).replace(/\s+/g, ' ').trim();
90389
90552
  if (!s) return '';
90390
- return s.replace(/[_\-]+/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2').replace(/\s+/g, ' ').trim().split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ');
90553
+ if (!isColumnStyle(s)) return s;
90554
+ return s.replace(/[_\-]+/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2').replace(/\s+/g, ' ').trim().split(' ').map(w => w === w.toLowerCase() ? w.charAt(0).toUpperCase() + w.slice(1) : w).join(' ');
90555
+ }
90556
+
90557
+ // A camelCase key ("camelCaseKey", "totalSpend"): two lower-case letters
90558
+ // before the first capital, so names such as "iPhone", "eBay" and "kWh"
90559
+ // stay as written.
90560
+ const CAMEL_CASE_KEY_RE = /^[a-z]{2}[a-z0-9]*[A-Z][A-Za-z0-9]*$/;
90561
+ function isColumnStyle(s) {
90562
+ return s.includes('_') || s === s.toLowerCase() || CAMEL_CASE_KEY_RE.test(s);
90391
90563
  }
90392
90564
  function isCurrencyKey(key) {
90393
90565
  return typeof key === 'string' && CURRENCY_KEY_RE.test(key);