Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Ephys Behavior #586

Merged
merged 31 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
23ae563
Update with new Ecephys behavior
garrettmflynn Feb 5, 2024
e02238f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 5, 2024
f9f8913
Merge branch 'main' into ephys-metadata-v2
CodyCBakerPhD Feb 5, 2024
7abb997
Merge branch 'main' into ephys-metadata-v2
garrettmflynn Feb 7, 2024
c0f2fd9
Update manage_neuroconv.py
garrettmflynn Feb 8, 2024
ef0be5d
Update base-metadata.schema.ts
garrettmflynn Feb 8, 2024
1e9319a
Merge branch 'main' into ephys-metadata-v2
CodyCBakerPhD Feb 8, 2024
9631b95
Merge branch 'main' into ephys-metadata-v2
CodyCBakerPhD Feb 8, 2024
e651280
Merge branch 'main' into ephys-metadata-v2
CodyCBakerPhD Feb 12, 2024
e659d76
Merge branch 'main' into ephys-metadata-v2
CodyCBakerPhD Feb 21, 2024
37984f5
Merge branch 'main' into ephys-metadata-v2
garrettmflynn Feb 27, 2024
b17e679
Electrode helper functions (#603)
CodyCBakerPhD Mar 11, 2024
8d8325b
Merge branch 'main' into ephys-metadata-v2
garrettmflynn Mar 11, 2024
4787feb
Fix merge and remove extra declarations
garrettmflynn Mar 11, 2024
10b7b3d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 11, 2024
cec5d21
Updates based on review
garrettmflynn Mar 11, 2024
4a9e2e6
Merge branch 'ephys-metadata-v2' of https://github.com/NeurodataWitho…
garrettmflynn Mar 11, 2024
40d3c00
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 11, 2024
1b8f309
Add extra inteface properties support
garrettmflynn Mar 11, 2024
b44bc51
Merge branch 'ephys-metadata-v2' of https://github.com/NeurodataWitho…
garrettmflynn Mar 11, 2024
20759c8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 11, 2024
9da6f66
Merge branch 'main' into ephys-metadata-v2
CodyCBakerPhD Mar 11, 2024
5409dd5
Update TSV
garrettmflynn Mar 11, 2024
3e5a1be
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 11, 2024
94ae7d7
Merge branch 'main' into ephys-metadata-v2
CodyCBakerPhD Mar 12, 2024
0347693
Convert ambiguous vales to strings
garrettmflynn Mar 12, 2024
828792b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 12, 2024
621eefc
Parse entries into JS with eval (over JSON)
garrettmflynn Mar 12, 2024
d7e037c
Merge branch 'ephys-metadata-v2' of https://github.com/NeurodataWitho…
garrettmflynn Mar 12, 2024
c3b50db
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 12, 2024
3f19cac
Merge branch 'main' into ephys-metadata-v2
CodyCBakerPhD Mar 12, 2024
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ src/build
.env
.env.local
.env.production

# Spyder
.spyproject/
353 changes: 289 additions & 64 deletions pyflask/manageNeuroconv/manage_neuroconv.py

Large diffs are not rendered by default.

41 changes: 36 additions & 5 deletions schemas/base-metadata.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { header, replaceRefsWithValue } from '../src/renderer/src/stories/forms/

import baseMetadataSchema from './json/base_metadata_schema.json' assert { type: "json" }

const uvMathFormat = `&micro;V`; //`<math xmlns="http://www.w3.org/1998/Math/MathML"><mo>&micro;</mo><mi>V</mi></math>`

function getSpeciesNameComponents(arr: any[]) {
const split = arr[arr.length - 1].split(' - ')
return {
Expand All @@ -13,6 +15,7 @@ function getSpeciesNameComponents(arr: any[]) {
}



function getSpeciesInfo(species: any[][] = []) {


Expand All @@ -34,6 +37,10 @@ function getSpeciesInfo(species: any[][] = []) {

}

const propsToInclude = {
ecephys: ["Device", "ElectrodeGroup", "Electrodes", "ElectrodeColumns", "definitions"]
}

export const preprocessMetadataSchema = (schema: any = baseMetadataSchema, global = false) => {


Expand Down Expand Up @@ -89,17 +96,40 @@ export const preprocessMetadataSchema = (schema: any = baseMetadataSchema, globa
// Override description of keywords
nwbProps.keywords.description = 'Terms to describe your dataset (e.g. Neural circuits, V1, etc.)' // Add description to keywords


const ecephys = copy.properties.Ecephys
const ophys = copy.properties.Ophys

if (ecephys) {

// Change rendering order for electrode table columns
const electrodesProp = ecephys.properties["Electrodes"]
for (let name in electrodesProp.properties) {
const interfaceProps = electrodesProp.properties[name].properties
const electrodeItems = interfaceProps["Electrodes"].items.properties
const uvProperties = ["gain_to_uV", "offset_to_uV"]

uvProperties.forEach(prop => {
electrodeItems[prop] = {}
electrodeItems[prop].title = prop.replace('uV', uvMathFormat)
console.log(electrodeItems[prop])
})
interfaceProps["Electrodes"].items.order = ["channel_name", "group_name", "shank_electrode_number", ...uvProperties];
interfaceProps["ElectrodeColumns"].items.order = ["name", "description", "data_type"];

}

}

if (ophys) {

ophys.required = Object.keys(ophys.properties)

const getProp = (name: string) => ophys.properties[name]

if (getProp("TwoPhotonSeries")) {
const tpsItemSchema = getProp("TwoPhotonSeries").items
const tpsItemSchema = getProp("TwoPhotonSeries")?.items

if (tpsItemSchema) {

tpsItemSchema.order = [
"name",
"description",
Expand All @@ -111,8 +141,9 @@ export const preprocessMetadataSchema = (schema: any = baseMetadataSchema, globa
}


if (getProp("ImagingPlane")) {
const imagingPlaneItems = getProp("ImagingPlane").items
const imagingPlaneItems = getProp("ImagingPlane")?.items

if (imagingPlaneItems) {
imagingPlaneItems.order = [
"name",
"description",
Expand Down
1 change: 0 additions & 1 deletion src/renderer/src/pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { GuidedSubjectsPage } from "./stories/pages/guided-mode/setup/GuidedSubj
import { GuidedSourceDataPage } from "./stories/pages/guided-mode/data/GuidedSourceData";
import { GuidedMetadataPage } from "./stories/pages/guided-mode/data/GuidedMetadata";
import { GuidedUploadPage } from "./stories/pages/guided-mode/options/GuidedUpload";
// import { GuidedConversionOptionsPage } from "./stories/pages/guided-mode/options/GuidedConversionOptions";
import { GuidedResultsPage } from "./stories/pages/guided-mode/results/GuidedResults";
import { Dashboard } from "./stories/Dashboard";
import { GuidedStubPreviewPage } from "./stories/pages/guided-mode/options/GuidedStubPreview";
Expand Down
126 changes: 88 additions & 38 deletions src/renderer/src/stories/BasicTable.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { LitElement, css, html } from "lit";
import { LitElement, css, html, unsafeCSS } from "lit";
import { styleMap } from "lit/directives/style-map.js";
import { header } from "./forms/utils";
import { checkStatus } from "../validation";
import { errorHue, warningHue } from "./globals";
import { emojiFontFamily, errorHue, warningHue } from "./globals";

import * as promises from "../promises";

import "./Button";
import { sortTable } from "./Table";
import tippy from "tippy.js";
import { getIgnore } from "./JSONSchemaForm";

export class BasicTable extends LitElement {
static get styles() {
Expand Down Expand Up @@ -65,6 +66,12 @@ export class BasicTable extends LitElement {
user-select: none;
}

.relative .info {
margin: 0px 5px;
font-size: 80%;
font-family: ${unsafeCSS(emojiFontFamily)};
}

th span {
display: inline-block;
}
Expand Down Expand Up @@ -115,6 +122,7 @@ export class BasicTable extends LitElement {
validateOnChange,
onStatusChange,
onLoaded,
onUpdate,
} = {}) {
super();
this.name = name ?? "data_table";
Expand All @@ -127,6 +135,7 @@ export class BasicTable extends LitElement {
this.ignore = ignore ?? {};

if (validateOnChange) this.validateOnChange = validateOnChange;
if (onUpdate) this.onUpdate = onUpdate;
if (onStatusChange) this.onStatusChange = onStatusChange;
if (onLoaded) this.onLoaded = onLoaded;
}
Expand Down Expand Up @@ -158,9 +167,29 @@ export class BasicTable extends LitElement {
return html`<div class="relative"><span>${header(str)}</span></div>`;
};

#renderHeader = (str, { description }) => {
if (description) return html`<th title="${description}">${this.#renderHeaderContent(str)}</th>`;
return html`<th>${this.#renderHeaderContent(str)}</th>`;
#renderHeader = (prop, { description, title = prop } = {}) => {
const th = document.createElement("th");

const required = this.#itemSchema.required ? this.#itemSchema.required.includes(prop) : false;
const container = document.createElement("div");
container.classList.add("relative");
const span = document.createElement("span");
span.innerHTML = header(title);
if (required) span.setAttribute("required", "");
container.appendChild(span);

// Add Description Tooltip
if (description) {
const span = document.createElement("span");
span.classList.add("info");
span.innerText = "ℹ️";
container.append(span);
tippy(span, { content: `${description[0].toUpperCase() + description.slice(1)}`, allowHTML: true });
}

th.appendChild(container);

return th;
};

#getRowData(row, cols = this.colHeaders) {
Expand All @@ -169,13 +198,12 @@ export class BasicTable extends LitElement {
let value;
if (col === this.keyColumn) {
if (hasRow) value = row;
else return "";
else return;
} else
value =
(hasRow ? this.data[row][col] : undefined) ??
// this.globals[col] ??
this.#itemSchema.properties[col].default ??
"";
this.#itemSchema.properties[col]?.default;
return value;
});
}
Expand Down Expand Up @@ -210,43 +238,52 @@ export class BasicTable extends LitElement {
onStatusChange = () => {};
onLoaded = () => {};

#validateCell = (value, col, parent) => {
#getType = (value, { type, data_type } = {}) => {
let inferred = typeof value;
if (Array.isArray(value)) inferred = "array";
if (value == undefined) inferred = "null";

const original = type || data_type;
let resolved = original;

// Handle based on JSON Schema types
if (type) {
if (resolved === "integer") resolved = "number"; // Map to javascript type
} else if (data_type) {
if (resolved.includes("array")) resolved = "array";
if (resolved.includes("int") || resolved.includes("float")) resolved = "number";
if (resolved.startsWith("bool")) resolved = "boolean";
if (resolved.startsWith("str")) resolved = "string";
}

return {
type: resolved,
original,
inferred,
};
};

#validateCell = (value, col, row, parent) => {
if (!value && !this.validateEmptyCells) return true; // Empty cells are valid
if (!this.validateOnChange) return true;

let result;

const propInfo = this.#itemProps[col] ?? {};
let thisTypeOf = typeof value;
let ogType;
let type = (ogType = propInfo.type || propInfo.data_type);

// Handle based on JSON Schema types
if ("type" in propInfo) {
// Map to javascript type
if (type === "integer") type = "number";

// Convert to json schema type
if (Array.isArray(value)) thisTypeOf = "array";
if (value == undefined) thisTypeOf = "null";
} else if ("data_type" in propInfo) {
if (type.includes("array")) type = "array";
if (type.includes("int") || type.includes("float")) type = "number";
if (type.startsWith("bool")) type = "boolean";
if (type.startsWith("str")) type = "string";
}
let { type, original, inferred } = this.#getType(value, propInfo);

// Check if required
if (!value && "required" in this.#itemSchema.required.includes(col))
if (!value && "required" in this.#itemSchema && this.#itemSchema.required.includes(col))
result = [{ message: `${col} is a required property`, type: "error" }];
// If not required, check matching types for values that are defined
else if (value !== "" && thisTypeOf !== type)
result = [{ message: `${col} is expected to be of type ${ogType}, not ${thisTypeOf}`, type: "error" }];
// If not required, check matching types (if provided) for values that are defined
else if (value !== "" && type && inferred !== type)
result = [{ message: `${col} is expected to be of type ${original}, not ${inferred}`, type: "error" }];
// Otherwise validate using the specified onChange function
else result = this.validateOnChange(col, parent, value, this.#itemProps[col]);
else result = this.validateOnChange([row, col], parent, value, this.#itemProps[col]);

// Will run synchronously if not a promise result
return promises.resolve(result, () => {
return promises.resolve(result, (result) => {
let info = {
title: undefined,
warning: undefined,
Expand Down Expand Up @@ -277,7 +314,7 @@ export class BasicTable extends LitElement {

const results = this.#data.map((v, i) => {
return v.map((vv, j) => {
const info = this.#validateCell(vv, this.colHeaders[j], { ...this.data[rows[i]] }); // Could be a promise or a basic response
const info = this.#validateCell(vv, this.colHeaders[j], i, { ...this.data[rows[i]] }); // Could be a promise or a basic response
return promises.resolve(info, (info) => {
if (info === true) return;
const td = this.shadowRoot.getElementById(`i${i}_j${j}`);
Expand Down Expand Up @@ -352,23 +389,30 @@ export class BasicTable extends LitElement {
}, {})
);

console.log("Got", structuredData);

Object.keys(this.data).forEach((row) => delete this.data[row]); // Delete all previous rows
Object.keys(data).forEach((row) => {
const cols = structuredData[row];
const latest = (this.data[this.keyColumn ? cols[this.keyColumn] : row] = {});
Object.entries(cols).forEach(([key, value]) => (key in this.#itemProps ? (latest[key] = value) : "")); // Only include data from schema
Object.entries(cols).forEach(([key, value]) => {
if (key in this.#itemProps) {
const { type } = this.#getType(value, this.#itemProps[key]);
if (type === "string") value = `${value}`; // Convert to string if necessary
latest[key] = value;
}
}); // Only include data from schema
});

this.onUpdate(null, null, value); // Update the whole table
if (this.onUpdate) this.onUpdate([], data); // Update the whole table
}

// Render Code
render() {
this.#updateRendered();

this.schema = this.schema; // Always update the schema
const entries = this.#itemProps;
for (let key in this.ignore) delete entries[key];
for (let key in this.ignore["*"] ?? {}) delete entries[key];

// Add existing additional properties to the entries variable if necessary
if (this.#itemSchema.additionalProperties) {
Expand All @@ -384,6 +428,10 @@ export class BasicTable extends LitElement {
}, entries);
}

// Ignore any additions in the ignore configuration
for (let key in this.ignore) delete entries[key];
for (let key in this.ignore["*"] ?? {}) delete entries[key];

// Sort Columns by Key Column and Requirement
const keys =
(this.#keys =
Expand Down Expand Up @@ -418,7 +466,9 @@ export class BasicTable extends LitElement {
${data.map(
(row, i) =>
html`<tr>
${row.map((col, j) => html`<td id="i${i}_j${j}"><div>${col}</div></td>`)}
${row.map(
(col, j) => html`<td id="i${i}_j${j}"><div>${JSON.stringify(col)}</div></td>`
)}
</tr>`
)}
</tbody>
Expand Down
Loading
Loading