@perspective-dev/viewer-datagrid 4.4.0 → 4.5.0

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.
Files changed (87) hide show
  1. package/dist/cdn/perspective-viewer-datagrid.js +4 -22
  2. package/dist/cdn/perspective-viewer-datagrid.js.map +4 -4
  3. package/dist/css/perspective-viewer-datagrid-toolbar.css +1 -1
  4. package/dist/css/perspective-viewer-datagrid.css +1 -1
  5. package/dist/esm/color_utils.d.ts +22 -0
  6. package/dist/esm/custom_elements/datagrid.d.ts +16 -21
  7. package/dist/esm/data_listener/format_cell.d.ts +1 -1
  8. package/dist/esm/data_listener/formatter_cache.d.ts +1 -1
  9. package/dist/esm/data_listener/index.d.ts +3 -2
  10. package/dist/esm/event_handlers/click/edit_click.d.ts +3 -2
  11. package/dist/esm/event_handlers/click.d.ts +4 -6
  12. package/dist/esm/event_handlers/dispatch_click.d.ts +3 -2
  13. package/dist/esm/event_handlers/expand_collapse.d.ts +1 -1
  14. package/dist/esm/event_handlers/focus.d.ts +4 -5
  15. package/dist/esm/event_handlers/header_click.d.ts +5 -3
  16. package/dist/esm/event_handlers/keydown/edit_keydown.d.ts +3 -4
  17. package/dist/esm/event_handlers/select_region.d.ts +3 -1
  18. package/dist/esm/event_handlers/sort.d.ts +8 -7
  19. package/dist/esm/model/create.d.ts +1 -1
  20. package/dist/esm/model/meta_columns.d.ts +1 -0
  21. package/dist/esm/perspective-viewer-datagrid.js +3 -3
  22. package/dist/esm/perspective-viewer-datagrid.js.map +4 -4
  23. package/dist/esm/plugin/activate.d.ts +1 -1
  24. package/dist/esm/plugin/column_config_schema.d.ts +31 -0
  25. package/dist/esm/style_handlers/body.d.ts +3 -3
  26. package/dist/esm/style_handlers/column_header.d.ts +4 -3
  27. package/dist/esm/style_handlers/consolidated.d.ts +3 -47
  28. package/dist/esm/style_handlers/editable.d.ts +3 -2
  29. package/dist/esm/style_handlers/focus.d.ts +4 -4
  30. package/dist/esm/style_handlers/group_header.d.ts +1 -1
  31. package/dist/esm/style_handlers/table_cell/boolean.d.ts +1 -1
  32. package/dist/esm/style_handlers/table_cell/cell_flash.d.ts +1 -1
  33. package/dist/esm/style_handlers/table_cell/datetime.d.ts +1 -1
  34. package/dist/esm/style_handlers/table_cell/numeric.d.ts +1 -1
  35. package/dist/esm/style_handlers/table_cell/row_header.d.ts +1 -1
  36. package/dist/esm/style_handlers/table_cell/string.d.ts +1 -1
  37. package/dist/esm/style_handlers/types.d.ts +0 -4
  38. package/dist/esm/types.d.ts +10 -17
  39. package/package.json +2 -4
  40. package/src/css/regular_table.css +87 -31
  41. package/src/css/row-hover.css +20 -7
  42. package/src/css/toolbar.css +11 -0
  43. package/src/ts/color_utils.ts +181 -16
  44. package/src/ts/custom_elements/datagrid.ts +70 -56
  45. package/src/ts/custom_elements/toolbar.ts +4 -5
  46. package/src/ts/data_listener/format_cell.ts +28 -9
  47. package/src/ts/data_listener/format_tree_header.ts +2 -2
  48. package/src/ts/data_listener/formatter_cache.ts +9 -96
  49. package/src/ts/data_listener/index.ts +13 -11
  50. package/src/ts/event_handlers/click/edit_click.ts +10 -6
  51. package/src/ts/event_handlers/click.ts +39 -68
  52. package/src/ts/event_handlers/dispatch_click.ts +27 -25
  53. package/src/ts/event_handlers/expand_collapse.ts +11 -8
  54. package/src/ts/event_handlers/focus.ts +38 -35
  55. package/src/ts/event_handlers/header_click.ts +107 -62
  56. package/src/ts/event_handlers/keydown/edit_keydown.ts +60 -54
  57. package/src/ts/event_handlers/select_region.ts +153 -131
  58. package/src/ts/event_handlers/sort.ts +20 -25
  59. package/src/ts/get_cell_config.ts +10 -3
  60. package/src/ts/model/column_overrides.ts +16 -9
  61. package/src/ts/model/create.ts +68 -55
  62. package/src/ts/{event_handlers/deselect_all.ts → model/meta_columns.ts} +33 -14
  63. package/src/ts/model/toolbar.ts +33 -8
  64. package/src/ts/plugin/activate.ts +122 -92
  65. package/src/ts/plugin/column_config_schema.ts +187 -0
  66. package/src/ts/plugin/draw.ts +1 -0
  67. package/src/ts/plugin/restore.ts +6 -2
  68. package/src/ts/plugin/save.ts +2 -5
  69. package/src/ts/style_handlers/body.ts +48 -51
  70. package/src/ts/style_handlers/column_header.ts +22 -21
  71. package/src/ts/style_handlers/consolidated.ts +23 -123
  72. package/src/ts/style_handlers/editable.ts +16 -10
  73. package/src/ts/style_handlers/focus.ts +7 -5
  74. package/src/ts/style_handlers/group_header.ts +13 -6
  75. package/src/ts/style_handlers/table_cell/boolean.ts +3 -3
  76. package/src/ts/style_handlers/table_cell/cell_flash.ts +11 -11
  77. package/src/ts/style_handlers/table_cell/datetime.ts +3 -3
  78. package/src/ts/style_handlers/table_cell/numeric.ts +24 -25
  79. package/src/ts/style_handlers/table_cell/row_header.ts +2 -2
  80. package/src/ts/style_handlers/table_cell/string.ts +20 -18
  81. package/src/ts/style_handlers/types.ts +0 -10
  82. package/src/ts/types.ts +28 -20
  83. package/dist/esm/event_handlers/deselect_all.d.ts +0 -5
  84. package/dist/esm/event_handlers/row_select_click.d.ts +0 -4
  85. package/dist/esm/plugin/column_style_controls.d.ts +0 -28
  86. package/src/ts/event_handlers/row_select_click.ts +0 -92
  87. package/src/ts/plugin/column_style_controls.ts +0 -76
