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

DIG-1514: An invalid chart in CustomOfflineCharts.js causes the searchpage to whitescreen. #202

Merged
merged 6 commits into from
Jan 14, 2025
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
212 changes: 123 additions & 89 deletions src/views/clinicalGenomic/widgets/dataVisualization.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,36 @@ import Button from '@mui/material/Button';
import { IconEdit, IconX, IconPlus } from '@tabler/icons-react';

// Custom Components and context
import CustomOfflineChart from 'views/summary/CustomOfflineChart';
import CustomOfflineChart, { VALID_CHART_TYPES, VISUALIZATION_LOCAL_STORAGE_KEY } from 'views/summary/CustomOfflineChart';
import { useSearchResultsReaderContext } from '../SearchResultsContext';

// Constants
import { validStackedCharts, DataVisualizationChartInfo } from 'store/constant';
import { HAS_CENSORED_DATA_MARKER } from 'utils/utils';

const DEFAULT_CHART_DEFINITIONS = [
{
data: 'patients_per_program',
chartType: 'bar',
trim: false
},
{
data: 'diagnosis_age_count',
chartType: 'bar',
trim: false
},
{
data: 'treatment_type_count',
chartType: 'bar',
trim: false
},
{
data: 'primary_site_count',
chartType: 'bar',
trim: false
}
];

