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

I13 display plot warnings #14

Merged
merged 4 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion src/components/CovariateOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function CovariateOptions({covariates}: Props) {
type: ActionType.SELECT_COVARIATE,
payload: {
name: selectedVariable.name,
levels: selectedVariable.levels,
levels: selectedVariable.levels.filter(l => l === 0 || l),
display: selectedDisplayOption
}
})
Expand Down
62 changes: 40 additions & 22 deletions src/components/LinePlot.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import React, {useContext, useState} from 'react';
import Plot from 'react-plotly.js';
import {RootContext, RootDispatchContext} from "../RootContext";
import {DataSeries} from "../generated";
import {DataSeries, ErrorDetail} from "../generated";
import {dataService} from "../services/dataService";
import {toFilename} from "../services/utils";
import {useDebouncedEffect} from "../hooks/useDebouncedEffect";
import PlotError from "./PlotError";
import PlotWarnings from "./PlotWarnings";
import {Dict} from "../types";

interface Props {
biomarker: string
Expand Down Expand Up @@ -35,6 +38,7 @@ export default function LinePlot({
const dispatch = useContext(RootDispatchContext);

const [seriesData, setSeries] = useState<DataSeries | null>(null);
const [plotError, setPlotError] = useState<ErrorDetail | null>(null);

const len = facetVariables.length
const facetDefinitions: string[] = [];
Expand All @@ -48,6 +52,7 @@ export default function LinePlot({
const scale = state.datasetSettings[state.selectedDataset].scale;
const splineSettings = state.datasetSettings[state.selectedDataset].splineSettings;
useDebouncedEffect(() => {
setPlotError(null);
const fetchData = async () => {
const result = await dataService(state.language, dispatch)
.getDataSeries(state.selectedDataset,
Expand All @@ -58,34 +63,44 @@ export default function LinePlot({
} else {
setSeries(null)
}
if (result && result.errors?.length) {
setPlotError(result.errors[0])
}
}
fetchData();
}, [state.language, dispatch, state.selectedDataset, biomarker, facetDefinition, covariateSettings, scale, splineSettings], 100);

let series: any[] = [];
const warnings: Dict<string[]> = {};

if (seriesData) {
series = seriesData.flatMap((series, index) => ([{
x: series.model.x,
y: series.model.y,
name: series.name,
legendgroup: series.name,
type: "scatter",
mode: "line",
line: {shape: 'spline', width: 2},
showlegend: seriesData.length > 1,
marker: {color: colors[index]}
},
{
x: series.raw.x,
y: series.raw.y,
name: series.name,
legendgroup: series.name,
series = seriesData.flatMap((series, index) => {
const name = series.name || "unknown";
if (series.warnings && series.warnings.length > 0) {
warnings[name] = series.warnings;
}
return [{
x: series.model?.x || [],
y: series.model?.y || [],
name: name,
legendgroup: name,
type: "scatter",
mode: "markers",
mode: "line",
line: {shape: 'spline', width: 2},
showlegend: false,
marker: {color: colors[index], opacity: 0.5}
}]))
marker: {color: colors[index]}
},
{
x: series.raw.x,
y: series.raw.y,
name: name,
legendgroup: name,
type: "scatter",
mode: "markers",
showlegend: seriesData.length > 1,
marker: {color: colors[index], opacity: 0.5}
}]
})
} else {
series = []
}
Expand All @@ -95,7 +110,7 @@ export default function LinePlot({
title += " " + facetDefinition
}

return <Plot
return <div>{series.length > 0 && <Plot
data={series}
layout={{
title: title,
Expand All @@ -114,5 +129,8 @@ export default function LinePlot({
config={{toImageButtonOptions: {filename: toFilename(title)}}}
useResizeHandler={true}
style={{minWidth: "400px", width: "100%", height: "500"}}
/>
/>}{plotError &&
<PlotError title={facetDefinition} error={plotError}/>
} <PlotWarnings warnings={warnings}/>
</div>
}
17 changes: 17 additions & 0 deletions src/components/PlotError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {Alert} from "react-bootstrap";
import React from "react";
import {ErrorDetail} from "../generated";

interface Props {
error: ErrorDetail
title: string
}

export default function PlotError({error, title}: Props) {
const message = error.detail ?? `API returned an error: ${error.error}`;
return <Alert variant={"danger"} className={"rounded-0 border-0 mb-1 ms-4"}>
<p>Facet for <strong>{title}</strong> could not be generated due to the following error:</p>
{message} {error.error === "SESSION_EXPIRED" &&
<a href={"/"}>Re-upload your data to continue.</a>}
</Alert>
}
24 changes: 24 additions & 0 deletions src/components/PlotWarnings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {Alert} from "react-bootstrap";
import React from "react";
import {Dict} from "../types";

interface Props {
warnings: Dict<string[]>
}

export default function PlotWarnings({warnings}: Props) {
const keys = Object.keys(warnings);
if (keys.length === 0) {
return null;
}
return <Alert variant={"warning"}
className={"rounded-0 border-0 mb-1 ms-4"}>
<p>Some traces generated warnings</p>
{keys.map((k, i) => <div key={"w" + i}>{k}:
<ul>{
warnings[k].map(w =>
<li key={w}>{w}</li>)}
</ul>
</div>)}
</Alert>
}
2 changes: 1 addition & 1 deletion src/components/SplineOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default function SplineOptions() {

const settings = state.datasetSettings[state.selectedDataset].splineSettings;

return <Form.Group>
return <Form.Group className={"mb-2 border p-2"}>
<Row className={"mt-2"}>
<Form.Label column sm="6" htmlFor="method">
Method:
Expand Down
14 changes: 7 additions & 7 deletions src/generated.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
* and run ./generate_types.sh to regenerate this file.
*/
export type DataSeries = {
name?: string;
name: string;
model: {
x: number[];
x: (number | string)[];
y: (number | null)[];
};
} | null;
raw: {
x: number[];
x: (number | string)[];
y: (number | null)[];
};
[k: string]: unknown;
warnings: string[] | null;
}[];
export interface DatasetMetadata {
variables: VariableSchema[];
Expand All @@ -23,7 +23,7 @@ export interface DatasetMetadata {
}
export interface VariableSchema {
name: string;
levels: string[];
levels: (string | number | null)[];
}
export type DatasetNames = string[];
export interface ErrorDetail {
Expand All @@ -50,6 +50,6 @@ export interface ResponseSuccess {
export type UploadResult = string;
export interface Variable {
name: string;
levels: string[];
levels: (string | number | null)[];
}
export type Version = string;
11 changes: 11 additions & 0 deletions src/services/apiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,17 @@ export class APIService implements API<ActionType> {
if (e.response && e.response.status === 404) {
this._dispatchError(APIService.createError("Your session may have expired.", "SESSION_EXPIRED"));
}

const error = (e.response && e.response.data);

if (isPorcelainResponse(error)) {
return error
} else {
return {
status: "failure",
errors: [APIService.createError("Could not parse API response. If error persists, please contact support.")]
}
}
};

private _dispatchError = (error: ErrorDetail) => {
Expand Down
2 changes: 1 addition & 1 deletion src/services/dataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export class DataService {

return await this._api
.ignoreSuccess()
.withError(ActionType.ERROR_ADDED)
.ignoreErrors()
.get<DataSeries>("/dataset/" + selectedDataset + "/trace/" + biomarker + "/" + queryString)
}
}
Expand Down
15 changes: 8 additions & 7 deletions test/components/ExploreDataset.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ import {
mockSeriesData,
mockSuccess
} from "../mocks";
import {render, screen} from "@testing-library/react";
import {render, screen, waitFor} from "@testing-library/react";
import {RootContext} from "../../src/RootContext";
import {ExploreDataset} from "../../src/components/ExploreDataset";
import {act} from "react";

// mock the react-plotly.js library
jest.mock("react-plotly.js", () => ({
Expand All @@ -37,10 +36,11 @@ describe("<ExploreDataset/>", () => {
biomarkers: ["ab", "ba"]
})
});
await act(() => render(<RootContext.Provider value={state}>
render(<RootContext.Provider value={state}>
<ExploreDataset/>
</RootContext.Provider>));
</RootContext.Provider>);

await waitFor(() => expect(screen.getAllByText("PLOT").length).toBe(2));
expect(screen.getAllByTestId("sidebar").length).toBe(1);
expect(screen.getAllByText("PLOT").length).toBe(2);
});
Expand Down Expand Up @@ -74,12 +74,13 @@ describe("<ExploreDataset/>", () => {
})
}
});
await act(() => render(<RootContext.Provider value={state}>
render(<RootContext.Provider value={state}>
<ExploreDataset/>
</RootContext.Provider>));
</RootContext.Provider>);

await waitFor(() => expect(screen.getAllByText("PLOT").length).toBe(4));
expect(screen.getAllByTestId("sidebar").length).toBe(1);
expect(screen.getAllByText("PLOTPLOT").length).toBe(2);
expect(screen.getAllByText("PLOT").length).toBe(4);
});
});
});
Loading