@@ -10,15 +10,182 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- import chroma from "chroma-js";
14
13
  import type { ColorRecord } from "./types.js";
15
14
 
15
+ /** 8-bit sRGB color as `[r, g, b]` with each channel in `[0, 255]`. */
16
+ export type RGB = [number, number, number];
17
+
18
+ /** HSL color as `[h, s, l]` with `h` in degrees `[0, 360)` and `s`, `l` in `[0, 1]`. */
19
+ export type HSL = [number, number, number];
20
+
21
+ const parse_cache = new Map<string, RGB>();
22
+ let parse_ctx: CanvasRenderingContext2D | null = null;
23
+
24
+ /** Parse a CSS hex color (`#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`). Returns `null` if `input` is not a hex literal. Alpha is ignored. */
25
+ function parse_hex(input: string): RGB | null {
26
+ const s = input.startsWith("#") ? input.slice(1) : input;
27
+ if (s.length === 3 || s.length === 4) {
28
+ const r = parseInt(s[0] + s[0], 16);
29
+ const g = parseInt(s[1] + s[1], 16);
30
+ const b = parseInt(s[2] + s[2], 16);
31
+ if (!isNaN(r) && !isNaN(g) && !isNaN(b)) {
32
+ return [r, g, b];
33
+ }
34
+ } else if (s.length === 6 || s.length === 8) {
35
+ const r = parseInt(s.slice(0, 2), 16);
36
+ const g = parseInt(s.slice(2, 4), 16);
37
+ const b = parseInt(s.slice(4, 6), 16);
38
+ if (!isNaN(r) && !isNaN(g) && !isNaN(b)) {
39
+ return [r, g, b];
40
+ }
41
+ }
42
+
43
+ return null;
44
+ }
45
+
46
+ /** Parse a CSS `rgb()` or `rgba()` functional color. Returns `null` if `input` does not match. Alpha is ignored. */
47
+ function parse_rgb_fn(input: string): RGB | null {
48
+ const m = input.match(
49
+ /^rgba?\(\s*([\d.]+)\s*[, ]\s*([\d.]+)\s*[, ]\s*([\d.]+)/i,
50
+ );
51
+ if (!m) {
52
+ return null;
53
+ }
54
+
55
+ return [
56
+ Math.round(parseFloat(m[1])),
57
+ Math.round(parseFloat(m[2])),
58
+ Math.round(parseFloat(m[3])),
59
+ ];
60
+ }
61
+
62
+ /** Fallback parser that defers to the browser by assigning `input` to a 2D canvas `fillStyle` and re-reading the normalized value. Handles named colors, `hsl()`, etc. Returns `[0, 0, 0]` if the value is invalid or no canvas context is available. */
63
+ function parse_via_canvas(input: string): RGB {
64
+ if (!parse_ctx) {
65
+ const canvas = document.createElement("canvas");
66
+ canvas.width = canvas.height = 1;
67
+ parse_ctx = canvas.getContext("2d");
68
+ }
69
+
70
+ if (!parse_ctx) {
71
+ return [0, 0, 0];
72
+ }
73
+
74
+ parse_ctx.fillStyle = "#000";
75
+ parse_ctx.fillStyle = input;
76
+ const normalized = parse_ctx.fillStyle as string;
77
+ return parse_hex(normalized) ?? parse_rgb_fn(normalized) ?? [0, 0, 0];
78
+ }
79
+
80
+ /** Parse any CSS color string into an `RGB` triple. Tries hex and `rgb()` fast paths, then falls back to a canvas-based parser for named colors, `hsl()`, etc. Results are memoized per input. */
81
+ export function parseColor(input: string): RGB {
82
+ const key = input.trim();
83
+ const cached = parse_cache.get(key);
84
+ if (cached) {
85
+ return cached;
86
+ }
87
+
88
+ const rgb = parse_hex(key) ?? parse_rgb_fn(key) ?? parse_via_canvas(key);
89
+ parse_cache.set(key, rgb);
90
+ return rgb;
91
+ }
92
+
93
+ /** Format a single channel as a clamped, zero-padded two-digit hex byte. */
94
+ function toHex(c: number): string {
95
+ const v = Math.max(0, Math.min(255, Math.round(c)));
96
+ return v.toString(16).padStart(2, "0");
97
+ }
98
+
99
+ /** Format an `RGB` triple as a `#rrggbb` hex string. Channels are clamped to `[0, 255]`. */
100
+ export function rgbToHex([r, g, b]: RGB): string {
101
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
102
+ }
103
+
104
+ /** Convert sRGB to HSL. Output `h` is in degrees `[0, 360)`; `s` and `l` are in `[0, 1]`. */
105
+ export function rgbToHsl([r, g, b]: RGB): HSL {
106
+ const rn = r / 255,
107
+ gn = g / 255,
108
+ bn = b / 255;
109
+ const max = Math.max(rn, gn, bn);
110
+ const min = Math.min(rn, gn, bn);
111
+ const l = (max + min) / 2;
112
+ const d = max - min;
113
+ let h = 0;
114
+ let s = 0;
115
+ if (d !== 0) {
116
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
117
+ if (max === rn) {
118
+ h = ((gn - bn) / d + (gn < bn ? 6 : 0)) * 60;
119
+ } else if (max === gn) {
120
+ h = ((bn - rn) / d + 2) * 60;
121
+ } else {
122
+ h = ((rn - gn) / d + 4) * 60;
123
+ }
124
+ }
125
+
126
+ return [h, s, l];
127
+ }
128
+
129
+ /** Convert HSL to sRGB. `h` is wrapped into `[0, 360)`; `s` and `l` are expected in `[0, 1]`. Output channels are rounded to integers in `[0, 255]`. */
130
+ export function hslToRgb([h, s, l]: HSL): RGB {
131
+ const hn = (((h % 360) + 360) % 360) / 360;
132
+ if (s === 0) {
133
+ const v = Math.round(l * 255);
134
+ return [v, v, v];
135
+ }
136
+
137
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
138
+ const p = 2 * l - q;
139
+ const f = (t: number): number => {
140
+ if (t < 0) {
141
+ t += 1;
142
+ }
143
+
144
+ if (t > 1) {
145
+ t -= 1;
146
+ }
147
+
148
+ if (t < 1 / 6) {
149
+ return p + (q - p) * 6 * t;
150
+ }
151
+
152
+ if (t < 1 / 2) {
153
+ return q;
154
+ }
155
+
156
+ if (t < 2 / 3) {
157
+ return p + (q - p) * (2 / 3 - t) * 6;
158
+ }
159
+
160
+ return p;
161
+ };
162
+
163
+ return [
164
+ Math.round(f(hn + 1 / 3) * 255),
165
+ Math.round(f(hn) * 255),
166
+ Math.round(f(hn - 1 / 3) * 255),
167
+ ];
168
+ }
169
+
170
+ /**
171
+ * Blend two `RGB` colors using LRGB (gamma-naive linear) interpolation,
172
+ * matching chroma-js's default `mix` mode. `f` is the weight of `b` in `[0, 1]`
173
+ * (0 returns `a`, 1 returns `b`).
174
+ */
175
+ export function mixRgb(a: RGB, b: RGB, f = 0.5): RGB {
176
+ return [
177
+ Math.round(Math.sqrt(a[0] * a[0] * (1 - f) + b[0] * b[0] * f)),
178
+ Math.round(Math.sqrt(a[1] * a[1] * (1 - f) + b[1] * b[1] * f)),
179
+ Math.round(Math.sqrt(a[2] * a[2] * (1 - f) + b[2] * b[2] * f)),
180
+ ];
181
+ }
182
+
183
+ /** 50/50 LRGB blend of CSS color `a` with `RGB`-ish triple `b`, returned as `#rrggbb`. */
16
184
  export function blend(a: string, b: number[]): string {
17
- return chroma.mix(a, `rgb(${b[0]},${b[1]},${b[2]})`, 0.5).hex();
185
+ return rgbToHex(mixRgb(parseColor(a), [b[0], b[1], b[2]], 0.5));
18
186
  }
19
187
 
20
- // AFAICT `chroma-js` has no alpha-aware blending? So we need a function to get
21
- // the color of a heatmap cell over the background.
188
+ /** Composite a premultiplied-style `RGBA` cell color over `source` (default white) and return the resulting opaque `RGB`. Used to flatten heatmap cells against the background. */
22
189
  export function rgbaToRgb(
23
190
  [r, g, b, a]: [number, number, number, number],
24
191
  source: [number, number, number] = [255, 255, 255],
@@ -30,7 +197,7 @@ export function rgbaToRgb(
30
197
  return [f(0, r), f(1, g), f(2, b)];
31
198
  }
32
199
 
33
- // Chroma does this but why bother?
200
+ /** Pick a readable foreground (`#161616` or `#ffffff`) for the given background using a perceptual luminance threshold. */
34
201
  export function infer_foreground_from_background([r, g, b]: [
35
202
  number,
36
203
  number,
@@ -42,21 +209,19 @@ export function infer_foreground_from_background([r, g, b]: [
42
209
  : "#ffffff";
43
210
  }
44
211
 
45
- function make_gradient(chromahex: chroma.Color): string {
46
- const [r, g, b] = chromahex.rgb();
47
- const [r1, g1, b1] = chromahex
48
- .set("hsl.h", (chromahex.get("hsl.h") - 15) % 360)
49
- .rgb();
50
- const [r2, g2, b2] = chromahex
51
- .set("hsl.h", (chromahex.get("hsl.h") + 15) % 360)
52
- .rgb();
212
+ /** Build a CSS `linear-gradient` that fans `rgb` ±15° in hue, used as the negative-value swatch in column color pickers. */
213
+ function make_gradient(rgb: RGB): string {
214
+ const [h, s, l] = rgbToHsl(rgb);
215
+ const [r, g, b] = rgb;
216
+ const [r1, g1, b1] = hslToRgb([h - 15, s, l]);
217
+ const [r2, g2, b2] = hslToRgb([h + 15, s, l]);
53
218
  return `linear-gradient(to right top,rgb(${r1},${g1},${b1}),rgb(${r},${g},${b}) 50%,rgb(${r2},${g2},${b2}))`;
54
219
  }
55
220
 
221
+ /** Precompute the tuple of derived color strings (RGB channels, gradient, opaque/transparent rgba) cached on the model for a configured plugin color. */
56
222
  export function make_color_record(color: string): ColorRecord {
57
- const chroma_neg = chroma(color);
58
- const _neg_grad = make_gradient(chroma_neg);
59
- const rgb = chroma_neg.rgb();
223
+ const rgb = parseColor(color);
224
+ const _neg_grad = make_gradient(rgb);
60
225
  return [
61
226
  color,
62
227
  rgb[0],
@@ -15,19 +15,22 @@ import { activate } from "../plugin/activate.js";
15
15
  import { restore } from "../plugin/restore.js";
16
16
  import { save } from "../plugin/save.js";
17
17
  import { draw } from "../plugin/draw.js";
18
- import column_style_controls, {
19
- ColumnStyleOpts,
20
- } from "../plugin/column_style_controls.js";
18
+ import column_config_schema, {
19
+ ColumnConfigSchema,
20
+ } from "../plugin/column_config_schema.js";
21
21
  import datagridStyles from "../../../dist/css/perspective-viewer-datagrid.css";
22
22
  import { format_raw } from "../data_listener/format_cell.js";
23
+ import { sourceColumn } from "@perspective-dev/viewer/src/ts/column-format.js";
23
24
 
24
25
  import type { View, ViewWindow } from "@perspective-dev/client";
25
- import type { IPerspectiveViewerPlugin } from "@perspective-dev/viewer";
26
+ import type {
27
+ IPerspectiveViewerPlugin,
28
+ PluginStaticConfig,
29
+ } from "@perspective-dev/viewer";
26
30
  import type {
27
31
  DatagridModel,
28
32
  DatagridToolbarElement,
29
33
  EditMode,
30
- PerspectiveViewerElement,
31
34
  DatagridPluginConfig,
32
35
  ColumnsConfig,
33
36
  } from "../types.js";
@@ -113,44 +116,64 @@ export class HTMLPerspectiveViewerDatagridPluginElement
113
116
  return await activate.call(this, view);
114
117
  }
115
118
 
116
- get name(): string {
117
- return "Datagrid";
118
- }
119
-
120
- get category(): string {
121
- return "Basic";
122
- }
123
-
124
- get select_mode(): string {
125
- return "toggle";
126
- }
127
-
128
- get min_config_columns(): number | undefined {
129
- return undefined;
130
- }
131
-
132
- get config_column_names(): string[] {
133
- return ["Columns"];
134
- }
135
-
136
- get group_rollups(): string[] {
137
- return ["rollup", "flat", "total"];
138
- }
139
-
140
- /**
141
- * Give the Datagrid a higher priority so it is loaded
142
- * over the default charts by default.
143
- */
144
- get priority(): number {
145
- return 1;
119
+ get_static_config(): PluginStaticConfig {
120
+ return {
121
+ name: "Datagrid",
122
+ category: "Basic",
123
+ select_mode: "toggle",
124
+ config_column_names: ["Columns"],
125
+ group_rollup_modes: ["rollup", "flat", "total"],
126
+ // Higher priority than the chart plugins so the Datagrid is
127
+ // loaded by default.
128
+ priority: 1,
129
+ can_render_column_styles: true,
130
+ };
146
131
  }
147
132
 
148
- can_render_column_styles(type: string, _group: string): boolean {
149
- return type !== "boolean";
133
+ plugin_config_schema(): ColumnConfigSchema {
134
+ const fields = [];
135
+ fields.push({
136
+ kind: "Enum",
137
+ key: "edit_mode",
138
+ default: "READ_ONLY",
139
+ variants: [
140
+ { value: "EDIT", label: "Edit" },
141
+ { value: "READ_ONLY", label: "Read-only" },
142
+ { value: "SELECT_ROW", label: "Row Select" },
143
+ { value: "SELECT_COLUMN", label: "Column Select" },
144
+ { value: "SELECT_REGION", label: "Region Select" },
145
+ { value: "SELECT_ROW_TREE", label: "Tree Select" },
146
+ ],
147
+ });
148
+
149
+ fields.push({
150
+ kind: "Bool",
151
+ key: "scroll_lock",
152
+ default: false,
153
+ });
154
+
155
+ return {
156
+ fields,
157
+ };
150
158
  }
151
159
 
152
- column_style_controls(type: string, group: string): ColumnStyleOpts {
153
- return column_style_controls.call(this, type as any, group);
160
+ column_config_schema(
161
+ type: string,
162
+ group: string | undefined,
163
+ column_name: string,
164
+ current_value: Record<string, unknown> | null,
165
+ viewer_config?: { group_by?: string[]; group_rollup_mode?: string },
166
+ column_stats?: { abs_max: number },
167
+ ): ColumnConfigSchema {
168
+ return column_config_schema.call(
169
+ this,
170
+ type as any,
171
+ group,
172
+ column_name,
173
+ current_value,
174
+ viewer_config,
175
+ column_stats,
176
+ );
154
177
  }
155
178
 
156
179
  async draw(view: View): Promise<void> {
@@ -170,9 +193,7 @@ export class HTMLPerspectiveViewerDatagridPluginElement
170
193
  }
171
194
  }
172
195
 
173
- async render(viewport?: ViewWindow): Promise<string> {
174
- const viewer = this.parentElement as PerspectiveViewerElement;
175
- const view = await viewer.getView();
196
+ async render(view: View, viewport?: ViewWindow): Promise<string> {
176
197
  const json = await view.to_columns(viewport as any);
177
198
  const cols = await view.column_paths(viewport as any);
178
199
 
@@ -192,7 +213,7 @@ export class HTMLPerspectiveViewerDatagridPluginElement
192
213
  const pluginConfig = (this.regular_table as any)[
193
214
  PRIVATE_PLUGIN_SYMBOL
194
215
  ] as ColumnsConfig | undefined;
195
- const columnName = col_name.split("|").at(-1)!;
216
+ const columnName = sourceColumn(col_name);
196
217
  const formatter = format_raw(
197
218
  type,
198
219
  pluginConfig?.[columnName] || {},
@@ -204,13 +225,14 @@ export class HTMLPerspectiveViewerDatagridPluginElement
204
225
  out += col[ridx] + "\t";
205
226
  }
206
227
  }
228
+
207
229
  out += "\n";
208
230
  }
209
231
 
210
232
  return out.trim();
211
233
  }
212
234
 
213
- async resize(): Promise<void> {
235
+ async resize(_view: View): Promise<void> {
214
236
  if (!this.isConnected || this.offsetParent == null) {
215
237
  return;
216
238
  }
@@ -225,25 +247,17 @@ export class HTMLPerspectiveViewerDatagridPluginElement
225
247
  this.regular_table.clear();
226
248
  }
227
249
 
228
- async save(): Promise<any> {
250
+ save(): any {
229
251
  return save.call(this);
230
252
  }
231
253
 
232
- async restore(
233
- token: DatagridPluginConfig,
234
- columns_config?: ColumnsConfig,
235
- ): Promise<any> {
254
+ restore(token: DatagridPluginConfig, columns_config?: ColumnsConfig): void {
236
255
  return restore.call(this, token, columns_config ?? {});
237
256
  }
238
257
 
239
- async restyle(): Promise<void> {
240
- // Get view from model if available, otherwise no-op
241
- if (this.model?._view) {
242
- await this.draw(this.model._view);
243
- }
244
- }
258
+ restyle() {}
245
259
 
246
- async delete(): Promise<void> {
260
+ delete(): void {
247
261
  this.disconnectedCallback();
248
262
  this._toolbar = undefined;
249
263
  if ((this.regular_table as any).table_model) {
@@ -10,12 +10,10 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
+ import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
13
14
  import TOOLBAR_STYLE from "../../../dist/css/perspective-viewer-datagrid-toolbar.css";
14
15
  import { toggle_edit_mode, toggle_scroll_lock } from "../model/toolbar.js";
15
- import type {
16
- DatagridPluginElement,
17
- PerspectiveViewerElement,
18
- } from "../types.js";
16
+ import type { DatagridPluginElement } from "../types.js";
19
17
 
20
18
  const stylesheet = new CSSStyleSheet();
21
19
  stylesheet.replaceSync(TOOLBAR_STYLE);
@@ -53,7 +51,7 @@ export class HTMLPerspectiveViewerDatagridToolbarElement extends HTMLElement {
53
51
  </div>
54
52
  `;
55
53
 
56
- const viewer = this.parentElement as PerspectiveViewerElement;
54
+ const viewer = this.parentElement as HTMLPerspectiveViewerElement;
57
55
  const plugin = this.previousElementSibling as DatagridPluginElement;
58
56
 
59
57
  plugin._scroll_lock = this.shadowRoot!.querySelector(
@@ -66,6 +64,7 @@ export class HTMLPerspectiveViewerDatagridToolbarElement extends HTMLElement {
66
64
  plugin._edit_button = this.shadowRoot!.querySelector(
67
65
  "#edit_mode",
68
66
  ) as HTMLElement;
67
+
69
68
  plugin._edit_button.addEventListener("click", () => {
70
69
  toggle_edit_mode.call(plugin);
71
70
  plugin.regular_table.draw();
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { FormatterCache, Formatter } from "./formatter_cache.js";
14
14
  import type { DatagridModel, ColumnsConfig, ColumnConfig } from "../types.js";
15
- import { ColumnType } from "@perspective-dev/client";
15
+ import type { ColumnType } from "@perspective-dev/client";
16
16
 
17
17
  const FORMAT_CACHE = new FormatterCache();
18
18
  const MAX_BAR_WIDTH_PCT = 1;
@@ -44,7 +44,11 @@ export function format_cell(
44
44
  const plugin: ColumnConfig = plugins[title] || {};
45
45
  const is_numeric = type === "integer" || type === "float";
46
46
 
47
- if (is_numeric && plugin?.number_fg_mode === "bar") {
47
+ if (
48
+ is_numeric &&
49
+ (plugin?.number_fg_mode === "bar" ||
50
+ plugin?.number_fg_mode === "label-bar")
51
+ ) {
48
52
  const a = Math.max(
49
53
  0,
50
54
  Math.min(
@@ -54,15 +58,30 @@ export function format_cell(
54
58
  ),
55
59
  );
56
60
 
57
- const div = this._div_factory.get();
58
- const anchor = (val as number) >= 0 ? "left" : "right";
61
+ const anchor = (val as number) >= 0 ? "" : "justify-self:flex-end;";
59
62
  const pct = (a * 100).toFixed(2);
60
- div.setAttribute(
61
- "style",
62
- `width:calc(${pct}% - 4px);position:absolute;${anchor}:2px;height:80%;top:10%;pointer-events:none;`,
63
- );
64
63
 
65
- return div;
64
+ if (plugin.number_fg_mode === "bar") {
65
+ const div = this._div_factory.get();
66
+ div.className = "psp-bar";
67
+ div.setAttribute(
68
+ "style",
69
+ `${anchor}width:${pct}%;height:80%;top:10%;pointer-events:none;background:var(--psp-label-bar-color)`,
70
+ );
71
+
72
+ return div;
73
+ } else {
74
+ const formatter = FORMAT_CACHE.get(type, plugin);
75
+ const label = formatter ? formatter.format(val) : (val as string);
76
+
77
+ const div = this._div_factory.get();
78
+ div.className = "psp-bar";
79
+ div.setAttribute(
80
+ "style",
81
+ `--label:"${label}";${anchor}width:${pct}%;height:80%;top:10%;pointer-events:none;background:var(--psp-label-bar-color)`,
82
+ );
83
+ return div;
84
+ }
66
85
  } else if (plugin?.format === "link" && type === "string") {
67
86
  const anchor = document.createElement("a");
68
87
  anchor.setAttribute("href", val as string);
@@ -27,7 +27,7 @@ export function* format_tree_header_row_path(
27
27
  ): Generator<RowHeaderCell[]> {
28
28
  const plugins: ColumnsConfig =
29
29
  (regularTable as any)[PRIVATE_PLUGIN_SYMBOL] || {};
30
- for (let path of paths) {
30
+ for (const path of paths) {
31
31
  const fullPath: unknown[] = ["TOTAL", ...path];
32
32
  const last = fullPath[fullPath.length - 1];
33
33
  let newPath: RowHeaderCell[] = fullPath
@@ -61,7 +61,7 @@ export function* format_flat_header_row_path(
61
61
  const plugins: ColumnsConfig =
62
62
  (regularTable as any)[PRIVATE_PLUGIN_SYMBOL] || {};
63
63
 
64
- for (let path of paths) {
64
+ for (const path of paths) {
65
65
  yield path.map((part, i) =>
66
66
  format_cell.call(this, row_headers[i], part, plugins, true),
67
67
  ) as RowHeaderCell[];
@@ -10,7 +10,12 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- import { ColumnType } from "@perspective-dev/client";
13
+ import type { ColumnType } from "@perspective-dev/client";
14
+ import {
15
+ createDateFormatter,
16
+ createDatetimeFormatter,
17
+ createNumberFormatter,
18
+ } from "@perspective-dev/viewer/src/ts/column-format.js";
14
19
  import type { ColumnConfig } from "../types.js";
15
20
 
16
21
  export interface Formatter {
@@ -26,30 +31,6 @@ class BooleanFormatter implements Formatter {
26
31
  // PluginConfig is a subset of ColumnConfig with the formatting properties
27
32
  type PluginConfig = Pick<ColumnConfig, "date_format" | "number_format">;
28
33
 
29
- const LEGACY_CONFIG: Record<
30
- string,
31
- { format: Intl.NumberFormatOptions | Intl.DateTimeFormatOptions }
32
- > = {
33
- float: {
34
- format: {
35
- style: "decimal",
36
- minimumFractionDigits: 2,
37
- maximumFractionDigits: 2,
38
- },
39
- },
40
- datetime: {
41
- format: {
42
- dateStyle: "short",
43
- timeStyle: "medium",
44
- } as Intl.DateTimeFormatOptions,
45
- },
46
- date: {
47
- format: {
48
- dateStyle: "short",
49
- } as Intl.DateTimeFormatOptions,
50
- },
51
- };
52
-
53
34
  export class FormatterCache {
54
35
  private _formatters: Map<string, Formatter | false>;
55
36
 
@@ -61,89 +42,21 @@ export class FormatterCache {
61
42
  _type: ColumnType,
62
43
  plugin: PluginConfig,
63
44
  ): Intl.DateTimeFormat {
64
- if (plugin.date_format?.format !== "custom") {
65
- const options: Intl.DateTimeFormatOptions = {
66
- timeZone: plugin.date_format?.timeZone,
67
- dateStyle:
68
- plugin.date_format?.dateStyle === "disabled"
69
- ? undefined
70
- : (plugin.date_format?.dateStyle ?? "short"),
71
- timeStyle:
72
- plugin.date_format?.timeStyle === "disabled"
73
- ? undefined
74
- : (plugin.date_format?.timeStyle ?? "medium"),
75
- };
76
-
77
- return new Intl.DateTimeFormat(
78
- navigator.languages as string[],
79
- options,
80
- );
81
- } else {
82
- const options: Intl.DateTimeFormatOptions = {
83
- timeZone: plugin.date_format?.timeZone,
84
- hour12: plugin.date_format?.hour12 ?? true,
85
- fractionalSecondDigits:
86
- plugin.date_format?.fractionalSecondDigits,
87
- };
88
-
89
- if (plugin.date_format?.year !== "disabled") {
90
- options.year = plugin.date_format?.year ?? "2-digit";
91
- }
92
- if (plugin.date_format?.month !== "disabled") {
93
- options.month = plugin.date_format?.month ?? "numeric";
94
- }
95
- if (plugin.date_format?.day !== "disabled") {
96
- options.day = plugin.date_format?.day ?? "numeric";
97
- }
98
- if (
99
- plugin.date_format?.weekday &&
100
- plugin.date_format?.weekday !== "disabled"
101
- ) {
102
- options.weekday = plugin.date_format.weekday;
103
- }
104
- if (plugin.date_format?.hour !== "disabled") {
105
- options.hour = plugin.date_format?.hour ?? "numeric";
106
- }
107
- if (plugin.date_format?.minute !== "disabled") {
108
- options.minute = plugin.date_format?.minute ?? "numeric";
109
- }
110
- if (plugin.date_format?.second !== "disabled") {
111
- options.second = plugin.date_format?.second ?? "numeric";
112
- }
113
-
114
- return new Intl.DateTimeFormat(
115
- navigator.languages as string[],
116
- options,
117
- );
118
- }
45
+ return createDatetimeFormatter(plugin.date_format);
119
46
  }
120
47
 
121
48
  private create_date_formatter(
122
49
  _type: ColumnType,
123
50
  plugin: PluginConfig,
124
51
  ): Intl.DateTimeFormat {
125
- const options: Intl.DateTimeFormatOptions = {
126
- timeZone: "utc",
127
- dateStyle:
128
- plugin.date_format?.dateStyle === "disabled"
129
- ? undefined
130
- : (plugin.date_format?.dateStyle ?? "short"),
131
- };
132
-
133
- return new Intl.DateTimeFormat(
134
- navigator.languages as string[],
135
- options,
136
- );
52
+ return createDateFormatter(plugin.date_format);
137
53
  }
138
54
 
139
55
  private create_number_formatter(
140
56
  type: ColumnType,
141
57
  plugin: PluginConfig,
142
58
  ): Intl.NumberFormat {
143
- const format =
144
- plugin.number_format ??
145
- (LEGACY_CONFIG[type]?.format as Intl.NumberFormatOptions);
146
- return new Intl.NumberFormat(navigator.languages as string[], format);
59
+ return createNumberFormatter(type, plugin.number_format);
147
60
  }
148
61
 
149
62
  private create_boolean_formatter(