function DataVisualization() {
// Hooks
const resultsContext = useSearchResultsReaderContext();
Expand Down Expand Up @@ -74,6 +97,17 @@ function DataVisualization() {
return newDataObj;
};

const removeInvalidCharts = (chartDefinitions) => {
const retVal = [];
for (let i = 0; i < chartDefinitions.length; i += 1) {
if (VALID_CHART_TYPES.includes(chartDefinitions[i].chartType) && typeof chartDefinitions[i].trim === 'boolean') {
retVal.push(chartDefinitions[i]);
}
}

return retVal;
};

const dataVis = {
patients_per_program: handleCensoring('patients_per_program', (site, _) => site, true) || {},
diagnosis_age_count: handleCensoring('age_at_diagnosis', (_, age) => age.replace(/ Years$/, '')) || {},
Expand All @@ -86,81 +120,82 @@ function DataVisualization() {
const [edit, setEdit] = useState(false);
const [open, setOpen] = useState(false);

// Top 4 keys from dataVis
const topKeys = Object.keys(dataVis).slice(0, 4);

// LocalStorage
const [dataValue, setDataValue] = useState(
localStorage.getItem('dataVisData') && JSON.parse(localStorage.getItem('dataVisData'))?.[0]
? JSON.parse(localStorage.getItem('dataVisData'))[0]
: 'patients_per_program'
);
const [chartType, setChartType] = useState(
localStorage.getItem('dataVisChartType') && JSON.parse(localStorage.getItem('dataVisChartType'))?.[0]
? JSON.parse(localStorage.getItem('dataVisChartType'))[0]
: 'bar'
);
const [dataVisData, setdataVisData] = useState(
localStorage.getItem('dataVisData') ? JSON.parse(localStorage.getItem('dataVisData')) : topKeys
);
const [dataVisChartType, setDataVisChartType] = useState(
localStorage.getItem('dataVisChartType') ? JSON.parse(localStorage.getItem('dataVisChartType')) : ['bar', 'bar', 'bar', 'bar']
);
const [dataVisTrim, setDataVisTrim] = useState(
localStorage.getItem('dataVisTrim') ? JSON.parse(localStorage.getItem('dataVisTrim')) : [false, false, false, false]
const [chartDefinitions, setChartDefinitions] = useState(
localStorage.getItem(VISUALIZATION_LOCAL_STORAGE_KEY)
? removeInvalidCharts(JSON.parse(localStorage.getItem(VISUALIZATION_LOCAL_STORAGE_KEY)))
: DEFAULT_CHART_DEFINITIONS
);
const [newDataKey, setNewDataKey] = useState('patients_per_program');
const [newChartType, setNewChartType] = useState('bar');

// Validate chart types to remove any that are not supported
useEffect(() => {
const invalidChartIndexes = [];
for (let i = 0; i < chartDefinitions.length; i += 1) {
// If this is not a valid chart type, remove it
if (!VALID_CHART_TYPES.includes(chartDefinitions[i].chartType)) {
invalidChartIndexes.push(i);
}
// If the data has loaded, but does not exist, then we also remove it
else if (!Object.keys(dataVis).includes(chartDefinitions[i].data)) {
invalidChartIndexes.push(i);
}
}

if (invalidChartIndexes.length > 0) {
setChartDefinitions((old) => {
let newChartDefinitions = old.slice();
invalidChartIndexes.forEach((index, numRemoved) => {
const indexToRemove = index - numRemoved;
newChartDefinitions = newChartDefinitions.slice(0, indexToRemove).concat(old.slice(indexToRemove + 1));
});
localStorage.setItem(VISUALIZATION_LOCAL_STORAGE_KEY, JSON.stringify(newChartDefinitions), { expires: 365 });
return newChartDefinitions;
});
}
}, [JSON.stringify(chartDefinitions)]); // eslint-disable-line react-hooks/exhaustive-deps

// Intial localStorage setting if there are none
useEffect(() => {
if (!localStorage.getItem('dataVisData') && !localStorage.getItem('dataVisChartType')) {
const charts = topKeys.map(() => 'bar');
localStorage.setItem('dataVisChartType', JSON.stringify(charts), { expires: 365 });
localStorage.setItem('dataVisData', JSON.stringify(topKeys), { expires: 365 });
localStorage.setItem('dataVisTrim', JSON.stringify([false, false, false, false]), { expires: 365 });
if (!localStorage.getItem(VISUALIZATION_LOCAL_STORAGE_KEY)) {
localStorage.setItem(VISUALIZATION_LOCAL_STORAGE_KEY, JSON.stringify(chartDefinitions), { expires: 365 });
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps

const handleToggleDialog = () => {
setOpen((prevOpen) => !prevOpen);
};

function setDataVisChartTypeSingle(index, newVal) {
const newDataVisChartType = dataVisChartType.slice();
newDataVisChartType[index] = newVal;
setDataVisChartType(newDataVisChartType);
localStorage.setItem('dataVisChartType', JSON.stringify(newDataVisChartType), { expires: 365 });
}

function setDataVisDataTypeSingle(index, newVal) {
const newDataVisData = dataVisData.slice();
newDataVisData[index] = newVal;
setdataVisData(newDataVisData);
localStorage.setItem('dataVisData', JSON.stringify(newDataVisData), { expires: 365 });
function setDataVisEntry(index, key, newVal) {
setChartDefinitions((old) => {
const newChartDefinitions = old.slice();
newChartDefinitions[index][key] = newVal;
localStorage.setItem(VISUALIZATION_LOCAL_STORAGE_KEY, JSON.stringify(newChartDefinitions), { expires: 365 });
return newChartDefinitions;
});
}

function removeChart(index) {
const newDataVisChartType = dataVisChartType.slice(0, index).concat(dataVisChartType.slice(index + 1));
const newdataVisData = dataVisData.slice(0, index).concat(dataVisData.slice(index + 1));
const newDataVisTrim = dataVisTrim.slice(0, index).concat(dataVisTrim.slice(index + 1));
setDataVisChartType(newDataVisChartType);
setdataVisData(newdataVisData);
setDataVisTrim(newDataVisTrim);
localStorage.setItem('dataVisData', JSON.stringify(newdataVisData), { expires: 365 });
localStorage.setItem('dataVisChartType', JSON.stringify(newDataVisChartType), { expires: 365 });
localStorage.setItem('dataVisTrim', JSON.stringify(newDataVisTrim), { expires: 365 });
setChartDefinitions((old) => {
const newChartDefinitions = old.slice(0, index).concat(old.slice(index + 1));
localStorage.setItem(VISUALIZATION_LOCAL_STORAGE_KEY, JSON.stringify(newChartDefinitions), { expires: 365 });
return newChartDefinitions;
});
}

function AddChart(data, chartType) {
setOpen(false);
const newdataVisData = [...dataVisData, data];
const newDataVisChartType = [...dataVisChartType, validStackedCharts.includes(data) ? 'bar' : chartType];
const newDataVisTrim = [...dataVisTrim, false];
setDataVisChartType(newDataVisChartType);
setdataVisData(newdataVisData);
setDataVisTrim(newDataVisTrim);
localStorage.setItem('dataVisChartType', JSON.stringify(newDataVisChartType), { expires: 365 });
localStorage.setItem('dataVisTrim', JSON.stringify(newDataVisTrim), { expires: 365 });
localStorage.setItem('dataVisData', JSON.stringify(newdataVisData), { expires: 365 });
setChartDefinitions((old) => {
const newDefs = old.slice();
newDefs.push({
data,
chartType: validStackedCharts.includes(data) ? 'bar' : chartType,
trim: false
});
localStorage.setItem(VISUALIZATION_LOCAL_STORAGE_KEY, JSON.stringify(newDefs), { expires: 365 });
return newDefs;
});
}
/* eslint-disable jsx-a11y/no-onchange */
function returnChartDialog() {
Expand All @@ -172,25 +207,30 @@ function DataVisualization() {
<form>
<label htmlFor="types" style={{ paddingRight: '1em' }}>
Data: &nbsp;
<select value={dataValue} name="types" id="types" onChange={(event) => setDataValue(event.target.value)}>
<select value={newDataKey} name="types" id="types" onChange={(event) => setNewDataKey(event.target.value)}>
{Object.keys(dataVis).map((key) => (
<option key={key} value={key}>
{DataVisualizationChartInfo[key].title}
</option>
))}
</select>
</label>
{validStackedCharts.includes(dataValue) ? (
{validStackedCharts.includes(newDataKey) ? (
<label htmlFor="types">
Chart Types: &nbsp;
<select value="bar" name="types" id="types" onChange={(event) => setChartType(event.target.value)}>
<select value="bar" name="types" id="types" onChange={(event) => setNewChartType(event.target.value)}>
<option value="bar">Stacked Bar</option>
</select>
</label>
) : (
<label htmlFor="types">
Chart Types: &nbsp;
<select value={chartType} name="types" id="types" onChange={(event) => setChartType(event.target.value)}>
<select
value={newChartType}
name="types"
id="types"
onChange={(event) => setNewChartType(event.target.value)}
>
<option value="bar">Bar</option>
<option value="line">Line</option>
<option value="column">Column</option>
Expand All @@ -202,39 +242,13 @@ function DataVisualization() {
</form>
<DialogActions>
<Button onClick={handleToggleDialog}>Cancel</Button>
<Button onClick={() => AddChart(dataValue, chartType || 'bar')}>Confirm</Button>
<Button onClick={() => AddChart(newDataKey, newChartType || 'bar')}>Confirm</Button>
</DialogActions>
</DialogContent>
</Dialog>
);
}

function returndataVisData() {
const data = dataVisData.map((item, index) => (
<Grid item xs={12} sm={12} md={6} lg={3} xl={3} key={item + index}>
<CustomOfflineChart
dataObject=""
dataVis={dataVis}
data={item}
index={index}
chartType={dataVisChartType[index]}
height="400px; auto"
dropDown
onRemoveChart={() => removeChart(index)}
edit={edit}
orderByFrequency={item !== 'diagnosis_age_count'}
orderAlphabetically={item === 'diagnosis_age_count'}
trimByDefault={dataVisTrim[index]}
onChangeDataVisChartType={(newType) => setDataVisChartTypeSingle(index, newType)}
onChangeDataVisData={(newData) => setDataVisDataTypeSingle(index, newData)}
loading={dataVis[item] === undefined}
/>
</Grid>
));

return data;
}

return (
<Box
mr={1}
Expand Down Expand Up @@ -267,7 +281,27 @@ function DataVisualization() {
Data Visualization
</Typography>
<Grid container spacing={1} alignItems="center" justifyContent="center">
{returndataVisData()}
{chartDefinitions.map((item, index) => (
<Grid item xs={12} sm={12} md={6} lg={3} xl={3} key={JSON.stringify(item)}>
<CustomOfflineChart
dataObject=""
dataVis={dataVis}
data={item.data}
index={index}
chartType={item.chartType}
height="400px; auto"
dropDown
onRemoveChart={() => removeChart(index)}
edit={edit}
orderByFrequency={item.data !== 'diagnosis_age_count'}
orderAlphabetically={item.data === 'diagnosis_age_count'}
trimByDefault={item.trim}
onChangeDataVisChartType={(newType) => setDataVisEntry(index, 'chartType', newType)}
onChangeDataVisData={(newData) => setDataVisEntry(index, 'data', newData)}
loading={dataVis[item.data] === undefined}
/>
</Grid>
))}
</Grid>
</Grid>
{edit && (
Expand Down
36 changes: 14 additions & 22 deletions src/views/summary/CustomOfflineChart.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ import config from 'config';

window.Highcharts = Highcharts;

// Used to ensure that we are not passed an invalid chart type
export const VALID_CHART_TYPES = ['bar', 'line', 'column', 'scatter', 'pie'];
// Defined here in order to prevent a circular dependancy
export const VISUALIZATION_LOCAL_STORAGE_KEY = 'chartDefinitions';

/*
* Component for offline chart
* @param {string} chartType
Expand Down Expand Up @@ -379,6 +384,7 @@ function CustomOfflineChart({
dataObject,
grayscale,
height,
index,
orderAlphabetically,
orderByFrequency,
theme.palette.grey,
Expand All @@ -387,25 +393,11 @@ function CustomOfflineChart({
theme.palette.tertiary
]);

function setLocalStorageDataVisChart(event) {
function setLocalStorageDataVis(event, key) {
// Set LocalStorage for Data Visualization Chart Type
const dataVisChart = JSON.parse(localStorage.getItem('dataVisChartType'));
dataVisChart[index] = event.target.value;
localStorage.setItem('dataVisChartType', JSON.stringify(dataVisChart), { expires: 365 });
}

function setLocalStorageDataVisData(event) {
// Set LocalStorage for Data Visualization Data
const dataVisData = JSON.parse(localStorage.getItem('dataVisData'));
dataVisData[index] = event.target.value;
localStorage.setItem('dataVisData', JSON.stringify(dataVisData), { expires: 365 });
}

function setLocalStorageDataVisTrim(value) {
// Set LocalStorage for Data Visualization Trim status
const dataVisTrim = JSON.parse(localStorage.getItem('dataVisTrim'));
dataVisTrim[index] = value;
localStorage.setItem('dataVisTrim', JSON.stringify(dataVisTrim), { expires: 365 });
const dataVisChart = JSON.parse(localStorage.getItem(VISUALIZATION_LOCAL_STORAGE_KEY));
dataVisChart[index][key] = event.target.value;
localStorage.setItem(VISUALIZATION_LOCAL_STORAGE_KEY, JSON.stringify(dataVisChart), { expires: 365 });
}

/* eslint-disable jsx-a11y/no-onchange */
Expand Down Expand Up @@ -459,7 +451,7 @@ function CustomOfflineChart({
onChange={(event) => {
setChartData(event.target.value);
onChangeDataVisData(event.target.value);
setLocalStorageDataVisData(event);
setLocalStorageDataVis(event, 'data');
}}
>
{Object.keys(dataVis).map((key) => (
Expand All @@ -481,7 +473,7 @@ function CustomOfflineChart({
onChange={(event) => {
setChart(event.target.value);
onChangeDataVisChartType(event.target.value);
setLocalStorageDataVisChart(event);
setLocalStorageDataVis(event, 'chartType');
}}
>
<option value="bar">Stacked Bar</option>
Expand All @@ -497,7 +489,7 @@ function CustomOfflineChart({
onChange={(event) => {
setChart(event.target.value);
onChangeDataVisChartType(event.target.value);
setLocalStorageDataVisChart(event);
setLocalStorageDataVis(event, 'chartType');
}}
>
<option value="bar">Bar</option>
Expand All @@ -514,7 +506,7 @@ function CustomOfflineChart({
type="checkbox"
id="trim"
onChange={() => {
setLocalStorageDataVisTrim(!trim);
setLocalStorageDataVis(!trim, 'trim');
setTrim((old) => !old);
}}
checked={trim}
Expand Down
Loading