Skip to content

Commit

Permalink
chore: Setup biome linting (#197)
Browse files Browse the repository at this point in the history
  • Loading branch information
manzt authored Jul 22, 2024
1 parent 04b4a11 commit 5b0f597
Show file tree
Hide file tree
Showing 11 changed files with 134 additions and 89 deletions.
31 changes: 16 additions & 15 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,34 @@ Set the `upstream` remote to the base `vizarr` repository:
git remote add upstream https://github.com/hms-dbmi/vizarr.git
```

Install the dependencies to develop and build `vizarr` via `npm`.
Install the dependencies to develop and build `vizarr` via `pnpm`.

```bash
npm install
pnpm install
```

> Note: You need to have [Node.js](https://nodejs.org/en/) (v15.0 or later) to build
> and develop `vizarr`. I recommend using [`nvm`](https://github.com/nvm-sh/nvm) or
> Note: You need to have [Node.js](https://nodejs.org/en/) (v20.0 or later) to build
> and develop `vizarr`. I recommend using [`nvm`](https://github.com/nvm-sh/nvm) or
> [`fnm`](https://github.com/Schniz/fnm) to manage different version of Node.js
> on your machine.
### Running the development server

```bash
npm start
pnpm dev
```

The `start` command will start a development server on `http://localhost:8080` which you can navigate
to in your web browser. You can "live" edit the contents of any of the files within `src/` or `public/`
when running this server; changes are reflected instantly in the browser. Stop the development
server when you are done making changes.
The `dev` command will start a development server on `http://localhost:5173`
which you can navigate to in your web browser. You can "live" edit the contents
of any of the files within `src/` or `public/` when running this server;
changes are reflected instantly in the browser. Stop the development server
when you are done making changes.

- `src/` - contains all TypeScript source code
- `public/` - contains all static assets required for the site

Have a look at the other `script` commands in `package.json` for the project. These are standard to any JS
build and can be executed by running `npm run <command>`.
build and can be executed by running `pnpm <command>`.

### Making changes

Expand All @@ -65,17 +66,17 @@ Update your remote branch:
git push -u origin your-feature-branch-name
```

You can then make a pull-request to `vizarr`'s `main` branch. When making a pull-request,
your code will be checked for linting with `prettier`. Please run `npm run format`
to automatically format your code when making a pull-request.
You can then make a pull-request to `vizarr`'s `main` branch. When making a
pull-request, your code will be checked for linting with `biome`. Please run
`pnpm fix` to automatically format your code when making a pull-request.


### (Note to self) Building and publishing `vizarr`

Build a production version of the site:

```bash
npm version [<new version> | major | minor | patch]
npm publish
pnpm version [<new version> | major | minor | patch]
pnpm publish
```

2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"lineWidth": 120
},
"linter": {
"enabled": false,
"enabled": true,
"rules": {
"recommended": true,
"style": {
Expand Down
20 changes: 10 additions & 10 deletions main.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import debounce from "just-debounce-it";
import * as vizarr from "./src/index";

function initStandaloneApp(viewer: vizarr.VizarrViewer) {
async function main() {
console.log(`vizarr v${vizarr.version}: https://github.com/hms-dbmi/vizarr`);
// biome-ignore lint/style/noNonNullAssertion: We know the element exists
const viewer = await vizarr.createViewer(document.querySelector("#root")!);
const url = new URL(window.location.href);

if (!url.searchParams.has("source")) {
return;
}

// see if we have initial viewState
if (url.searchParams.has("viewState")) {
const viewState = JSON.parse(url.searchParams.get("viewState")!);
const viewStateString = url.searchParams.get("viewState");
if (viewStateString) {
const viewState = JSON.parse(viewStateString);
viewer.setViewState(viewState);
}

Expand All @@ -26,9 +30,11 @@ function initStandaloneApp(viewer: vizarr.VizarrViewer) {
);

// parse image config
const config: any = {};
// @ts-expect-error - TODO: validate config
const config: vizarr.ImageLayerConfig = {};

for (const [key, value] of url.searchParams) {
// @ts-expect-error - TODO: validate config
config[key] = value;
}

Expand All @@ -43,10 +49,4 @@ function initStandaloneApp(viewer: vizarr.VizarrViewer) {
}
}

async function main() {
console.log(`vizarr v${vizarr.version}: https://github.com/hms-dbmi/vizarr`);
const viewer = await vizarr.createViewer(document.querySelector("#root")!);
initStandaloneApp(viewer);
}

main();
8 changes: 4 additions & 4 deletions src/components/LayerController/AxisSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Divider, Grid, Typography } from "@material-ui/core";
import { Slider } from "@material-ui/core";
import { withStyles } from "@material-ui/styles";
import { useAtom, useAtomValue } from "jotai";
import * as React from "react";
import type { ChangeEvent } from "react";
import React, { useState, useEffect } from "react";
import type { ControllerProps } from "../../state";
import DimensionOptions from "./AxisOptions";

