Skip to content

Commit f517e9c

Browse files
committed
Remove datagrid dependencies
Signed-off-by: Andrew Stein <steinlink@gmail.com>
1 parent 410d3c4 commit f517e9c

31 files changed

Lines changed: 437 additions & 159 deletions

packages/viewer-datagrid/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,12 @@
3535
"dependencies": {
3636
"@perspective-dev/client": "workspace:",
3737
"@perspective-dev/viewer": "workspace:",
38-
"chroma-js": "catalog:",
3938
"regular-table": "catalog:"
4039
},
4140
"devDependencies": {
4241
"lightningcss": "catalog:",
4342
"@perspective-dev/esbuild-plugin": "workspace:",
4443
"@perspective-dev/test": "workspace:",
45-
"@types/chroma-js": "catalog:",
4644
"prettier": "catalog:",
4745
"typescript": "catalog:",
4846
"zx": "catalog:"

packages/viewer-datagrid/src/css/regular_table.css

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ regular-table table {
405405
td,
406406
th {
407407
border-color: var(--psp-inactive--border-color, #8b868045);
408-
height: 23px;
408+
height: var(--psp-datagrid--row--height, 23px);
409409
}
410410

411411
.psp-header-group {
@@ -416,8 +416,8 @@ regular-table table {
416416
border-bottom-width: 0px;
417417

418418
span {
419-
height: 23px;
420-
min-height: 23px;
419+
height: var(--psp-datagrid--row--height, 23px);
420+
min-height: var(--psp-datagrid--row--height, 23px);
421421
}
422422
}
423423

@@ -497,7 +497,7 @@ regular-table table thead tr:last-child:after {
497497
width: 10000px;
498498
box-sizing: border-box;
499499
display: block;
500-
height: 23px;
500+
height: var(--psp-datagrid--row--height, 23px);
501501
content: " ";
502502
border-bottom: 1px solid var(--psp-inactive--border-color);
503503
}
@@ -507,7 +507,7 @@ regular-table table tbody tr:after {
507507
width: 10000px;
508508
box-sizing: border-box;
509509
display: block;
510-
height: 23px;
510+
height: var(--psp-datagrid--row--height, 23px);
511511
content: " ";
512512
border-top: 1px solid transparent;
513513
}

packages/viewer-datagrid/src/ts/color_utils.ts

Lines changed: 144 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,145 @@
1010
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
1111
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1212

13-
import chroma from "chroma-js";
1413
import type { ColorRecord } from "./types.js";
1514

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)) return [r, g, b];
32+
} else if (s.length === 6 || s.length === 8) {
33+
const r = parseInt(s.slice(0, 2), 16);
34+
const g = parseInt(s.slice(2, 4), 16);
35+
const b = parseInt(s.slice(4, 6), 16);
36+
if (!isNaN(r) && !isNaN(g) && !isNaN(b)) return [r, g, b];
37+
}
38+
return null;
39+
}
40+
41+
/** Parse a CSS `rgb()` or `rgba()` functional color. Returns `null` if `input` does not match. Alpha is ignored. */
42+
function parse_rgb_fn(input: string): RGB | null {
43+
const m = input.match(
44+
/^rgba?\(\s*([\d.]+)\s*[, ]\s*([\d.]+)\s*[, ]\s*([\d.]+)/i,
45+
);
46+
if (!m) return null;
47+
return [
48+
Math.round(parseFloat(m[1])),
49+
Math.round(parseFloat(m[2])),
50+
Math.round(parseFloat(m[3])),
51+
];
52+
}
53+
54+
/** 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. */
55+
function parse_via_canvas(input: string): RGB {
56+
if (!parse_ctx) {
57+
const canvas = document.createElement("canvas");
58+
canvas.width = canvas.height = 1;
59+
parse_ctx = canvas.getContext("2d");
60+
}
61+
if (!parse_ctx) return [0, 0, 0];
62+
parse_ctx.fillStyle = "#000";
63+
parse_ctx.fillStyle = input;
64+
const normalized = parse_ctx.fillStyle as string;
65+
return parse_hex(normalized) ?? parse_rgb_fn(normalized) ?? [0, 0, 0];
66+
}
67+
68+
/** 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. */
69+
export function parseColor(input: string): RGB {
70+
const key = input.trim();
71+
const cached = parse_cache.get(key);
72+
if (cached) return cached;
73+
const rgb = parse_hex(key) ?? parse_rgb_fn(key) ?? parse_via_canvas(key);
74+
parse_cache.set(key, rgb);
75+
return rgb;
76+
}
77+
78+
/** Format a single channel as a clamped, zero-padded two-digit hex byte. */
79+
function toHex(c: number): string {
80+
const v = Math.max(0, Math.min(255, Math.round(c)));
81+
return v.toString(16).padStart(2, "0");
82+
}
83+
84+
/** Format an `RGB` triple as a `#rrggbb` hex string. Channels are clamped to `[0, 255]`. */
85+
export function rgbToHex([r, g, b]: RGB): string {
86+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
87+
}
88+
89+
/** Convert sRGB to HSL. Output `h` is in degrees `[0, 360)`; `s` and `l` are in `[0, 1]`. */
90+
export function rgbToHsl([r, g, b]: RGB): HSL {
91+
const rn = r / 255,
92+
gn = g / 255,
93+
bn = b / 255;
94+
const max = Math.max(rn, gn, bn);
95+
const min = Math.min(rn, gn, bn);
96+
const l = (max + min) / 2;
97+
const d = max - min;
98+
let h = 0;
99+
let s = 0;
100+
if (d !== 0) {
101+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
102+
if (max === rn) h = ((gn - bn) / d + (gn < bn ? 6 : 0)) * 60;
103+
else if (max === gn) h = ((bn - rn) / d + 2) * 60;
104+
else h = ((rn - gn) / d + 4) * 60;
105+
}
106+
return [h, s, l];
107+
}
108+
109+
/** 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]`. */
110+
export function hslToRgb([h, s, l]: HSL): RGB {
111+
const hn = (((h % 360) + 360) % 360) / 360;
112+
if (s === 0) {
113+
const v = Math.round(l * 255);
114+
return [v, v, v];
115+
}
116+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
117+
const p = 2 * l - q;
118+
const f = (t: number): number => {
119+
if (t < 0) t += 1;
120+
if (t > 1) t -= 1;
121+
if (t < 1 / 6) return p + (q - p) * 6 * t;
122+
if (t < 1 / 2) return q;
123+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
124+
return p;
125+
};
126+
return [
127+
Math.round(f(hn + 1 / 3) * 255),
128+
Math.round(f(hn) * 255),
129+
Math.round(f(hn - 1 / 3) * 255),
130+
];
131+
}
132+
133+
/**
134+
* Blend two `RGB` colors using LRGB (gamma-naive linear) interpolation,
135+
* matching chroma-js's default `mix` mode. `f` is the weight of `b` in `[0, 1]`
136+
* (0 returns `a`, 1 returns `b`).
137+
*/
138+
export function mixRgb(a: RGB, b: RGB, f = 0.5): RGB {
139+
return [
140+
Math.round(Math.sqrt(a[0] * a[0] * (1 - f) + b[0] * b[0] * f)),
141+
Math.round(Math.sqrt(a[1] * a[1] * (1 - f) + b[1] * b[1] * f)),
142+
Math.round(Math.sqrt(a[2] * a[2] * (1 - f) + b[2] * b[2] * f)),
143+
];
144+
}
145+
146+
/** 50/50 LRGB blend of CSS color `a` with `RGB`-ish triple `b`, returned as `#rrggbb`. */
16147
export function blend(a: string, b: number[]): string {
17-
return chroma.mix(a, `rgb(${b[0]},${b[1]},${b[2]})`, 0.5).hex();
148+
return rgbToHex(mixRgb(parseColor(a), [b[0], b[1], b[2]], 0.5));
18149
}
19150

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.
151+
/** 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. */
22152
export function rgbaToRgb(
23153
[r, g, b, a]: [number, number, number, number],
24154
source: [number, number, number] = [255, 255, 255],
@@ -30,7 +160,7 @@ export function rgbaToRgb(
30160
return [f(0, r), f(1, g), f(2, b)];
31161
}
32162

33-
// Chroma does this but why bother?
163+
/** Pick a readable foreground (`#161616` or `#ffffff`) for the given background using a perceptual luminance threshold. */
34164
export function infer_foreground_from_background([r, g, b]: [
35165
number,
36166
number,
@@ -42,21 +172,19 @@ export function infer_foreground_from_background([r, g, b]: [
42172
: "#ffffff";
43173
}
44174

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();
175+
/** Build a CSS `linear-gradient` that fans `rgb` ±15° in hue, used as the negative-value swatch in column color pickers. */
176+
function make_gradient(rgb: RGB): string {
177+
const [h, s, l] = rgbToHsl(rgb);
178+
const [r, g, b] = rgb;
179+
const [r1, g1, b1] = hslToRgb([h - 15, s, l]);
180+
const [r2, g2, b2] = hslToRgb([h + 15, s, l]);
53181
return `linear-gradient(to right top,rgb(${r1},${g1},${b1}),rgb(${r},${g},${b}) 50%,rgb(${r2},${g2},${b2}))`;
54182
}
55183

184+
/** Precompute the tuple of derived color strings (RGB channels, gradient, opaque/transparent rgba) cached on the model for a configured plugin color. */
56185
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();
186+
const rgb = parseColor(color);
187+
const _neg_grad = make_gradient(rgb);
60188
return [
61189
color,
62190
rgb[0],

packages/viewer-datagrid/src/ts/custom_elements/datagrid.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ import datagridStyles from "../../../dist/css/perspective-viewer-datagrid.css";
2222
import { format_raw } from "../data_listener/format_cell.js";
2323

2424
import type { View, ViewWindow } from "@perspective-dev/client";
25-
import type { IPerspectiveViewerPlugin } from "@perspective-dev/viewer";
25+
import type {
26+
HTMLPerspectiveViewerElement,
27+
IPerspectiveViewerPlugin,
28+
} from "@perspective-dev/viewer";
2629
import type {
2730
DatagridModel,
2831
DatagridToolbarElement,
2932
EditMode,
30-
PerspectiveViewerElement,
3133
DatagridPluginConfig,
3234
ColumnsConfig,
3335
} from "../types.js";
@@ -171,7 +173,7 @@ export class HTMLPerspectiveViewerDatagridPluginElement
171173
}
172174

173175
async render(viewport?: ViewWindow): Promise<string> {
174-
const viewer = this.parentElement as PerspectiveViewerElement;
176+
const viewer = this.parentElement as HTMLPerspectiveViewerElement;
175177
const view = await viewer.getView();
176178
const json = await view.to_columns(viewport as any);
177179
const cols = await view.column_paths(viewport as any);

packages/viewer-datagrid/src/ts/custom_elements/toolbar.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,10 @@
1010
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
1111
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1212

13+
import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
1314
import TOOLBAR_STYLE from "../../../dist/css/perspective-viewer-datagrid-toolbar.css";
1415
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";
1917

2018
const stylesheet = new CSSStyleSheet();
2119
stylesheet.replaceSync(TOOLBAR_STYLE);
@@ -53,7 +51,7 @@ export class HTMLPerspectiveViewerDatagridToolbarElement extends HTMLElement {
5351
</div>
5452
`;
5553

56-
const viewer = this.parentElement as PerspectiveViewerElement;
54+
const viewer = this.parentElement as HTMLPerspectiveViewerElement;
5755
const plugin = this.previousElementSibling as DatagridPluginElement;
5856

5957
plugin._scroll_lock = this.shadowRoot!.querySelector(

packages/viewer-datagrid/src/ts/data_listener/format_cell.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import { FormatterCache, Formatter } from "./formatter_cache.js";
1414
import type { DatagridModel, ColumnsConfig, ColumnConfig } from "../types.js";
15-
import { ColumnType } from "@perspective-dev/client";
15+
import type { ColumnType } from "@perspective-dev/client";
1616

1717
const FORMAT_CACHE = new FormatterCache();
1818
const MAX_BAR_WIDTH_PCT = 1;

packages/viewer-datagrid/src/ts/data_listener/formatter_cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
1111
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1212

13-
import { ColumnType } from "@perspective-dev/client";
13+
import type { ColumnType } from "@perspective-dev/client";
1414
import type { ColumnConfig } from "../types.js";
1515

1616
export interface Formatter {

packages/viewer-datagrid/src/ts/data_listener/index.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,10 @@ import {
1717
format_tree_header,
1818
format_tree_header_row_path,
1919
} from "./format_tree_header.js";
20-
import type {
21-
DatagridModel,
22-
PerspectiveViewerElement,
23-
RegularTable,
24-
Schema,
25-
} from "../types.js";
20+
import type { DatagridModel, RegularTable, Schema } from "../types.js";
2621
import type { CellScalar, DataResponse } from "regular-table/dist/esm/types.js";
27-
import { ViewConfig, ViewWindow } from "@perspective-dev/client";
22+
import type { ViewConfig, ViewWindow } from "@perspective-dev/client";
23+
import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
2824

2925
interface ColumnData {
3026
__ROW_PATH__?: unknown[][];
@@ -40,7 +36,7 @@ interface ColumnData {
4036
* @returns A data listener for the plugin.
4137
*/
4238
export function createDataListener(
43-
viewer: PerspectiveViewerElement,
39+
viewer: HTMLPerspectiveViewerElement,
4440
): (
4541
regularTable: RegularTable,
4642
x0: number,

packages/viewer-datagrid/src/ts/event_handlers/click.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,15 @@
1212

1313
import * as edit_click from "./click/edit_click.js";
1414
import * as edit_keydown from "./keydown/edit_keydown.js";
15-
import type {
16-
DatagridModel,
17-
PerspectiveViewerElement,
18-
SelectedPositionMap,
19-
} from "../types.js";
15+
import type { DatagridModel, SelectedPositionMap } from "../types.js";
2016
import { isEditableMode } from "../types.js";
2117
import { RegularTableElement } from "regular-table";
18+
import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
2219

2320
export function createKeydownListener(
2421
model: DatagridModel,
2522
table: RegularTableElement,
26-
viewer: PerspectiveViewerElement,
23+
viewer: HTMLPerspectiveViewerElement,
2724
selected_position_map: SelectedPositionMap,
2825
): EventListener {
2926
return (event: Event): void => {
@@ -51,7 +48,7 @@ export function createKeydownListener(
5148
export function createEditClickListener(
5249
model: DatagridModel,
5350
table: RegularTableElement,
54-
viewer: PerspectiveViewerElement,
51+
viewer: HTMLPerspectiveViewerElement,
5552
): EventListener {
5653
return (event: Event): void => {
5754
const mouseEvent = event as MouseEvent;

packages/viewer-datagrid/src/ts/event_handlers/click/edit_click.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ import { CellMetadataBody } from "regular-table/dist/esm/types.js";
1414
import {
1515
type RegularTable,
1616
type DatagridModel,
17-
type PerspectiveViewerElement,
1817
get_psp_type,
1918
} from "../../types.js";
2019

20+
import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
21+
2122
export function write_cell(
2223
table: RegularTable,
2324
model: DatagridModel,
@@ -58,7 +59,7 @@ export function write_cell(
5859
export function clickListener(
5960
model: DatagridModel,
6061
table: RegularTable,
61-
_viewer: PerspectiveViewerElement,
62+
_viewer: HTMLPerspectiveViewerElement,
6263
event: MouseEvent,
6364
): void {
6465
const meta = table.getMeta(event.target as HTMLElement);

0 commit comments

Comments
 (0)