Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin' into 349-permutation-logic
Browse files Browse the repository at this point in the history
  • Loading branch information
bveal52 committed Dec 16, 2024
2 parents 4b1d4ab + c50a30d commit ee5b8eb
Show file tree
Hide file tree
Showing 8 changed files with 424 additions and 58 deletions.
227 changes: 227 additions & 0 deletions apps/frontend/app/components/flows/ViewExperiment/Chart.tsx
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}&apos;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 apps/frontend/app/components/flows/ViewExperiment/ChartModal.tsx
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;
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import { ChevronRightIcon } from '@heroicons/react/24/solid';
import { useEffect, useState } from 'react';
import { ExperimentData } from '../../../../lib/db_types';
import { MdEdit, MdPadding } from 'react-icons/md';
import { Timestamp } from 'mongodb';
import { MdEdit } from 'react-icons/md';
import Chart from './Chart';
import { addShareLink, unfollowExperiment, updateExperimentNameById } from '../../../../lib/mongodb_funcs';
import toast from 'react-hot-toast';
import { useSession } from 'next-auth/react';
Expand Down Expand Up @@ -41,6 +41,7 @@ export const ExperimentListing = ({ projectData: projectData, onCopyExperiment,
const [originalProjectName, setOriginalProjectName] = useState(projectData.name); // State to store the original project name

const [isDeleteModalOpen, setDeleteModalOpen] = useState(false);
const [showGraphModal, setShowGraphModal] = useState(false);

const handleEdit = () => {
// Enable editing and set the edited project name to the current project name
Expand Down Expand Up @@ -97,6 +98,15 @@ export const ExperimentListing = ({ projectData: projectData, onCopyExperiment,
setDeleteModalOpen(false);
};

const openGraphModal = () => {
setShowGraphModal(true);
};

const closeGraphModal = () => {
setShowGraphModal(false);
}


return (
<div className='flex items-center justify-between space-x-4'>
<div className='min-w-0 space-y-3'>
Expand Down Expand Up @@ -247,33 +257,41 @@ export const ExperimentListing = ({ projectData: projectData, onCopyExperiment,
</button> :
<button type="button"
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'
onClick={() => {
toast.promise(unfollowExperiment(project.expId, session?.user?.id!), {
success: 'Unfollowed experiment', error: 'Failed to unfollow experiment',
loading: "Unfollowing experiment..."
});
}}>
onClick={() => {
toast.promise(unfollowExperiment(project.expId, session?.user?.id!), {
success: 'Unfollowed experiment', error: 'Failed to unfollow experiment',
loading: "Unfollowing experiment..."
});
}}>
Unfollow Experiment
</button>
}
{
project.creator == session?.user?.id! ?
<button
type="button"
{project.finished ?
<button type="button"
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'
onClick={
async () => {
//Get the link
const link = await addShareLink(project.expId);
//Copy the link to the clipboard
navigator.clipboard.writeText(`${window.location.origin}/share?link=${link}`);
toast.success('Link copied to clipboard!', { duration: 1500 });
}
}
onClick={openGraphModal}
>
Share Experiment
See Graph
</button> : null
}
{
project.creator == session?.user?.id! ?
<button
type="button"
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'
onClick={
async () => {
//Get the link
const link = await addShareLink(project.expId);
//Copy the link to the clipboard
navigator.clipboard.writeText(`${window.location.origin}/share?link=${link}`);
toast.success('Link copied to clipboard!', { duration: 1500 });
}
}
>
Share Experiment
</button> : null
}
</div>
<div className='sm:hidden'>
<ChevronRightIcon
Expand Down Expand Up @@ -335,6 +353,11 @@ export const ExperimentListing = ({ projectData: projectData, onCopyExperiment,
</p> :
null
}
{
(showGraphModal && project.finished) && (
<Chart onClose={closeGraphModal} project={project} />
)
}
</div>
</div>
);
Expand Down
Loading

0 comments on commit ee5b8eb

Please sign in to comment.