Expand Down Expand Up @@ -32,13 +32,13 @@ function AxisSlider({ sourceAtom, layerAtom, axisIndex, max }: ControllerProps<P
axisLabel = axisLabel.toUpperCase();
}
// state of the slider to update UI while dragging
const [value, setValue] = useState(0);
const [value, setValue] = React.useState(0);

// If axis index change externally, need to update state
useEffect(() => {
React.useEffect(() => {
// Use first channel to get initial value of slider - can be undefined on first render
setValue(layer.layerProps.selections[0] ? layer.layerProps.selections[0][axisIndex] : 1);
}, [layer.layerProps.selections]);
}, [layer.layerProps.selections, axisIndex]);

const handleRelease = () => {
setLayer((prev) => {
Expand Down
21 changes: 14 additions & 7 deletions src/components/Viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ import { type WritableAtom, useAtom } from "jotai";
import { useAtomValue } from "jotai";
import * as React from "react";

import type { LayerState, ViewState } from "../state";
import type { LayerProps } from "@deck.gl/core/lib/layer";
import type { ZarrPixelSource } from "../ZarrPixelSource";
import type { ViewState } from "../state";
import { layerAtoms } from "../state";
import { fitBounds, isInterleaved } from "../utils";

function getLayerSize(props: LayerState["layerProps"]) {
type Data = { loader: ZarrPixelSource; rows: number; columns: number };
type VizarrLayer = Layer<unknown, LayerProps<unknown> & Data>;

function getLayerSize(props: Data) {
const { loader } = props;
const [base, maxZoom] = Array.isArray(loader) ? [loader[0], loader.length] : [loader, 0];
const interleaved = isInterleaved(base.shape);
Expand All @@ -24,17 +29,18 @@ function getLayerSize(props: LayerState["layerProps"]) {
}

function WrappedViewStateDeck(props: {
layers: Layer<any, any>[];
layers: Array<VizarrLayer | null>;
viewStateAtom: WritableAtom<ViewState | undefined, ViewState>;
}) {
const [viewState, setViewState] = useAtom(props.viewStateAtom);
const deckRef = React.useRef<DeckGL>(null);
const firstLayerProps = props.layers[0]?.props;

// If viewState hasn't been updated, use the first loader to guess viewState
// TODO: There is probably a better place / way to set the intital view and this is a hack.
if (deckRef.current && !viewState && props.layers[0]?.props?.loader) {
if (deckRef.current && !viewState && firstLayerProps?.loader) {
const { deck } = deckRef.current;
const { width, height, maxZoom } = getLayerSize(props.layers[0].props);
const { width, height, maxZoom } = getLayerSize(firstLayerProps);
const padding = deck.width < 400 ? 10 : deck.width < 600 ? 30 : 50; // Adjust depending on viewport width.
const bounds = fitBounds([width, height], [deck.width, deck.height], maxZoom, padding);
setViewState(bounds);
Expand All @@ -59,10 +65,11 @@ function WrappedViewStateDeck(props: {

function Viewer({ viewStateAtom }: { viewStateAtom: WritableAtom<ViewState | undefined, ViewState> }) {
const layerConstructors = useAtomValue(layerAtoms);
const layers = layerConstructors.map((layer) => {
// @ts-expect-error - Viv types are giving up an issue
const layers: Array<VizarrLayer | null> = layerConstructors.map((layer) => {
return !layer.on ? null : new layer.Layer(layer.layerProps);
});
return <WrappedViewStateDeck viewStateAtom={viewStateAtom} layers={layers as Layer<any, any>[]} />;
return <WrappedViewStateDeck viewStateAtom={viewStateAtom} layers={layers} />;
}

export default Viewer;
73 changes: 47 additions & 26 deletions src/gridLayer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { CompositeLayerProps } from "@deck.gl/core/lib/composite-layer";
import { CompositeLayer, SolidPolygonLayer, TextLayer } from "deck.gl";
import { CompositeLayer, type PickInfo, SolidPolygonLayer, TextLayer } from "deck.gl";
import pMap from "p-map";

import type { SolidPolygonLayerProps, TextLayerProps } from "@deck.gl/layers";
import { ColorPaletteExtension, XRLayer } from "@hms-dbmi/viv";
import type { SupportedTypedArray } from "@vivjs/types";
import type { ZarrPixelSource } from "./ZarrPixelSource";
import type { BaseLayerProps } from "./state";
import { assert } from "./utils";
Expand All @@ -14,8 +16,10 @@ export interface GridLoader {
name: string;
}

type Polygon = Array<[number, number]>;

export interface GridLayerProps
extends Omit<CompositeLayerProps<any>, "modelMatrix" | "opacity" | "onClick" | "id">,
extends Omit<CompositeLayerProps<unknown>, "modelMatrix" | "opacity" | "onClick" | "id">,
BaseLayerProps {
loaders: GridLoader[];
rows: number;
Expand All @@ -26,7 +30,8 @@ export interface GridLayerProps
}

const defaultProps = {
...(XRLayer as any).defaultProps,
// @ts-expect-error - XRLayer props are not typed
...XRLayer.defaultProps,
// Special grid props
loaders: { type: "array", value: [], compare: true },
spacer: { type: "number", value: 5, compare: true },
Expand All @@ -51,10 +56,10 @@ function validateWidthHeight(d: { data: { width: number; height: number } }[]) {
// Return early if no grid data. Maybe throw an error?
const { width, height } = first.data;
// Verify that all grid data is same shape (ignoring undefined)
d.forEach(({ data }) => {
if (!data) return;
for (const { data } of d) {
if (!data) continue;
assert(data.width === width && data.height === height, "Grid data is not same shape.");
});
}
return { width, height };
}

Expand All @@ -81,7 +86,7 @@ function refreshGridData(props: GridLayerProps) {
return pMap(loaders, mapper, { concurrency });
}

export default class GridLayer<P extends GridLayerProps = GridLayerProps> extends CompositeLayer<any, P> {
export default class GridLayer extends CompositeLayer<unknown, CompositeLayerProps<unknown> & GridLayerProps> {
initializeState() {
this.state = { gridData: [], width: 0, height: 0 };
refreshGridData(this.props).then((gridData) => {
Expand All @@ -90,7 +95,17 @@ export default class GridLayer<P extends GridLayerProps = GridLayerProps> extend
});
}

updateState({ props, oldProps, changeFlags }: { props: GridLayerProps; oldProps: GridLayerProps; changeFlags: any }) {
updateState({
props,
oldProps,
changeFlags,
}: {
props: GridLayerProps;
oldProps: GridLayerProps;
changeFlags: {
propsChanged: string | boolean | null;
};
}) {
const { propsChanged } = changeFlags;
const loaderChanged = typeof propsChanged === "string" && propsChanged.includes("props.loaders");
const loaderSelectionChanged = props.selections !== oldProps.selections;
Expand All @@ -102,7 +117,7 @@ export default class GridLayer<P extends GridLayerProps = GridLayerProps> extend
}
}

getPickingInfo({ info }: { info: any }) {
getPickingInfo({ info }: { info: PickInfo<unknown> }) {
// provide Grid row and column info for mouse events (hover & click)
if (!info.coordinate) {
return info;
Expand All @@ -112,16 +127,19 @@ export default class GridLayer<P extends GridLayerProps = GridLayerProps> extend
const [x, y] = info.coordinate;
const row = Math.floor(y / (height + spacer));
const column = Math.floor(x / (width + spacer));
info.gridCoord = { row, column }; // add custom property
return info;
return {
...info,
gridCoord: { row, column },
};
}

renderLayers() {
const { gridData, width, height } = this.state;
if (width === 0 || height === 0) return null; // early return if no data

const { rows, columns, spacer = 0, id = "" } = this.props;
const layers = gridData.map((d: any) => {
type Data = { row: number; col: number; loader: Pick<ZarrPixelSource, "dtype">; data: Array<SupportedTypedArray> };
const layers = gridData.map((d: Data) => {
const y = d.row * (height + spacer);
const x = d.col * (width + spacer);
const layerProps = {
Expand All @@ -132,44 +150,47 @@ export default class GridLayer<P extends GridLayerProps = GridLayerProps> extend
pickable: false,
extensions: [new ColorPaletteExtension()],
};
return new (XRLayer as any)({ ...this.props, ...layerProps });
// @ts-expect-error - XRLayer props are not well typed
return new XRLayer({ ...this.props, ...layerProps });
});

if (this.props.pickable) {
const [top, left] = [0, 0];
type Data = { polygon: Polygon };
const bottom = rows * (height + spacer);
const right = columns * (width + spacer);
const polygon = [
[left, top],
[right, top],
[0, 0],
[right, 0],
[right, bottom],
[left, bottom],
];
[0, bottom],
] satisfies Polygon;
const layerProps = {
data: [{ polygon }],
getPolygon: (d: any) => d.polygon,
getPolygon: (d) => d.polygon,
getFillColor: [0, 0, 0, 0], // transparent
getLineColor: [0, 0, 0, 0],
pickable: true, // enable picking
id: `${id}-GridLayer-picking`,
} as any; // I was having an issue with typescript here....
const pickableLayer = new SolidPolygonLayer({ ...this.props, ...layerProps });
layers.push(pickableLayer);
} satisfies SolidPolygonLayerProps<Data>;
// @ts-expect-error - SolidPolygonLayer props are not well typed
const layer = new SolidPolygonLayer<Data, SolidPolygonLayerProps<Data>>({ ...this.props, ...layerProps });
layers.push(layer);
}

if (this.props.text) {
const textLayer = new TextLayer({
type Data = { col: number; row: number; name: string };
const layer = new TextLayer<Data, TextLayerProps<Data>>({
id: `${id}-GridLayer-text`,
data: gridData,
getPosition: (d: any) => [d.col * (width + spacer), d.row * (height + spacer)],
getText: (d: any) => d.name,
getPosition: (d) => [d.col * (width + spacer), d.row * (height + spacer)],
getText: (d) => d.name,
getColor: [255, 255, 255, 255],
getSize: 16,
getAngle: 0,
getTextAnchor: "start",
getAlignmentBaseline: "top",
});
layers.push(textLayer);
layers.push(layer);
}

return layers;
Expand Down
2 changes: 1 addition & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function createViewer(element: HTMLElement, options: { menuOpen?: boolean
on: emitter.on.bind(emitter),
destroy: () => root.unmount(),
}),
[],
[setViewState, addImage],
);
React.useEffect(() => {
if (ref.current) {
Expand Down
Loading

0 comments on commit 5b0f597

Please sign in to comment.