@pythia-software/query-table-ui 0.9.5 → 0.9.6

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/README.md CHANGED
@@ -379,6 +379,54 @@ status indicators an accessible label on their wrapper.
379
379
 
380
380
  ## Mobile behavior
381
381
 
382
+ ### Responsive target sizing
383
+
384
+ Set the public, unitless `--qt-touch-target-scale` CSS custom property to
385
+ configure the **added** touch-target size in the bundled theme:
386
+
387
+ | Value | Target sizing |
388
+ | --- | --- |
389
+ | `1` (default, including when unset) | Existing responsive enlargement |
390
+ | `0.5` | Half the added size |
391
+ | `0` | Desktop target sizing with the responsive layout |
392
+
393
+ ```css
394
+ /* Scope to a host wrapper containing the builder, table and metrics. */
395
+ .pharos-query-table {
396
+ --qt-touch-target-scale: 0.5;
397
+ }
398
+ ```
399
+
400
+ For each enlarged dimension, the theme uses
401
+ `desktop + (responsive - desktop) * scale`. A desktop 24px button that normally
402
+ grows to 44px therefore becomes 34px at `0.5`. Each control uses its own desktop
403
+ baseline, including its line box, padding and borders; collapse controls, chips,
404
+ picker rows and editor buttons have different baselines. Narrow and coarse
405
+ pointer rules use the final responsive size once, so matching both does not
406
+ compound the enlargement. Use a number from `0` to `1`, without a unit.
407
+
408
+ The setting covers query actions/chips, table cells/actions, picker and menu
409
+ rows, selection actions, saved-query controls, JSON controls, and select,
410
+ property and metric editor controls. Content remains a lower bound: wrapped
411
+ labels, icons, native controls and the retained mobile input fonts can require
412
+ more room than the scaled minimum. Existing responsive size reductions stay
413
+ compact. Layout widths, wrapping, sheets, scrolling, focus behavior, drag/touch
414
+ actions, breakpoints and input font sizes are independent of the factor.
415
+
416
+ Use `:root` for an application-wide setting, or a wrapper for independent
417
+ instances. Built-in portaled menus, pickers, dialogs and sheets inherit the
418
+ setting from their trigger/overlay anchor through the same theme propagation
419
+ as the palette, including live ancestor `class`, `style` and `data-theme`
420
+ changes. Keep standalone overlays beneath that wrapper in the React tree (and
421
+ their anchor within it). Host-owned portals and controls with custom styling
422
+ must propagate/apply their own sizing. `QueryBuilder`'s `compact` prop controls
423
+ clause presentation separately.
424
+
425
+ Run `npm run test:touch-targets` with `npm run demo` running to check relative
426
+ sizing, scoped portal updates and interactions in Chromium and WebKit.
427
+
428
+ ### Responsive layout and interactions
429
+
382
430
  The bundled theme keeps the table as a horizontally scrollable grid on narrow
383
431
  screens so columns remain comparable. Narrow screens (up to 760px) and short,
384
432
  touch-based landscape viewports use a mobile interaction model:
package/dist/index.js CHANGED
@@ -8472,6 +8472,7 @@ import {
8472
8472
  } from "react";
8473
8473
  import {
8474
8474
  MAX_AGGREGATIONS,
8475
+ MAX_METRIC_WINDOW_PERIODS,
8475
8476
  METRIC_FUNCTIONS as METRIC_FUNCTIONS2,
8476
8477
  aggOpsForField,
8477
8478
  compileFormula,
@@ -9812,6 +9813,7 @@ function MetricCard({
9812
9813
  buckets.length > clause.display.maxGroups ? " plus Other" : "",
9813
9814
  "."
9814
9815
  ] }),
