Skip to content

Commit

Permalink
Merge pull request #283 from allen-cell-animated/feature/url-feature-key
Browse files Browse the repository at this point in the history
- Changes Dataset to generate feature keys if none exists (see new `getKeyFromName` method in `data_utils.ts`).
- Updates Datasets to lookup features by key instead of name.
- Updates all other references to features to use keys instead of names.
- The URL will now preferentially use feature keys, but the viewer can also look up by feature name if the key is not found. (This happens on the initial URL load for the scatterplot axes, feature, and thresholds/filters.)
  • Loading branch information
ShrimpCryptid authored Mar 29, 2024
2 parents bc540d7 + d0dfe32 commit f326235
Show file tree
Hide file tree
Showing 17 changed files with 501 additions and 316 deletions.
122 changes: 66 additions & 56 deletions src/Viewer.tsx

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions src/colorizer/ColorizeCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export default class ColorizeCanvas {
private points: Float32Array;
private canvasResolution: Vector2 | null;

private featureName: string | null;
private featureKey: string | null;
private selectedBackdropKey: string | null;
private colorRamp: ColorRamp;
private colorMapRangeMin: number;
Expand Down Expand Up @@ -191,7 +191,7 @@ export default class ColorizeCanvas {

this.dataset = null;
this.canvasResolution = null;
this.featureName = null;
this.featureKey = null;
this.selectedBackdropKey = null;
this.colorRamp = new ColorRamp(["black"]);
this.categoricalPalette = new ColorRamp(["black"]);
Expand Down Expand Up @@ -448,12 +448,12 @@ export default class ColorizeCanvas {
this.setUniform("highlightedId", this.track.getIdAtTime(this.currentFrame));
}

setFeature(name: string): void {
if (!this.dataset?.hasFeature(name)) {
setFeatureKey(key: string): void {
if (!this.dataset?.hasFeatureKey(key)) {
return;
}
const featureData = this.dataset.getFeatureData(name)!;
this.featureName = name;
const featureData = this.dataset.getFeatureData(key)!;
this.featureKey = key;
this.setUniform("featureData", featureData.tex);
this.render(); // re-render necessary because map range may have changed
}
Expand All @@ -467,10 +467,10 @@ export default class ColorizeCanvas {
}

resetColorMapRange(): void {
if (!this.featureName) {
if (!this.featureKey) {
return;
}
const featureData = this.dataset?.getFeatureData(this.featureName);
const featureData = this.dataset?.getFeatureData(this.featureKey);
if (featureData) {
this.colorMapRangeMin = featureData.min;
this.colorMapRangeMax = featureData.max;
Expand Down Expand Up @@ -575,7 +575,7 @@ export default class ColorizeCanvas {
* selected feature.
*/
updateRamp(): void {
if (this.featureName && this.dataset?.isFeatureCategorical(this.featureName)) {
if (this.featureKey && this.dataset?.isFeatureCategorical(this.featureKey)) {
this.setUniform("colorRamp", this.categoricalPalette.texture);
this.setUniform("featureColorRampMin", 0);
this.setUniform("featureColorRampMax", MAX_FEATURE_CATEGORIES - 1);
Expand Down
103 changes: 70 additions & 33 deletions src/colorizer/Dataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { RGBAFormat, RGBAIntegerFormat, Texture, Vector2 } from "three";

import { MAX_FEATURE_CATEGORIES } from "../constants";
import { FeatureArrayType, FeatureDataType } from "./types";
import { getKeyFromName } from "./utils/data_utils";
import { AnyManifestFile, ManifestFile, ManifestFileMetadata, updateManifestVersion } from "./utils/dataset_utils";
import * as urlUtils from "./utils/url_utils";

Expand All @@ -18,6 +19,8 @@ export enum FeatureType {
}

export type FeatureData = {
name: string;
key: string;
data: Float32Array;
tex: Texture;
min: number;
Expand Down Expand Up @@ -57,6 +60,7 @@ export default class Dataset {

private arrayLoader: IArrayLoader;
// Use map to enforce ordering
/** Ordered map from feature keys to feature data. */
private features: Map<string, FeatureData>;

private outlierFile?: string;
Expand Down Expand Up @@ -122,10 +126,11 @@ export default class Dataset {

/**
* Loads a feature from the dataset, fetching its data from the provided url.
* @returns A promise of an array tuple containing the feature name and its FeatureData.
* @returns A promise of an array tuple containing the feature key and its FeatureData.
*/
private async loadFeature(metadata: ManifestFile["features"][number]): Promise<[string, FeatureData]> {
const name = metadata.name;
const key = metadata.key || getKeyFromName(name);
const url = this.resolveUrl(metadata.data);
const source = await this.arrayLoader.load(url);
const featureType = this.parseFeatureType(metadata.type);
Expand All @@ -142,8 +147,10 @@ export default class Dataset {
}

return [
name,
key,
{
name,
key,
tex: source.getTexture(FeatureDataType.F32),
data: source.getBuffer(FeatureDataType.F32),
min: source.getMin(),
Expand All @@ -155,57 +162,87 @@ export default class Dataset {
];
}

public hasFeature(name: string): boolean {
return this.featureNames.includes(name);
public hasFeatureKey(key: string): boolean {
return this.featureKeys.includes(key);
}

/**
* Returns the feature key if a feature with a matching key or name exists in the
* dataset.
* @param keyOrName String key or name of the feature to find.
* @returns The feature key if found, otherwise undefined.
*/
public findFeatureByKeyOrName(keyOrName: string): string | undefined {
if (this.hasFeatureKey(keyOrName)) {
return keyOrName;
} else {
return this.findFeatureKeyFromName(keyOrName);
}
}

/**
* Attempts to find a matching feature key for a feature name.
* @returns The feature key if found, otherwise undefined.
*/
public findFeatureKeyFromName(name: string): string | undefined {
return Array.from(this.features.values()).find((f) => f.name === name)?.key;
}

/**
* Attempts to get the feature data from this dataset for the given feature name.
* Attempts to get the feature data from this dataset for the given feature key.
* Returns `undefined` if feature is not in the dataset.
*/
public getFeatureData(name: string): FeatureData | undefined {
return this.features.get(name);
public getFeatureData(key: string): FeatureData | undefined {
return this.features.get(key);
}

public getFeatureNameWithUnits(name: string): string {
const units = this.getFeatureUnits(name);
if (units) {
return `${name} (${units})`;
} else {
return name;
}
public getFeatureName(key: string): string | undefined {
return this.features.get(key)?.name;
}

/**
* Gets the feature's units if it exists; otherwise returns an empty string.
*/
public getFeatureUnits(name: string): string {
return this.getFeatureData(name)?.units || "";
public getFeatureUnits(key: string): string {
return this.getFeatureData(key)?.units || "";
}

public getFeatureNameWithUnits(key: string): string {
const name = this.getFeatureName(key);
if (!name) {
return "N/A";
}
const units = this.getFeatureUnits(key);
if (units) {
return `${name} (${units})`;
} else {
return name;
}
}

/**
* Returns the FeatureType of the given feature, if it exists.
* @param name Feature name to retrieve
* @param key Feature key to retrieve
* @throws An error if the feature does not exist.
* @returns The FeatureType of the given feature (categorical, continuous, or discrete)
*/
public getFeatureType(name: string): FeatureType {
const featureData = this.getFeatureData(name);
public getFeatureType(key: string): FeatureType {
const featureData = this.getFeatureData(key);
if (featureData === undefined) {
throw new Error("Feature '" + name + "' does not exist in dataset.");
throw new Error("Feature '" + key + "' does not exist in dataset.");
}
return featureData.type;
}

/**
* Returns the array of string categories for the given feature, if it exists and is categorical.
* @param name Feature name to retrieve.
* @param key Feature key to retrieve.
* @returns The array of string categories for the given feature, or null if the feature is not categorical.
*/
public getFeatureCategories(name: string): string[] | null {
const featureData = this.getFeatureData(name);
public getFeatureCategories(key: string): string[] | null {
const featureData = this.getFeatureData(key);
if (featureData === undefined) {
throw new Error("Feature '" + name + "' does not exist in dataset.");
throw new Error("Feature '" + key + "' does not exist in dataset.");
}
if (featureData.type === FeatureType.CATEGORICAL) {
return featureData.categories;
Expand All @@ -214,8 +251,8 @@ export default class Dataset {
}

/** Returns whether the given feature represents categorical data. */
public isFeatureCategorical(name: string): boolean {
const featureData = this.getFeatureData(name);
public isFeatureCategorical(key: string): boolean {
const featureData = this.getFeatureData(key);
return featureData !== undefined && featureData.type === FeatureType.CATEGORICAL;
}

Expand Down Expand Up @@ -246,12 +283,12 @@ export default class Dataset {
return this.frames?.length || 0;
}

public get featureNames(): string[] {
public get featureKeys(): string[] {
return Array.from(this.features.keys());
}

public get numObjects(): number {
const featureData = this.getFeatureData(this.featureNames[0]);
const featureData = this.getFeatureData(this.featureKeys[0]);
if (!featureData) {
throw new Error("Dataset.numObjects: The first feature could not be loaded. Is the dataset manifest file valid?");
}
Expand Down Expand Up @@ -362,8 +399,8 @@ export default class Dataset {
this.bounds = bounds;

// Keep original sorting order of features by inserting in promise order.
featureResults.forEach(([name, data]) => {
this.features.set(name, data);
featureResults.forEach(([key, data]) => {
this.features.set(key, data);
});

if (this.features.size === 0) {
Expand Down Expand Up @@ -428,10 +465,10 @@ export default class Dataset {
* Get the times and values of a track for a given feature
* this data is suitable to hand to d3 or plotly as two arrays of domain and range values
*/
public buildTrackFeaturePlot(track: Track, feature: string): { domain: number[]; range: number[] } {
const featureData = this.getFeatureData(feature);
public buildTrackFeaturePlot(track: Track, featureKey: string): { domain: number[]; range: number[] } {
const featureData = this.getFeatureData(featureKey);
if (!featureData) {
throw new Error("Dataset.buildTrackFeaturePlot: Feature '" + feature + "' does not exist in dataset.");
throw new Error("Dataset.buildTrackFeaturePlot: Feature '" + featureKey + "' does not exist in dataset.");
}
const range = track.ids.map((i) => featureData.data[i]);
const domain = track.times;
Expand Down
6 changes: 3 additions & 3 deletions src/colorizer/Plotting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ export default class Plotting {
this.dataset = dataset;
}

plot(track: Track, feature: string, time: number): void {
plot(track: Track, featureKey: string, time: number): void {
if (!this.dataset) {
return;
}
const plotinfo = this.dataset?.buildTrackFeaturePlot(track, feature);
const plotinfo = this.dataset?.buildTrackFeaturePlot(track, featureKey);
this.trace = {
x: plotinfo.domain,
y: plotinfo.range,
Expand All @@ -67,7 +67,7 @@ export default class Plotting {

const layout: Partial<Plotly.Layout> = {
yaxis: {
title: this.dataset.getFeatureNameWithUnits(feature),
title: this.dataset.getFeatureNameWithUnits(featureKey),
},
shapes: [
{
Expand Down
4 changes: 1 addition & 3 deletions src/colorizer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,7 @@ export enum ThresholdType {
}

type BaseFeatureThreshold = {
// TODO: Replace with key string
// featureKey: string;
featureName: string;
featureKey: string;
units: string;
type: ThresholdType;
};
Expand Down
Loading

0 comments on commit f326235

Please sign in to comment.