Skip to content

Commit

Permalink
Merge pull request #404 from AutomatingSciencePipeline/376-format-dat…
Browse files Browse the repository at this point in the history
…a-for-boxplot-and-violin-03

376 format data for boxplot and violin
  • Loading branch information
rhit-windsors authored Jan 13, 2025
2 parents 7340ba7 + 677e573 commit 14e8a46
Showing 1 changed file with 128 additions and 13 deletions.
141 changes: 128 additions & 13 deletions apps/frontend/app/components/flows/ViewExperiment/Chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ const ChartModal: React.FC<ChartModalProps> = ({ onClose, project }) => {
const [experimentChartData, setExperimentChartData] = useState({ _id: '', experimentId: '', resultContent: '' });
const [loading, setLoading] = useState(true);
const [xAxis, setXAxis] = useState('X');
//const [yAxis, setYAxis] = useState('Y');
const [aggregateMode, setAggregateMode] = useState('sum');
const [headers, setHeaders] = useState<string[]>([]);
const [img, setImg] = useState<string>('');
const [isFullscreen, setIsFullscreen] = useState(false);
const [aggregateData, setAggregateData] = useState(false);

const toggleFullscreen = () => {
setIsFullscreen(!isFullscreen);
Expand All @@ -35,6 +36,8 @@ const ChartModal: React.FC<ChartModalProps> = ({ onClose, project }) => {
const setBoxPlot = () => setChartType('boxplot');
const setViolin = () => setChartType('violin');

const aggregateModes = ['sum', 'count', 'average', 'median', 'mode']

useEffect(() => {
fetch(`/api/download/csv/${project.expId}`).then((response) => response.json()).then((record) => {
setExperimentChartData(record);
Expand Down Expand Up @@ -64,13 +67,8 @@ const ChartModal: React.FC<ChartModalProps> = ({ onClose, project }) => {
const headers = rows[0].split(',') as string[];
//Create a dictionary to store the data
const dataDict = {} as any;
const splitRows = [] as any;

// Iterate through the rows and put them under the correct header
for (let i = 0; i < headers.length; i++) {
dataDict[headers[i]] = [];
}

// Iterate through the rows and put them under the correct header
for (let i = 1; i < rows.length; i++) {
// Split the row by commas when not inside quotes
const row = rows[i].split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);
Expand All @@ -79,9 +77,21 @@ const ChartModal: React.FC<ChartModalProps> = ({ onClose, project }) => {
// If the value is a number, convert it to a number
if (!isNaN(val)) {
row[j] = parseFloat(val);
dataDict[headers[j]].push(row[j]);
}
}
splitRows.push(row);
}

// Initialize dataDict with arrays
for (let i = 0; i < headers.length; i++) {
dataDict[headers[i]] = [];
}

// Iterate through the rows and put them under the correct header
for (let i = 1; i < splitRows.length; i++) {
for (let j = 0; j < splitRows[i].length; j++) {
dataDict[headers[j]].push(splitRows[i][j]);
}
}

//Remove items with empty arrays
Expand All @@ -95,6 +105,70 @@ const ChartModal: React.FC<ChartModalProps> = ({ onClose, project }) => {
const xIndex = headers.indexOf(xAxis);
const xList = headers.includes(xAxis) ? dataDict[xAxis] : dataDict[headers[0]];
const yLists = headers.map((header) => dataDict[header]);

//If we need to aggregate the data, go through and sum the values of duplicates
if (aggregateData && chartType != 'boxplot' && chartType != 'violin') {
const uniqueX = Array.from(new Set(xList));
const uniqueY = [] as any[];
for (let i = 0; i < yLists.length; i++) {
const yList = yLists[i];
const uniqueYList = [] as any[];
for (let j = 0; j < uniqueX.length; j++) {
const x = uniqueX[j];
const indices = xList.reduce((acc, e, i) => (e === x ? acc.concat(i) : acc), []);
if (aggregateMode == 'sum') {
const sum = indices.reduce((acc, e) => acc + yList[e], 0);
uniqueYList.push(sum);
}
else if (aggregateMode == 'count') {
uniqueYList.push(indices.length);
}
else if (aggregateMode == 'average') {
const sum = indices.reduce((acc, e) => acc + yList[e], 0);
uniqueYList.push(sum / indices.length);
}
else if (aggregateMode == 'median') {
const values = indices.map((e) => yList[e]);
values.sort((a, b) => a - b);
const half = Math.floor(values.length / 2);
if (values.length % 2) {
uniqueYList.push(values[half]);
}
else {
uniqueYList.push((values[half - 1] + values[half]) / 2.0);
}
}
else if (aggregateMode == 'mode') {
const values = indices.map((e) => yList[e]);
const mode = values.sort((a, b) =>
values.filter((v) => v === a).length - values.filter((v) => v === b).length
).pop();
uniqueYList.push(mode);
}
}
uniqueY.push(uniqueYList);
}
return { returnHeaders, xList: uniqueX, yLists: uniqueY, xIndex };
};

//These need to be formatted like
// {label: 'x1', data: [[1, 2, 3], [3, 4, 5]]}
if (chartType == 'boxplot' || chartType == 'violin') {
const uniqueX = Array.from(new Set(xList));
const uniqueY = [] as any[];
for (let i = 0; i < yLists.length; i++) {
const yList = yLists[i];
const uniqueYList = [] as any[];
for (let j = 0; j < uniqueX.length; j++) {
const x = uniqueX[j];
const indices = xList.reduce((acc, e, i) => (e === x ? acc.concat(i) : acc), []);
const values = indices.map((e) => yList[e]);
uniqueYList.push(values);
}
uniqueY.push(uniqueYList);
}
return { returnHeaders, xList: uniqueX, yLists: uniqueY, xIndex };
}
return { returnHeaders, xList, yLists, xIndex };
};

Expand Down Expand Up @@ -133,7 +207,6 @@ const ChartModal: React.FC<ChartModalProps> = ({ onClose, project }) => {
backgroundColor: colors
}));


const newChartInstance = new Chart(ctx, {
type: chartType,
data: {
Expand Down Expand Up @@ -166,11 +239,28 @@ const ChartModal: React.FC<ChartModalProps> = ({ onClose, project }) => {
}
}
});

//Set all of the datasets to be unselected
//If it is a pie chart you have to use meta
if (chartType == 'pie') {
var meta = newChartInstance.getDatasetMeta(0);
meta.data.forEach(function (ds) {
(ds as any).hidden = true;
});
}
else {
newChartInstance.data.datasets.forEach((dataset) => {
dataset.hidden = true;
});
}
newChartInstance.update();
console.log(newChartInstance?.data.datasets);

setChartInstance(newChartInstance);

setHeaders(headers);
}
}, [loading, experimentChartData, chartType, xAxis, isFullscreen]);
}, [loading, experimentChartData, chartType, xAxis, isFullscreen, aggregateData, aggregateMode]);

