Skip to content

Commit

Permalink
Merge pull request #762 from NeurodataWithoutBorders/time-alignment-ui
Browse files Browse the repository at this point in the history
Time alignment
  • Loading branch information
CodyCBakerPhD authored May 30, 2024
2 parents d7c5773 + fdee491 commit 06488b6
Show file tree
Hide file tree
Showing 7 changed files with 559 additions and 135 deletions.
17 changes: 9 additions & 8 deletions src/electron/frontend/core/components/Dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,14 +274,15 @@ export class Dashboard extends LitElement {
}
})
.catch((e) => {
const previousId = previous?.info?.id;
if (previousId) {
page.onTransition(previousId); // Revert back to previous page
page.notify(
`<h4 style="margin: 0">Fallback to previous page after error occurred</h4><small>${e}</small>`,
"error"
);
} else reloadPageToHome();
const previousId = previous?.info?.id ?? -1;
this.main.onTransition(previousId); // Revert back to previous page
const hasHTML = /<[^>]*>/.test(e);
page.notify(
hasHTML
? e.message
: `<h4 style="margin: 0">Fallback to previous page after error occurred</h4><small>${e}</small>`,
"error"
);
});
}

Expand Down
31 changes: 31 additions & 0 deletions src/electron/frontend/core/components/JSONSchemaInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,16 @@ export class JSONSchemaInput extends LitElement {
padding: 0;
margin-bottom: 1em;
}
select {
background: url("data:image/svg+xml,<svg height='10px' width='10px' viewBox='0 0 16 16' fill='%23000000' xmlns='http://www.w3.org/2000/svg'><path d='M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z'/></svg>")
no-repeat;
background-position: calc(100% - 0.75rem) center !important;
-moz-appearance: none !important;
-webkit-appearance: none !important;
appearance: none !important;
padding-right: 2rem !important;
}
`;
}

Expand Down Expand Up @@ -1115,6 +1125,27 @@ export class JSONSchemaInput extends LitElement {

// Basic enumeration of properties on a select element
if (schema.enum && schema.enum.length) {
// Use generic selector
if (schema.strict && schema.search !== true) {
return html`
<select
class="guided--input schema-input"
@input=${(ev) => this.#updateData(fullPath, schema.enum[ev.target.value])}
@change=${() => validateOnChange && this.#triggerValidation(name, path)}
>
<option disabled selected value>${schema.placeholder ?? "Select an option"}</option>
${schema.enum
.sort()
.map(
(item, i) =>
html`<option value=${i} ?selected=${this.value === item}>
${schema.enumLabels?.[item] ?? item}
</option>`
)}
</select>
`;
}

const options = schema.enum.map((v) => {
return {
key: v,
Expand Down
3 changes: 2 additions & 1 deletion src/electron/frontend/core/components/pages/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export class Page extends LitElement {
const { subject, session, globalState = this.info.globalState } = info;
const file = `sub-${subject}/sub-${subject}_ses-${session}.nwb`;

const { conversion_output_folder, name, SourceData } = globalState.project;
const { conversion_output_folder, name, SourceData, alignment } = globalState.project;

const sessionResults = globalState.results[subject][session];

Expand All @@ -197,6 +197,7 @@ export class Page extends LitElement {
...conversionOptions, // Any additional conversion options override the defaults

interfaces: globalState.interfaces,
alignment,
},
swalOpts
).catch((error) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,6 @@ export class GuidedMetadataPage extends ManagedPage {

const patternPropsToRetitle = ["Ophys.Fluorescence", "Ophys.DfOverF", "Ophys.SegmentationImages"];

console.log("schema", structuredClone(schema), structuredClone(results));

const ophys = schema.properties.Ophys;
if (ophys) {
drillSchemaProperties(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { JSONSchemaForm } from "../../../JSONSchemaForm.js";
import { InstanceManager } from "../../../InstanceManager.js";
import { ManagedPage } from "./ManagedPage.js";
import { onThrow } from "../../../../errors";
import { merge } from "../../utils";
import { merge, sanitize } from "../../utils";
import preprocessSourceDataSchema from "../../../../../../../schemas/source-data.schema";

import { TimeAlignment } from "./alignment/TimeAlignment.js";

import { createGlobalFormModal } from "../../../forms/GlobalFormModal";
import { header } from "../../../forms/utils";
import { Button } from "../../../Button.js";
Expand All @@ -18,6 +20,8 @@ import { getInfoFromId } from "./utils";
import { Modal } from "../../../Modal";
import Swal from "sweetalert2";

import { baseUrl } from "../../../../server/globals";

const propsToIgnore = {
"*": {
verbose: true,
Expand Down Expand Up @@ -80,7 +84,9 @@ export class GuidedSourceDataPage extends ManagedPage {
heightAuto: false,
backdrop: "rgba(0,0,0, 0.4)",
timerProgressBar: false,
didOpen: () => Swal.showLoading(),
didOpen: () => {
Swal.showLoading();
},
});
};

Expand All @@ -93,22 +99,39 @@ export class GuidedSourceDataPage extends ManagedPage {
const info = this.info.globalState.results[subject][session];

// NOTE: This clears all user-defined results
const result = await run(
`neuroconv/metadata`,
{
source_data: form.resolved, // Use resolved values, including global source data
const result = await fetch(`${baseUrl}/neuroconv/metadata`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
source_data: sanitize(structuredClone(form.resolved)), // Use resolved values, including global source data
interfaces: this.info.globalState.interfaces,
},
{ swal: false }
).catch((e) => {
Swal.close();
stillFireSwal = false;
this.notify(e.message, "error");
throw e;
});
}),
})
.then((res) => res.json())
.catch((error) => {
Swal.close();
stillFireSwal = false;
this.notify(`<b>Critical Error:</b> ${error.message}`, "error", 4000);
throw error;
});

Swal.close();

if (isStorybook) return;

if (result.message) {
const [type, ...splitText] = result.message.split(":");
const text = splitText.length
? splitText.join(":").replaceAll("<", "&lt").replaceAll(">", "&gt")
: result.traceback
? `<small><pre>${result.traceback.trim().split("\n").slice(-2)[0].trim()}</pre></small>`
: "";

const message = `<h4 style="margin: 0;">Request Failed</h4><small>${type}</small><p>${text}</p>`;
this.notify(message, "error");
throw result;
}

const { results: metadata, schema } = result;

// Merge arrays from generated pipeline data
Expand All @@ -128,8 +151,6 @@ export class GuidedSourceDataPage extends ManagedPage {
})
);

Swal.close();

await this.save(undefined, false); // Just save new raw values

return this.to(1);
Expand Down Expand Up @@ -205,9 +226,7 @@ export class GuidedSourceDataPage extends ManagedPage {
}

render() {
this.localState = {
results: structuredClone(this.info.globalState.results ?? {}),
};
this.localState = { results: structuredClone(this.info.globalState.results ?? {}) };

this.forms = this.mapSessions(this.createForm, this.localState.results);

Expand All @@ -223,114 +242,83 @@ export class GuidedSourceDataPage extends ManagedPage {
instances,
controls: [
{
name: "Check Alignment",
name: "View Temporal Alignment",
primary: true,
onClick: async (id) => {
const { globalState } = this.info;

const { subject, session } = getInfoFromId(id);

const souceCopy = structuredClone(globalState.results[subject][session].source_data);

const sessionInfo = {
interfaces: globalState.interfaces,
source_data: merge(globalState.project.SourceData, souceCopy),
};

const results = await run("neuroconv/alignment", sessionInfo, {
title: "Checking Alignment",
message: "Please wait...",
});
this.dismiss();

const header = document.createElement("div");
const h2 = document.createElement("h2");
Object.assign(h2.style, {
marginBottom: "10px",
});
h2.innerText = `Alignment Preview: ${subject}/${session}`;
const warning = document.createElement("small");
warning.innerHTML =
"<b>Warning:</b> This is just a preview. We do not currently have the features implemented to change the alignment of your interfaces.";
header.append(h2, warning);

const modal = new Modal({
header,
});

document.body.append(modal);

const content = document.createElement("div");
Object.assign(content.style, {
display: "flex",
flexDirection: "column",
gap: "20px",
padding: "20px",
});

modal.append(content);

const flatTimes = Object.values(results)
.map((interfaceTimestamps) => {
return [interfaceTimestamps[0], interfaceTimestamps.slice(-1)[0]];
})
.flat()
.filter((timestamp) => !isNaN(timestamp));
Object.assign(header.style, { paddingTop: "10px" });
const h2 = document.createElement("h3");
Object.assign(h2.style, { margin: "0px" });
const small = document.createElement("small");
small.innerText = `${subject}/${session}`;
h2.innerText = `Temporal Alignment`;

const minTime = Math.min(...flatTimes);
const maxTime = Math.max(...flatTimes);
header.append(h2, small);

const normalizeTime = (time) => (time - minTime) / (maxTime - minTime);
const normalizeTimePct = (time) => `${normalizeTime(time) * 100}%`;
const modal = new Modal({ header });

for (let name in results) {
const container = document.createElement("div");
const label = document.createElement("label");
label.innerText = name;
container.append(label);
let alignment;

const data = results[name];
modal.footer = new Button({
label: "Update",
primary: true,
onClick: async () => {
console.log("Submit to backend");

const barContainer = document.createElement("div");
Object.assign(barContainer.style, {
height: "10px",
width: "100%",
marginTop: "5px",
border: "1px solid lightgray",
position: "relative",
});
if (alignment) {
globalState.project.alignment = alignment.results;
this.unsavedUpdates = "conversions";
await this.save();
}

if (data.length) {
const firstTime = data[0];
const lastTime = data[data.length - 1];
const sourceCopy = structuredClone(globalState.results[subject][session].source_data);

label.innerText += ` (${firstTime.toFixed(2)} - ${lastTime.toFixed(2)} sec)`;
const alignmentInfo =
globalState.project.alignment ?? (globalState.project.alignment = {});

const firstTimePct = normalizeTimePct(firstTime);
const lastTimePct = normalizeTimePct(lastTime);
const sessionInfo = {
interfaces: globalState.interfaces,
source_data: merge(globalState.project.SourceData, sourceCopy),
alignment: alignmentInfo,
};

const width = `calc(${lastTimePct} - ${firstTimePct})`;

const bar = document.createElement("div");

Object.assign(bar.style, {
position: "absolute",
const data = await run("neuroconv/alignment", sessionInfo, {
title: "Checking Alignment",
message: "Please wait...",
});

left: firstTimePct,
width: width,
height: "100%",
background: "blue",
const { metadata } = data;
if (Object.keys(metadata).length === 0) {
this.notify(
`<h4 style="margin: 0">Time Alignment Failed</h4><small>Please ensure that all source data is specified.</small>`,
"error"
);
return false;
}

alignment = new TimeAlignment({
data,
interfaces: globalState.interfaces,
results: alignmentInfo,
});

barContainer.append(bar);
} else {
barContainer.style.background =
"repeating-linear-gradient(45deg, lightgray, lightgray 10px, white 10px, white 20px)";
}
modal.innerHTML = "";
modal.append(alignment);

container.append(barContainer);
return true;
},
});

const result = await modal.footer.onClick();
if (!result) return;

content.append(container);
}
document.body.append(modal);

modal.open = true;
},
Expand Down
Loading

0 comments on commit 06488b6

Please sign in to comment.