Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
- only run `pnpm exec tsgo` when TypeScript files or TS-facing configs changed
- when TypeScript files change, run `pnpm exec biome lint` on touched paths only (read-only; no `--write`); do not run `jsformat`, `jslint`, or `biome check --write` on `./src` — formatting the tree would touch many unrelated lines; do not remove `biome-ignore` comments without reading the rationale and confirming the underlying constraint is still addressed
- for Python checks in worktrees, prefer the local `venv`:
- tests: `./venv/bin/python -m pytest python/mdvtools/tests -m "not performance"`
- pyright: from `python/`, run `../venv/bin/pyright`
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"docker_stack_prune": "docker stack rm mdv_stack && docker container prune -f && docker image prune -a -f && docker volume prune -f",
"fix_pnpm_win": "pnpm config set script-shell \"cmd\"",
"jslint": "biome lint --write ./src",
"jscheck": "biome lint ./src",
"jsformat": "biome format --write ./src",
"test": "vitest",
"playwright-setup": "pnpm install && pnpm exec playwright install --with-deps",
Expand Down
2 changes: 1 addition & 1 deletion src/charts/css/charts.css
Original file line number Diff line number Diff line change
Expand Up @@ -972,4 +972,4 @@ select, input, textarea {
width: 1em;
height: 1em;
}
}
}
1 change: 1 addition & 0 deletions src/charts/schemas/ChartConfigSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ const ContourVisualConfigSchema = z.object({

const DensityFieldsConfigSchema = z.object({
densityFields: FieldSpecsSchema.optional().describe("Numeric fields rendered as density overlays"),
density_mode: z.enum(["overlay", "grid"]).optional().describe("Whether density fields are overlaid or shown as a small-multiple grid"),
}).describe("Density field selection shared by contour-based charts");

const LegacyContourCategorySelectionConfigSchema = z.object({
Expand Down
49 changes: 48 additions & 1 deletion src/lib/deckMonkeypatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { EventManager, InputDirection, Pan, Pinch, Tap } from 'mjolnir.js';
// import { EVENT_HANDLERS, RECOGNIZERS } from '@deck.gl/core/dist/lib/constants';
import { Deck } from '@deck.gl/core';
import { EditableGeoJsonLayer } from '@deck.gl-community/editable-layers';
import type { Position } from '@turf/helpers';
import { unprojectCanvasPoint } from '@/react/components/densityGridUtils';

// we don't need the keys; we'll break in.
// we aren't really using this as an actual subclass, just declaring things as public so we can access them.
Expand Down Expand Up @@ -47,7 +49,52 @@ class EditableLayer {
```
*/

export class MonkeyPatchEditableGeoJsonLayer extends EditableGeoJsonLayer {}
/**
* Editable layers use `context.viewport.unproject` with canvas-relative pixels.
* In density-grid mode one canvas hosts many sub-viewports, so we resolve the
* viewport under the pointer before unprojecting.
*/
export class MonkeyPatchEditableGeoJsonLayer extends EditableGeoJsonLayer {
getMapCoords(screenCoords: Position): Position {
const deck = this.context.deck;
const viewports = deck?.getViewports?.();
if (viewports && viewports.length > 1) {
return unprojectCanvasPoint(viewports, screenCoords[0], screenCoords[1]) as Position;
}
return this.context.viewport.unproject([screenCoords[0], screenCoords[1]]) as Position;
}

getPicks(screenCoords: [number, number]) {
const deck = this.context.deck;
if (!deck) {
return [];
}
const radius = this.props.pickingRadius;
const depth = this.props.pickingDepth;
const layerId = this.props.id;
const allPicks = deck.pickMultipleObjects({
x: screenCoords[0],
y: screenCoords[1],
radius,
depth,
});
const matching = allPicks.filter(
(pick) =>
pick.layer?.id &&
(pick.layer.id === layerId || pick.layer.id.startsWith(`${layerId}-`)),
);
if (matching.length > 0) {
return matching;
}
return deck.pickMultipleObjects({
x: screenCoords[0],
y: screenCoords[1],
layerIds: [layerId],
radius,
depth,
});
}
}

const EVENT_HANDLERS: { [eventName: string]: string } = {
click: 'onClick', //is onClick a thing?
Expand Down
103 changes: 103 additions & 0 deletions src/react/components/ChartArrayLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* Multi-chart layout shell (density grid and similar). Layout modes, flex/grid CSS, and
* resize measurement are an initial prototype — not a stable API; expect rework for
* user-configurable layouts and context-specific arrangements.
*/
import {
useCallback,
useImperativeHandle,
useRef,
type CSSProperties,
type ReactNode,
type Ref,
} from "react";
import "./chartArrayLayout.css";
import {
getChartArrayLayoutMode,
type ChartArrayLayoutMode,
} from "./chartArrayLayoutUtils";
import { useChartArrayLayoutGeometry } from "../hooks/useChartArrayLayoutGeometry";

export type ChartArrayLayoutHandle = {
root: HTMLDivElement | null;
cells: readonly (HTMLDivElement | null)[];
};

type ChartArrayLayoutProps = {
cellCount: number;
cellKeys?: readonly string[];
/** Defaults from {@link getChartArrayLayoutMode}; override for future user layout prefs. */
layout?: ChartArrayLayoutMode;
layoutRef?: Ref<ChartArrayLayoutHandle>;
className?: string;
style?: CSSProperties;
/** Shared-canvas layer (e.g. DeckGL) sized to the grid root via CSS. */
canvasOverlay?: ReactNode;
renderCell: (index: number) => ReactNode;
};

export default function ChartArrayLayout({
cellCount,
cellKeys,
layout,
layoutRef,
className,
style,
canvasOverlay,
renderCell,
}: ChartArrayLayoutProps) {
const layoutMode = layout ?? getChartArrayLayoutMode(cellCount);
const rootRef = useRef<HTMLDivElement>(null);
const { gridFillsViewport, gridColumns } = useChartArrayLayoutGeometry(
rootRef,
cellCount,
layoutMode,
);
const cellRefs = useRef<(HTMLDivElement | null)[]>([]);

const setCellRef = useCallback((index: number, element: HTMLDivElement | null) => {
cellRefs.current[index] = element;
}, []);
Comment on lines +56 to +60

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Truncate stale cell refs when cellCount decreases.

cellRefs.current can keep old entries after a shrink, so layoutRef.cells may expose stale DOM refs beyond current cells.

Suggested change
 import {
     useCallback,
+    useEffect,
     useImperativeHandle,
     useRef,
...
     const cellRefs = useRef<(HTMLDivElement | null)[]>([]);
+    useEffect(() => {
+        cellRefs.current.length = cellCount;
+    }, [cellCount]);

Also applies to: 91-100

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/react/components/ChartArrayLayout.tsx` around lines 56 - 60,
cellRefs.current can retain stale entries when the number of cells shrinks; add
logic to truncate cellRefs.current to the current cellCount whenever cellCount
decreases (e.g., in a useEffect watching cellCount) so layoutRef.cells won't
expose DOM nodes past the active range, and update setCellRef to guard against
writing refs at indices >= cellCount (and similarly apply the same
truncation/guard to the other identical ref array usage around the 91-100
block); reference cellRefs, setCellRef, layoutRef.cells and the cellCount
prop/state when implementing this change.


useImperativeHandle(
layoutRef,
() => ({
get root() {
return rootRef.current;
},
get cells() {
return cellRefs.current;
},
}),
[],
);

return (
<div
ref={rootRef}
className={["mdv-chart-array", className].filter(Boolean).join(" ")}
data-layout={layoutMode}
data-grid-fill={layoutMode === "grid" && gridFillsViewport ? "true" : undefined}
style={
layoutMode === "grid"
? {
...style,
["--mdv-chart-array-columns" as string]: String(gridColumns),
}
: style
}
>
{canvasOverlay ? <div className="mdv-chart-array__canvas">{canvasOverlay}</div> : null}
{Array.from({ length: cellCount }, (_, index) => (
<div
key={cellKeys?.[index] ?? index}
ref={(element) => setCellRef(index, element)}
className="mdv-chart-array__cell"
data-chart-array-index={String(index)}
>
{renderCell(index)}
</div>
))}
</div>
);
}
Loading
Loading