9816
+ (clause.window || clause.share) && /* @__PURE__ */ jsx31("p", { className: "qt-metric-note", children: seriesNote(clause) }),
9815
9817
  !!result?.hiddenGroups && clause.minCount !== void 0 && /* @__PURE__ */ jsxs27("p", { className: "qt-metric-note", children: [
9816
9818
  result.hiddenGroups.toLocaleString(),
9817
9819
  " ",
@@ -9833,6 +9835,23 @@ function MetricCard({
9833
9835
  }
9834
9836
  );
9835
9837
  }
9838
+ function seriesNote(clause) {
9839
+ const unit = clause.dateGrouping?.interval ?? "interval", window2 = clause.window;
9840
+ const parts = [];
9841
+ if (window2) {
9842
+ const span = window2.periods === "all" ? `from the first ${unit}` : `over the trailing ${window2.periods} ${unit}s`;
9843
+ parts.push(
9844
+ window2.reduce === "mean" ? `Mean per ${unit} ${span}` : `Total ${span}`
9845
+ );
9846
+ if (window2.periods !== "all" && window2.partial !== "include")
9847
+ parts.push("shorter windows at the start have no value");
9848
+ }
9849
+ if (clause.share)
9850
+ parts.push(
9851
+ clause.share === "group0" ? `${parts.length ? "as a share" : "Share"} of each ${clause.dateGrouping ? unit : "first group"}'s total` : `${parts.length ? "as a share" : "Share"} of the total`
9852
+ );
9853
+ return `${parts.join("; ")}.`;
9854
+ }
9836
9855
  function BigNumber({
9837
9856
  bucket,
9838
9857
  fmt,
@@ -10508,12 +10527,26 @@ function MetricsEditor({
10508
10527
  const index = name === void 0 ? -1 : groupBy.indexOf(name);
10509
10528
  return index < 0 ? [] : [{ ...s, key: `group${index}` }];
10510
10529
  });
10530
+ const dated = selected.dateGrouping?.field === groupBy[0];
10531
+ const share = groupBy.length >= (selected.share === "group0" ? 2 : 1) ? selected.share : void 0;
10511
10532
  patch(selected.id, {
10512
10533
  groupBy,
10513
10534
  ...sort ? { sort } : {},
10514
- dateGrouping: selected.dateGrouping?.field === groupBy[0] ? selected.dateGrouping : void 0
10535
+ dateGrouping: dated ? selected.dateGrouping : void 0,
10536
+ window: dated ? selected.window : void 0,
10537
+ share,
10538
+ ...shareDisplay(selected.share, share)
10515
10539
  });
10516
10540
  }
10541
+ function shareDisplay(previous, next) {
10542
+ if (next && !previous && !chosen.format)
10543
+ return { display: { ...chosen, format: SHARE_FORMAT } };
10544
+ if (!next && previous && sameFormat(chosen.format, SHARE_FORMAT)) {
10545
+ const { format: _format, ...display2 } = chosen;
10546
+ return { display: display2 };
10547
+ }
10548
+ return {};
10549
+ }
10517
10550
  function changeSize(key, raw) {
10518
10551
  if (!selected || !Number.isFinite(raw)) return;
10519
10552
  const layout = {
@@ -10526,7 +10559,12 @@ function MetricsEditor({
10526
10559
  }
10527
10560
  function changeDisplay(kind) {
10528
10561
  if (!selected) return;
10529
- const next = { display: { ...chosen, kind } };
10562
+ const single = !["box", "histogram", "scatter"].includes(kind);
10563
+ const display2 = !single && shareDisplay(selected.share, void 0).display || chosen;
10564
+ const next = {
10565
+ display: { ...display2, kind },
10566
+ ...single ? {} : { window: void 0, share: void 0 }
10567
+ };
10530
10568
  setFormatTarget(kind === "scatter" || kind === "histogram" ? "x" : "value");
10531
10569
  if (selected.sort)
10532
10570
  next.sort = selected.sort.filter(
@@ -11507,7 +11545,10 @@ function MetricsEditor({
11507
11545
  value: selected.dateGrouping?.interval ?? "exact",
11508
11546
  onChange: (e) => {
11509
11547
  if (e.target.value === "exact") {
11510
- patch(selected.id, { dateGrouping: void 0 });
11548
+ patch(selected.id, {
11549
+ dateGrouping: void 0,
11550
+ window: void 0
11551
+ });
11511
11552
  return;
11512
11553
  }
11513
11554
  const interval = e.target.value;
@@ -11642,6 +11683,25 @@ function MetricsEditor({
11642
11683
  ] }),
11643
11684
  /* @__PURE__ */ jsx33("p", { className: "qt-metric-help", children: "Direct counts and sums use 0 for empty dates. Averages, ratios, and other expressions have no value." })
11644
11685
  ] }),
11686
+ /* @__PURE__ */ jsxs28("label", { className: "qt-metric-date-toggle", children: [
11687
+ /* @__PURE__ */ jsx33(
11688
+ "input",
11689
+ {
11690
+ type: "checkbox",
11691
+ "aria-label": "Whole intervals only",
11692
+ disabled: !selected.dateGrouping,
11693
+ checked: selected.dateGrouping?.partialIntervals === "exclude",
11694
+ onChange: (e) => {
11695
+ const { partialIntervals: _partial, ...rest } = selected.dateGrouping;
11696
+ patch(selected.id, {
11697
+ dateGrouping: e.target.checked ? { ...rest, partialIntervals: "exclude" } : rest
11698
+ });
11699
+ }
11700
+ }
11701
+ ),
11702
+ "Whole intervals only"
11703
+ ] }),
11704
+ /* @__PURE__ */ jsx33("p", { className: "qt-metric-help", children: "Leave out intervals the date filters cut short and the interval still in progress." }),
11645
11705
  /* @__PURE__ */ jsx33("p", { className: "qt-metric-help", children: "Calendar boundaries and custom dates use the grouping timezone. Weeks begin Monday." })
11646
11706
  ] }),
11647
11707
  /* @__PURE__ */ jsxs28(
@@ -11664,6 +11724,18 @@ function MetricsEditor({
11664
11724
  ]
11665
11725
  }
11666
11726
  ),
11727
+ selected.groupBy.length > 0 && !["box", "histogram", "scatter"].includes(chosen.kind) && /* @__PURE__ */ jsx33(
11728
+ MetricSeriesSettings,
11729
+ {
11730
+ clause: selected,
11731
+ fields,
11732
+ settings: settingsState("Window & share"),
11733
+ onChange: (values) => patch(selected.id, {
11734
+ ...values,
11735
+ ..."share" in values ? shareDisplay(selected.share, values.share) : {}
11736
+ })
11737
+ }
11738
+ ),
11667
11739
  /* @__PURE__ */ jsxs28(
11668
11740
  Settings,
11669
11741
  {
@@ -12567,6 +12639,167 @@ function Settings({
12567
12639
  }
12568
12640
  );
12569
12641
  }
12642
+ var SHARE_FORMAT = { kind: "percent", decimals: 1 };
12643
+ var sameFormat = (a, b) => !!a && JSON.stringify(a) === JSON.stringify(b);
12644
+ var windowOf = (periods, mean, partial) => ({
12645
+ periods,
12646
+ ...mean ? { reduce: "mean" } : {},
12647
+ ...partial ? { partial: "include" } : {}
12648
+ });
12649
+ function MetricSeriesSettings({
12650
+ clause,
12651
+ fields,
12652
+ settings,
12653
+ onChange
12654
+ }) {
12655
+ const { window: window2, share, dateGrouping } = clause;
12656
+ const first = fields.find((f) => f.name === clause.groupBy[0])?.label ?? clause.groupBy[0];
12657
+ const unit = dateGrouping?.interval ?? "interval";
12658
+ const mode = !window2 ? "none" : window2.periods === "all" ? "all" : "trailing";
12659
+ const setWindow = (next) => onChange({
12660
+ window: next,
12661
+ ...next && dateGrouping && !dateGrouping.includeEmpty ? { dateGrouping: { ...dateGrouping, includeEmpty: true } } : {},
12662
+ // A share of the total cannot be windowed, and its option is hidden.
12663
+ ...next && share === "total" ? { share: void 0 } : {}
12664
+ });
12665
+ const summary = [
12666
+ !window2 ? "" : window2.periods === "all" ? "Running" : `Trailing ${window2.periods} ${unit}s`,
12667
+ window2?.reduce === "mean" ? `mean per ${unit}` : "",
12668
+ share === "group0" ? `share of each ${dateGrouping ? unit : first}` : share === "total" ? "share of total" : ""
12669
+ ].filter(Boolean).join(" \xB7 ");
12670
+ return /* @__PURE__ */ jsxs28(
12671
+ Settings,
12672
+ {
12673
+ title: "Window & share",
12674
+ ...settings,
12675
+ summary: summary || "Values as computed",
12676
+ children: [
12677
+ dateGrouping && /* @__PURE__ */ jsxs28(Fragment20, { children: [
12678
+ /* @__PURE__ */ jsxs28("label", { children: [
12679
+ "Window",
12680
+ /* @__PURE__ */ jsxs28(
12681
+ "select",
12682
+ {
12683
+ "aria-label": "Window",
12684
+ value: mode,
12685
+ onChange: (e) => setWindow(
12686
+ e.target.value === "none" ? void 0 : windowOf(
12687
+ e.target.value === "all" ? "all" : window2 && window2.periods !== "all" ? window2.periods : 7,
12688
+ window2?.reduce === "mean",
12689
+ // A running value has no partial windows.
12690
+ e.target.value !== "all" && window2?.partial === "include"
12691
+ )
12692
+ ),
12693
+ children: [
12694
+ /* @__PURE__ */ jsx33("option", { value: "none", children: "None" }),
12695
+ /* @__PURE__ */ jsxs28("option", { value: "trailing", children: [
12696
+ "Trailing ",
12697
+ unit,
12698
+ "s"
12699
+ ] }),
12700
+ /* @__PURE__ */ jsxs28("option", { value: "all", children: [
12701
+ "Running from the first ",
12702
+ unit
12703
+ ] })
12704
+ ]
12705
+ }
12706
+ )
12707
+ ] }),
12708
+ window2 && window2.periods !== "all" && /* @__PURE__ */ jsxs28("label", { children: [
12709
+ `${unit[0].toUpperCase()}${unit.slice(1)}s per window`,
12710
+ /* @__PURE__ */ jsx33(
12711
+ "input",
12712
+ {
12713
+ "aria-label": "Window length",
12714
+ type: "number",
12715
+ min: 2,
12716
+ max: MAX_METRIC_WINDOW_PERIODS,
12717
+ value: window2.periods,
12718
+ onChange: (e) => {
12719
+ const n = e.currentTarget.valueAsNumber;
12720
+ if (Number.isInteger(n) && n >= 2 && n <= MAX_METRIC_WINDOW_PERIODS)
12721
+ setWindow({ ...window2, periods: n });
12722
+ }
12723
+ }
12724
+ )
12725
+ ] }),
12726
+ window2 && /* @__PURE__ */ jsxs28(Fragment20, { children: [
12727
+ /* @__PURE__ */ jsxs28("label", { children: [
12728
+ "Combine",
12729
+ /* @__PURE__ */ jsxs28(
12730
+ "select",
12731
+ {
12732
+ "aria-label": "Combine window",
12733
+ value: window2.reduce ?? "total",
12734
+ onChange: (e) => setWindow(
12735
+ windowOf(
12736
+ window2.periods,
12737
+ e.target.value === "mean",
12738
+ window2.partial === "include"
12739
+ )
12740
+ ),
12741
+ children: [
12742
+ /* @__PURE__ */ jsx33("option", { value: "total", children: "Total over the window" }),
12743
+ /* @__PURE__ */ jsxs28("option", { value: "mean", children: [
12744
+ "Mean per ",
12745
+ unit
12746
+ ] })
12747
+ ]
12748
+ }
12749
+ )
12750
+ ] }),
12751
+ window2.periods !== "all" && /* @__PURE__ */ jsxs28("label", { className: "qt-metric-date-toggle", children: [
12752
+ /* @__PURE__ */ jsx33(
12753
+ "input",
12754
+ {
12755
+ type: "checkbox",
12756
+ "aria-label": "Show partial windows",
12757
+ checked: window2.partial === "include",
12758
+ onChange: (e) => setWindow(
12759
+ windowOf(
12760
+ window2.periods,
12761
+ window2.reduce === "mean",
12762
+ e.target.checked
12763
+ )
12764
+ )
12765
+ }
12766
+ ),
12767
+ "Show windows that start before the range"
12768
+ ] }),
12769
+ /* @__PURE__ */ jsxs28("p", { className: "qt-metric-help", children: [
12770
+ "A total evaluates the metric over every row in the window, so ratios and averages pool their parts. A mean per ",
12771
+ unit,
12772
+ " ",
12773
+ "averages a single sum, count or distinct count."
12774
+ ] })
12775
+ ] })
12776
+ ] }),
12777
+ /* @__PURE__ */ jsxs28("label", { children: [
12778
+ "Show values as",
12779
+ /* @__PURE__ */ jsxs28(
12780
+ "select",
12781
+ {
12782
+ "aria-label": "Show values as",
12783
+ value: share ?? "values",
12784
+ onChange: (e) => onChange({
12785
+ share: e.target.value === "values" ? void 0 : e.target.value
12786
+ }),
12787
+ children: [
12788
+ /* @__PURE__ */ jsx33("option", { value: "values", children: "Values" }),
12789
+ clause.groupBy.length > 1 && /* @__PURE__ */ jsxs28("option", { value: "group0", children: [
12790
+ "Share of each ",
12791
+ dateGrouping ? unit : first
12792
+ ] }),
12793
+ !window2 && /* @__PURE__ */ jsx33("option", { value: "total", children: "Share of all groups" })
12794
+ ]
12795
+ }
12796
+ )
12797
+ ] }),
12798
+ share && /* @__PURE__ */ jsx33("p", { className: "qt-metric-help", children: "Divides each group by a total of the same sum or count, including groups past the group limit." })
12799
+ ]
12800
+ }
12801
+ );
12802
+ }
12570
12803
  function MetricFormatControls({
12571
12804
  format,
12572
12805
  onChange,