-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'origin' into 349-permutation-logic
- Loading branch information
Showing
8 changed files
with
424 additions
and
58 deletions.
There are no files selected for viewing
227 changes: 227 additions & 0 deletions
227
apps/frontend/app/components/flows/ViewExperiment/Chart.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,227 @@ | ||
import React, { useState, useEffect } from 'react'; | ||
import { Chart, registerables, ChartTypeRegistry } from 'chart.js'; | ||
import { BoxPlotController, BoxAndWiskers, ViolinController, Violin } from '@sgratzl/chartjs-chart-boxplot'; | ||
import 'tailwindcss/tailwind.css'; | ||
import { ExperimentData } from '../../../../lib/db_types'; | ||
import GraphModal from './ChartModal'; | ||
|
||
Chart.register(...registerables); | ||
Chart.register(BoxPlotController, BoxAndWiskers, ViolinController, Violin); | ||
|
||
interface ChartModalProps { | ||
onClose: () => void; | ||
project: ExperimentData; | ||
} | ||
|
||
const ChartModal: React.FC<ChartModalProps> = ({ onClose, project }) => { | ||
const [chartInstance, setChartInstance] = useState<Chart | null>(null); | ||
const [canvasKey, setCanvasKey] = useState(0); | ||
const [chartType, setChartType] = useState<keyof ChartTypeRegistry>('line'); | ||
const [experimentChartData, setExperimentChartData] = useState({ _id: '', experimentId: '', resultContent: '' }); | ||
const [loading, setLoading] = useState(true); | ||
const [xAxis, setXAxis] = useState('X'); | ||
//const [yAxis, setYAxis] = useState('Y'); | ||
const [headers, setHeaders] = useState<string[]>([]); | ||
const [img, setImg] = useState<string>(''); | ||
const [isFullscreen, setIsFullscreen] = useState(false); | ||
|
||
const toggleFullscreen = () => { | ||
setIsFullscreen(!isFullscreen); | ||
}; | ||
|
||
const setLineChart = () => setChartType('line'); | ||
const setBarChart = () => setChartType('bar'); | ||
const setPieChart = () => setChartType('pie'); | ||
const setBoxPlot = () => setChartType('boxplot'); | ||
const setViolin = () => setChartType('violin'); | ||
|
||
useEffect(() => { | ||
fetch(`/api/download/csv/${project.expId}`).then((response) => response.json()).then((record) => { | ||
setExperimentChartData(record); | ||
setLoading(false); | ||
}).catch((response) => { | ||
console.warn('Error getting experiment results', response.status); | ||
response.json().then((json: any) => { | ||
console.warn(json?.response ?? json); | ||
const message = json?.response; | ||
if (message) { | ||
alert(`Error getting experiment results: ${message}`); | ||
} | ||
}); | ||
} | ||
); | ||
}, [project.expId]); | ||
|
||
const downloadImage = () => { | ||
const a = document.createElement('a'); | ||
a.href = img; | ||
a.download = `${project.name}.png`; | ||
a.click(); | ||
}; | ||
|
||
const parseCSV = (data) => { | ||
const rows = data.trim().split('\n'); | ||
const headers = rows[0].split(','); | ||
|
||
const xList = [] as any[]; | ||
const yLists = [] as any[]; | ||
|
||
var xIndex = 0; | ||
for (let i = 0; i < headers.length; i++) { | ||
yLists.push([]); | ||
if (headers[i] === xAxis) { | ||
xIndex = i; | ||
} | ||
} | ||
|
||
yLists.splice(xIndex, 1); | ||
|
||
for (let i = 1; i < rows.length; i++) { | ||
const cols = rows[i].split(','); | ||
xList.push(cols[xIndex]); | ||
for (let j = 0; j < cols.length; j++) { | ||
if (j < xIndex) { | ||
yLists[j].push(cols[j]); | ||
} | ||
else if (j > xIndex) { | ||
yLists[j - 1].push(cols[j]); | ||
} | ||
} | ||
} | ||
|
||
return { headers, xList, yLists, xIndex }; | ||
}; | ||
|
||
const generateColors = (numColors) => { | ||
const colors = [] as string[]; | ||
for (let i = 0; i < numColors; i++) { | ||
const color = `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 0.2)`; | ||
colors.push(color); | ||
} | ||
return colors; | ||
}; | ||
|
||
useEffect(() => { | ||
if (!loading && experimentChartData.resultContent) { | ||
const { headers, xList, yLists, xIndex } = parseCSV(experimentChartData.resultContent); | ||
const colors = generateColors(xList.length); | ||
const ctx = document.getElementById('myChart') as HTMLCanvasElement; | ||
if (chartInstance) { | ||
chartInstance.destroy(); | ||
} | ||
|
||
const totalLength = headers.length; | ||
const newHeaders = [] as any[]; | ||
for (let i = 0; i < totalLength; i++) { | ||
if (i != xIndex) { | ||
newHeaders.push(headers[i]); | ||
} | ||
} | ||
|
||
const datasetsObj = newHeaders.map((header, i) => ({ | ||
label: header, | ||
data: yLists[i], | ||
borderColor: colors, | ||
backgroundColor: colors | ||
})); | ||
|
||
|
||
const newChartInstance = new Chart(ctx, { | ||
type: chartType, | ||
data: { | ||
labels: xList, | ||
datasets: datasetsObj | ||
}, | ||
options: { | ||
responsive: true, | ||
maintainAspectRatio: false, | ||
scales: { | ||
x: { | ||
display: true, | ||
title: { | ||
display: true, | ||
text: 'X Axis' | ||
} | ||
}, | ||
y: { | ||
display: true, | ||
title: { | ||
display: true, | ||
text: 'Y Axis' | ||
} | ||
} | ||
}, | ||
animation: { | ||
onComplete: function () { | ||
setImg(newChartInstance.toBase64Image()); | ||
} | ||
} | ||
} | ||
}); | ||
setChartInstance(newChartInstance); | ||
|
||
setHeaders(headers); | ||
} | ||
}, [loading, experimentChartData, chartType, xAxis, isFullscreen]); | ||
|
||
const regenerateCanvas = () => { | ||
setCanvasKey(prevKey => prevKey + 1); | ||
}; | ||
|
||
|
||
return ( | ||
<GraphModal onClose={() => { onClose(); regenerateCanvas(); }} fullScreen={isFullscreen} toggleFullscreen={toggleFullscreen}> | ||
<div className='container flex items-center justify-between space-x-3'> | ||
<button onClick={setBarChart} 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'> | ||
Bar Chart | ||
</button> | ||
<button onClick={setLineChart} 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'> | ||
Line Chart | ||
</button> | ||
<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'> | ||
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> */} | ||
</div> | ||
<div | ||
className={isFullscreen ? 'p-4 h-[65vh]' : 'p-4 h-[50vh]'}> | ||
<h2 className='text-xl font-bold mb-4'>{project.name}'s Chart</h2> | ||
{loading ? ( | ||
<p>Loading data...</p> | ||
) : ( | ||
<div className='h-full' key={canvasKey}> | ||
<canvas id='myChart'></canvas> | ||
</div> | ||
)} | ||
</div> | ||
<div className='p-4'> | ||
<p className="font-bold">X-Axis Column:</p> | ||
<fieldset> | ||
{headers.map((header) => ( | ||
<div key={header} className="p-1"> | ||
<input | ||
type="radio" | ||
id={header} | ||
onChange={() => setXAxis(header)} | ||
name="xaxis" | ||
value={header} | ||
|
||
/> | ||
<label htmlFor={header} className="font-bold pl-2">{header}</label> | ||
</div> | ||
))} | ||
</fieldset> | ||
</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> | ||
</GraphModal> | ||
); | ||
}; | ||
|
||
export default ChartModal; |
48 changes: 48 additions & 0 deletions
48
apps/frontend/app/components/flows/ViewExperiment/ChartModal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import React from 'react'; | ||
import ReactDOM from 'react-dom'; | ||
import { ArrowsPointingOutIcon, ArrowsPointingInIcon, XMarkIcon } from '@heroicons/react/24/outline' | ||
|
||
interface ModalProps { | ||
onClose?: () => void; | ||
fullScreen?: boolean; | ||
toggleFullscreen?: () => void; | ||
children: React.ReactNode; | ||
} | ||
|
||
const GraphModal: React.FC<ModalProps> = ({ onClose, fullScreen, toggleFullscreen, children }) => { | ||
return ReactDOM.createPortal( | ||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"> | ||
<div | ||
className={fullScreen ? "bg-white rounded-lg shadow-lg w-screen h-screen" : "bg-white rounded-lg shadow-lg w-11/12 md:w-1/2 lg:w-1/3"}> | ||
<div className="flex justify-end p-2"> | ||
<button | ||
onClick={toggleFullscreen} | ||
className="flex items-center justify-center p-2 text-gray-500 transition duration-200 ease-in-out bg-gray-200 rounded-full shadow hover:bg-gray-300 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 mx-2" | ||
aria-label="Toggle Fullscreen" | ||
> | ||
{fullScreen ? | ||
( | ||
<ArrowsPointingInIcon className="w-5 h-5" /> | ||
) : ( | ||
<ArrowsPointingOutIcon className="w-5 h-5" /> | ||
) | ||
} | ||
</button> | ||
<button | ||
onClick={onClose} | ||
className="flex items-center justify-center p-2 text-gray-500 transition duration-200 ease-in-out bg-gray-200 rounded-full shadow hover:bg-gray-300 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2" | ||
aria-label="Close" | ||
> | ||
<XMarkIcon className="w-5 h-5" /> | ||
</button> | ||
</div> | ||
<div className="p-4"> | ||
{children} | ||
</div> | ||
</div> | ||
</div>, | ||
document.body | ||
); | ||
}; | ||
|
||
export default GraphModal; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.