const regenerateCanvas = () => {
setCanvasKey(prevKey => prevKey + 1);
Expand All @@ -189,12 +279,12 @@ const ChartModal: React.FC<ChartModalProps> = ({ onClose, project }) => {
<button onClick={setPieChart} className='inline-flex items-center justify-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 xl:w-full'>
Pie Chart
</button>
{/* <button onClick={setBoxPlot} className='inline-flex items-center justify-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 xl:w-full'>
<button onClick={setBoxPlot} className='inline-flex items-center justify-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 xl:w-full'>
Box Plot
</button>
<button onClick={setViolin} className='inline-flex items-center justify-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 xl:w-full'>
Violin Plot
</button> */}
</button>
</div>
<div
className={isFullscreen ? 'p-4 h-[65vh]' : 'p-4 h-[50vh]'}>
Expand All @@ -210,7 +300,7 @@ const ChartModal: React.FC<ChartModalProps> = ({ onClose, project }) => {
<div className='p-4'>
<p className="font-bold">X-Axis Column:</p>
<select
className="p-2 border rounded-md font-bold"
className="p-2 border rounded-md font-bold w-auto"
onChange={(e) => setXAxis(e.target.value)}
name="xaxis"
defaultValue={headers[0]}
Expand All @@ -223,6 +313,31 @@ const ChartModal: React.FC<ChartModalProps> = ({ onClose, project }) => {
</select>

</div>
<div className='p-4'>
<label className='p-2' htmlFor='aggregate-data-box'>Aggregate data?</label>
<input className='p-2' id='aggregate-data-box' type="checkbox" checked={aggregateData} onChange={() => setAggregateData(!aggregateData)}></input>
{
aggregateData ?
(<div className='p-4'>
<select
id='aggregate-select'
className="p-2 border rounded-md font-bold w-auto"
disabled={!aggregateData}
name="aggregate"
defaultValue='sum'
onChange={(e) => setAggregateMode(e.target.value)}
>
{aggregateModes.map((mode) => (
<option key={mode} value={mode}>
{mode}
</option>
))}
</select>
<label className='p-2' htmlFor='aggregate-select'>Aggregate Mode:</label>
</div>)
: null
}
</div>
<button onClick={downloadImage} className='inline-flex items-center justify-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 xl:w-full'>
Download Image
</button>
Expand Down

0 comments on commit 14e8a46

Please sign in to comment.