Skip to content

Commit

Permalink
Merge pull request #24 from ETH-PEACH-Lab/interactive-graph
Browse files Browse the repository at this point in the history
Interactive graph
  • Loading branch information
mchami02 authored Aug 28, 2024
2 parents 5954b23 + b50e35a commit b08c6ba
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 17 deletions.
43 changes: 39 additions & 4 deletions src/Flowchart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ interface ClusterLink {
}


interface Props { }
interface Props {
handleClusterClick?: ((cluster: string, cls: string) => void);
handleClassClick?: ((cls: string) => void);
selectedClusters?: string[];
}

interface State {
selectedCells: NotebookCellWithID[];
Expand Down Expand Up @@ -76,6 +80,9 @@ class Flowchart extends Component<Props, State> {
if (prevState.selectedCells !== this.state.selectedCells) {
this.drawChart();
}
if(prevProps.selectedClusters !== this.props.selectedClusters) {
this.drawChart();
}
}

updateSelectedCells = (newSelectedCells: NotebookCellWithID[], allNotebooks: boolean) => {
Expand Down Expand Up @@ -125,7 +132,14 @@ class Flowchart extends Component<Props, State> {
.attr('height', nodeHeight)
.attr('rx', 10)
.attr('ry', 10)
.attr('fill', d => colorScheme[d.id] || '#69b3a2'); // Replace with your color logic
.attr('fill', d => colorScheme[d.id] || '#69b3a2')
.on('click', d => {
const cls = d.target.__data__.id;
if (this.props.handleClassClick) {
this.props.handleClassClick(cls);
}
}
);

svg.selectAll('text')
.data(nodes)
Expand Down Expand Up @@ -251,7 +265,17 @@ class Flowchart extends Component<Props, State> {
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', circleRadius)
.attr('fill', d => colorScheme[d.class] || '#69b3a2'); // Use color scheme based on class
.attr('stroke', d => this.props.selectedClusters?.includes(d.cluster) ? 'black' : 'none')
.attr('stroke-width', d => this.props.selectedClusters?.includes(d.cluster) ? '2px' : '0px')
.attr('fill', d => colorScheme[d.class] || '#69b3a2')
.on('click', d => {
const clstr = d.target.__data__.cluster;
const cls = d.target.__data__.class;
if (this.props.handleClusterClick) {
this.props.handleClusterClick(clstr, cls);
}
}
);

// Draw text labels (cluster names)
svg.selectAll('text')
Expand All @@ -264,6 +288,7 @@ class Flowchart extends Component<Props, State> {
.attr('dominant-baseline', 'middle')
.attr('fill', '#000') // Set text color to black
.style('font-size', '10px') // Set font size to smaller
.style('font-weight', d => this.props.selectedClusters?.includes(d.cluster) ? 'bold' : 'normal') // Make text bold if in selectedClusters
.each(function (d) {
const words = d.cluster.split(' '); // Split cluster name into words
let tspan = d3.select(this).append('tspan')
Expand Down Expand Up @@ -375,6 +400,9 @@ class Flowchart extends Component<Props, State> {

export class FlowchartWidget extends ReactWidget {
graph: React.RefObject<Flowchart>;
handleClusterClick: ((cluster: string, cls: string) => void) | undefined;
handleClassClick: ((cls: string) => void) | undefined;
selectedClusters: string[] | undefined;

constructor() {
super();
Expand All @@ -386,10 +414,17 @@ export class FlowchartWidget extends ReactWidget {
this.graph.current?.updateSelectedCells(selectedCells, allNotebooks);
}

public addProps(handleClusterClick: (cluster: string, cls: string) => void, handleClassClick: (cls: string) => void, selectedClusters: string[]): void {
this.handleClusterClick = handleClusterClick;
this.handleClassClick = handleClassClick;
this.selectedClusters = selectedClusters;
this.update();
}

render(): JSX.Element {
return (
<div className="flowchart-widget">
<Flowchart ref={this.graph} />
<Flowchart ref={this.graph} handleClassClick={this.handleClassClick} handleClusterClick={this.handleClusterClick} selectedClusters={this.selectedClusters}/>
</div>
);
}
Expand Down
51 changes: 40 additions & 11 deletions src/VizComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import colorScheme from './colorScheme';
import '../style/VizComponent.css';
import CodeCell from './CodeCell';
Expand Down Expand Up @@ -34,13 +34,22 @@ interface GroupedCellsProps {
className: string;
cells: NotebookCellWithID[];
onSelectNotebook: (notebookIds: [number]) => void;
selectedClusters: string[];
setSelectedClusters: (clusters: string[]) => void;
scrolledClass: string;
}

const GroupedCells: React.FC<GroupedCellsProps> = ({ className, cells, onSelectNotebook }) => {
const GroupedCells: React.FC<GroupedCellsProps> = ({ className, cells, onSelectNotebook, selectedClusters, setSelectedClusters, scrolledClass }) => {
const [isOpen, setIsOpen] = useState(true);
const [openClusters, setOpenClusters] = useState<string[]>([]); // Manage multiple open clusters
const containerRef = useRef<HTMLDivElement>(null); // Add ref here

const toggleOpen = () => setIsOpen(!isOpen);

useEffect(() => {
if(scrolledClass === className && containerRef.current) {
containerRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [scrolledClass]);

// Group cells by their cluster
const clusters = cells.reduce((acc, cell) => {
Expand All @@ -65,15 +74,18 @@ const GroupedCells: React.FC<GroupedCellsProps> = ({ className, cells, onSelectN
};

const handleClusterClick = (clusterName: string) => {
setOpenClusters((prev) =>
prev.includes(clusterName) ? prev.filter((name) => name !== clusterName) : [...prev, clusterName]
);
if(selectedClusters.includes(clusterName)) {
setSelectedClusters(selectedClusters.filter(cluster => cluster !== clusterName));
}
else {
setSelectedClusters([...selectedClusters, clusterName]);
}
};

const handleIdentifierClick = (clusterIdentifier: string) => {
const cluster = clusterIdentifiers.find(ci => ci.identifier === clusterIdentifier);
if (cluster) {
setOpenClusters([cluster.name as string]);
setSelectedClusters([cluster.name as string]);
}
};

Expand All @@ -84,7 +96,11 @@ const GroupedCells: React.FC<GroupedCellsProps> = ({ className, cells, onSelectN
}));

return (
<div className="group-container" style={{ borderColor: colorScheme[className] }}>
<div
className="group-container"
style={{ borderColor: colorScheme[className] }}
ref={containerRef}
>
<div
className="group-header"
style={{ backgroundColor: colorScheme[className] || '#ddd' }}
Expand All @@ -101,7 +117,7 @@ const GroupedCells: React.FC<GroupedCellsProps> = ({ className, cells, onSelectN
{clusterIdentifiers.map(({ name, identifier }) => (
<button
key={name}
className={`cluster-button ${openClusters.includes(name) ? 'active' : ''}`}
className={`cluster-button ${selectedClusters.includes(name) ? 'active' : ''}`}
onClick={() => handleClusterClick(name)}
>
<span className="cluster-identifier">{identifier}</span> {/* Identifier (A, B, C) */}
Expand All @@ -110,7 +126,7 @@ const GroupedCells: React.FC<GroupedCellsProps> = ({ className, cells, onSelectN
))}
</div>
<div className="cluster-cells-container">
{selectedCells(openClusters)?.map((cell) => (
{selectedCells(selectedClusters)?.map((cell) => (
<div
key={`${cell.cell.notebook_id}-${cell.cell.cell_id}`}
className="cell-container"
Expand All @@ -134,7 +150,16 @@ const GroupedCells: React.FC<GroupedCellsProps> = ({ className, cells, onSelectN
);
};

const VizComponent: React.FC<{ data: VizData; onSelectNotebook: (notebookIds: [number]) => void }> = ({ data, onSelectNotebook }) => {
interface VizComponentProps {
data: VizData;
onSelectNotebook: (notebookIds: [number]) => void;
selectedClusters: string[];
setSelectedClusters: (clusters: string[]) => void;
scrolledClass: string;
}

const VizComponent: React.FC<VizComponentProps> = ({data, onSelectNotebook, selectedClusters, setSelectedClusters, scrolledClass}) => {

if (!data.notebooks || !Array.isArray(data.notebooks)) {
return <div>No valid notebook data found.</div>;
}
Expand Down Expand Up @@ -171,12 +196,16 @@ const VizComponent: React.FC<{ data: VizData; onSelectNotebook: (notebookIds: [n
className={className}
cells={cells}
onSelectNotebook={onSelectNotebook}
selectedClusters={selectedClusters}
setSelectedClusters={setSelectedClusters}
scrolledClass={scrolledClass}
/>
))}
</div>
);
};


export const LoadingComponent = () => <div>Loading...</div>;

export const DataNotFoundComponent = () => <div>No data found.</div>;
Expand Down
25 changes: 24 additions & 1 deletion src/VizContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ interface VizContentProps {
const VizContent: React.FC<VizContentProps> = ({ context, flowchartWidget }) => {
const [selectedNotebookIds, setSelectedNotebookIds] = useState<number[]>([-2]);
const [isReady, setIsReady] = useState<boolean>(context.isReady);
const [selectedClusters, setSelectedClusters] = useState<string[]>([]);
const [scrolledClass, setScrolledClass] = useState<string>('');

useEffect(() => {
if (!context.isReady) {
Expand All @@ -31,6 +33,25 @@ const VizContent: React.FC<VizContentProps> = ({ context, flowchartWidget }) =>
}
}, [context]);

// Function to handle Class selection
const handleClassSelection = (selectedClass: string) => {
console.log("Selected class", selectedClass);
setScrolledClass(selectedClass);
};

// Function to handle cluster selection
const handleClusterSelection = (selectedCluster: string, selectedClass: string) => {
if(selectedClusters.includes(selectedCluster)) {
setSelectedClusters(selectedClusters.filter(cluster => cluster !== selectedCluster));
}
else {
setSelectedClusters([...selectedClusters, selectedCluster]);
setScrolledClass(selectedClass);
}
};

flowchartWidget.addProps(handleClusterSelection, handleClassSelection, selectedClusters);

// Function to handle notebook selection
const handleNotebookSelection = useCallback((selectedIds: number[]) => {
setSelectedNotebookIds([...selectedIds]);
Expand Down Expand Up @@ -137,7 +158,6 @@ const VizContent: React.FC<VizContentProps> = ({ context, flowchartWidget }) =>
selectedNotebookIds.includes(notebook.notebook_id)
);

console.log("notebook names"+ jsonData.notebooks.map(notebook=> notebook.notebook_name))

return (
<div style={{ height: '100%', overflowY: 'auto' }}>
Expand All @@ -150,6 +170,9 @@ const VizContent: React.FC<VizContentProps> = ({ context, flowchartWidget }) =>
<VizComponent
data={{ notebooks: selectedNotebooks }}
onSelectNotebook={handleNotebookSelection}
selectedClusters={selectedClusters}
setSelectedClusters={setSelectedClusters}
scrolledClass={scrolledClass}
/>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const extension: JupyterFrontEndPlugin<void> = {
preferKernel: false
},
flowchartWidget);

app.docRegistry.addWidgetFactory(factory);

// Use the widget factory to create a new widget
Expand Down

0 comments on commit b08c6ba

Please sign in to comment.