Skip to content

Commit e737075

Browse files
authored
Merge pull request #56 from CarnegieLearningWeb/dev
Dev
2 parents 055cca2 + 38fda5e commit e737075

3 files changed

Lines changed: 112 additions & 76 deletions

File tree

src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,8 @@ function App() {
159159
{/* Graph and Data Display */}
160160
{!loading && csvData && (
161161
<div>
162-
<div className="relative w-full h-[800px] border border-gray-300 bg-white overflow-auto">
163-
<div className="w-max h-max mx-auto">
162+
<div className="relative w-full h-[700px] border border-gray-300 bg-white overflow-fit">
163+
<div className="w-max h-max mx-auto ">
164164
{/* GraphvizParent component generates and displays the graph based on the CSV data */}
165165
<GraphvizParent
166166
csvData={csvData}

src/components/GraphvizParent.tsx

Lines changed: 109 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// React component code
2-
import React, { useContext, useEffect, useRef, useState } from 'react';
3-
import { graphviz } from 'd3-graphviz';
2+
import React, {useContext, useEffect, useRef, useState} from 'react';
3+
import {graphviz} from 'd3-graphviz';
44
import {
55
generateDotString,
66
normalizeThicknesses,
@@ -11,8 +11,8 @@ import {
1111
} from './GraphvizProcessing';
1212
import ErrorBoundary from "@/components/errorBoundary.tsx";
1313
import '../GraphvizContainer.css';
14-
import { Context } from "@/Context.tsx";
15-
import { Button } from './ui/button';
14+
import {Context} from "@/Context.tsx";
15+
import {Button} from './ui/button';
1616

1717
interface GraphvizParentProps {
1818
csvData: string;
@@ -22,15 +22,15 @@ interface GraphvizParentProps {
2222
}
2323

2424
const GraphvizParent: React.FC<GraphvizParentProps> = ({
25-
csvData,
26-
filter,
27-
selfLoops,
28-
minVisits,
29-
}) => {
25+
csvData,
26+
filter,
27+
selfLoops,
28+
minVisits,
29+
}) => {
3030
const [dotString, setDotString] = useState<string | null>(null);
3131
const [filteredDotString, setFilteredDotString] = useState<string | null>(null);
3232
const [topDotString, setTopDotString] = useState<string | null>(null);
33-
const { selectedSequence, setSelectedSequence, top5Sequences, setTop5Sequences } = useContext(Context);
33+
const {selectedSequence, setSelectedSequence, top5Sequences, setTop5Sequences} = useContext(Context);
3434

3535
// Refs for rendering the Graphviz graphs
3636
const graphRefMain = useRef<HTMLDivElement>(null);
@@ -127,88 +127,124 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
127127
}, [csvData, filter, selfLoops, minVisits, selectedSequence]);
128128

129129
// Render Graphviz graphs using d3-graphviz
130-
const renderGraph = (dot: string | null, ref: React.RefObject<HTMLDivElement>) => {
130+
const renderGraph = (
131+
dot: string | null,
132+
ref: React.RefObject<HTMLDivElement>,
133+
filename: string,
134+
numberOfGraphs: number
135+
) => {
131136
if (dot && ref.current) {
137+
// Dynamically adjust width based on the number of graphs
138+
const width = numberOfGraphs === 3 ? 325 : 425; // Adjust the width for 3 graphs or 2 graphs
139+
const height = 530; // Fixed height (or adjust dynamically if needed)
140+
132141
graphviz(ref.current)
133-
.width(800)
134-
.height(600)
135-
.renderDot(dot);
142+
.width(width)
143+
.height(height)
144+
.renderDot(dot)
145+
.on('end', () => {
146+
const svgElement = ref.current?.querySelector('svg');
147+
if (svgElement) {
148+
exportGraphAsPNG(svgElement, filename);
149+
}
150+
});
136151
}
137152
};
138153

139154
// Export a graph as high-quality PNG
140-
const exportGraphAsPNG = (
141-
ref: React.RefObject<HTMLDivElement>,
142-
fileName: string,
143-
scale: number = 2, // Scale for higher quality
144-
margin: number = 20 // Smaller margin in pixels
145-
) => {
146-
if (ref.current) {
147-
const svgElement = ref.current.querySelector('svg');
148-
if (svgElement) {
149-
const svgData = new XMLSerializer().serializeToString(svgElement);
150-
151-
const canvas = document.createElement('canvas');
152-
const context = canvas.getContext('2d');
153-
const img = new Image();
154-
155-
img.onload = () => {
156-
const graphWidth = img.width * scale;
157-
const graphHeight = img.height * scale;
158-
159-
// Set canvas size with smaller margins
160-
canvas.width = graphWidth + margin * 2;
161-
canvas.height = graphHeight + margin * 2;
162-
163-
// Fill background (optional)
164-
context!.fillStyle = 'white';
165-
context!.fillRect(0, 0, canvas.width, canvas.height);
166-
167-
// Draw the graph centered within the canvas
168-
const xOffset = (canvas.width - graphWidth) / 2;
169-
const yOffset = (canvas.height - graphHeight) / 2;
170-
context!.drawImage(img, xOffset, yOffset, graphWidth, graphHeight);
171-
172-
// Export to PNG
173-
const pngData = canvas.toDataURL('image/png');
174-
const link = document.createElement('a');
175-
link.href = pngData;
176-
link.download = `${fileName}.png`;
177-
link.click();
178-
};
179-
180-
img.src = 'data:image/svg+xml;base64,' + btoa(svgData);
181-
}
182-
}
155+
const exportGraphAsPNG = (graphRef: React.RefObject<HTMLDivElement>, filename: string) => {
156+
if (!graphRef.current) return;
157+
158+
const svgElement = graphRef.current.querySelector('svg');
159+
if (!svgElement) return;
160+
161+
// Get SVG dimensions
162+
const width = svgElement.viewBox.baseVal.width || 425;
163+
const height = svgElement.viewBox.baseVal.height || 600;
164+
165+
// Clone the SVG to avoid style inheritance issues
166+
const clonedSvg = svgElement.cloneNode(true);
167+
168+
// Create a high-resolution canvas
169+
const scaleFactor = 5; // Adjust for higher quality (e.g., 2x or 3x)
170+
const canvas = document.createElement('canvas');
171+
canvas.width = (width * scaleFactor) * 1.25;
172+
canvas.height = (height * scaleFactor) * 1.5;
173+
const ctx = canvas.getContext('2d');
174+
175+
if (!ctx) return;
176+
177+
// Serialize the SVG
178+
const svgData = new XMLSerializer().serializeToString(clonedSvg);
179+
180+
// Convert SVG to an image
181+
const img = new Image();
182+
img.onload = () => {
183+
// Scale the canvas content for higher resolution
184+
ctx.clearRect(0, 0, canvas.width, canvas.height);
185+
ctx.scale(scaleFactor, scaleFactor);
186+
187+
ctx.drawImage(img, 0, 0);
188+
189+
// Export as PNG
190+
const link = document.createElement('a');
191+
link.download = `${filename}.png`;
192+
link.href = canvas.toDataURL('image/png');
193+
link.click();
194+
};
195+
196+
img.onerror = (err) => {
197+
console.error('Failed to load SVG for export:', err);
198+
};
199+
200+
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgData)}`;
183201
};
184202

185-
useEffect(() => renderGraph(dotString, graphRefMain), [dotString]);
186-
useEffect(() => renderGraph(filteredDotString, graphRefFiltered), [filteredDotString]);
187-
useEffect(() => renderGraph(topDotString, graphRefTop), [topDotString]);
203+
const numberOfGraphs = [topDotString, dotString, filteredDotString].filter(Boolean).length;
204+
205+
useEffect(() => {
206+
renderGraph(filteredDotString, graphRefFiltered, 'filtered_graph', numberOfGraphs);
207+
}, [topDotString]);
208+
209+
useEffect(() => {
210+
renderGraph(topDotString, graphRefTop, 'selected_sequence', numberOfGraphs);
211+
}, [dotString]);
212+
213+
useEffect(() => {
214+
renderGraph(dotString, graphRefMain, 'all_students', numberOfGraphs);
215+
}, [filteredDotString]);
216+
217+
218+
188219

189220
return (
190-
<div className="graphviz-container flex flex-col gap-8 w-full items-center">
221+
<div className="graphviz-container flex-col w-[500px] items-center">
191222
<ErrorBoundary>
192-
<div className="graphs flex justify-center gap-8 w-full">
223+
<div className="graphs flex justify-center w-[500px] h-[650px]"> {/*Not sure what this does*/}
193224
{topDotString && (
194-
<div className="graph-item flex flex-col items-center">
225+
<div
226+
className={`graph-item flex flex-col items-center ${topDotString && dotString && filteredDotString ? 'w-[400px]' : 'w-[500px]'} border-2 border-gray-700 rounded-lg p-4 bg-gray-100`}>
195227
<h2 className="text-lg font-semibold text-center mb-2">Selected Sequence</h2>
196-
<div ref={graphRefTop} className="w-auto h-auto"></div>
197-
<ExportButton onClick={() => exportGraphAsPNG(graphRefTop, 'selected_sequence')} />
228+
<div ref={graphRefTop}
229+
className="w-full h-[575px] border-2 border-gray-700 rounded-lg p-4 bg-white items-center"></div>
230+
<ExportButton onClick={() => exportGraphAsPNG(graphRefTop, 'selected_sequence')}/>
198231
</div>
199232
)}
200233
{dotString && (
201-
<div className="graph-item flex flex-col items-center">
234+
<div
235+
className={`graph-item flex flex-col items-center ${topDotString && dotString && filteredDotString ? 'w-[400px]' : 'w-[500px]'} border-2 border-gray-700 rounded-lg p-4 bg-gray-100`}>
202236
<h2 className="text-lg font-semibold text-center mb-2">All Students, All Paths</h2>
203-
<div ref={graphRefMain} className="w-auto h-auto"></div>
204-
<ExportButton onClick={() => exportGraphAsPNG(graphRefMain, 'all_students')} />
205-
</div>
237+
<div ref={graphRefMain}
238+
className="w-full h-[575px] border-2 border-gray-700 rounded-lg p-4 bg-white"></div>
239+
<ExportButton onClick={() => exportGraphAsPNG(graphRefMain, 'all_students')}/></div>
206240
)}
207241
{filteredDotString && (
208-
<div className="graph-item flex flex-col items-center">
242+
<div
243+
className={`graph-item flex flex-col items-center ${topDotString && dotString && filteredDotString ? 'w-[400px]' : 'w-[500px]'} border-2 border-gray-700 rounded-lg p-4 bg-gray-100`}>
209244
<h2 className="text-lg font-semibold text-center mb-4">Filtered Graph</h2>
210-
<div ref={graphRefFiltered} className="w-auto h-auto"></div>
211-
<ExportButton onClick={() => exportGraphAsPNG(graphRefFiltered, 'filtered_graph')} />
245+
<div ref={graphRefFiltered}
246+
className="w-full h-[575px] border-2 border-gray-700 rounded-lg p-4 bg-white"></div>
247+
<ExportButton onClick={() => exportGraphAsPNG(graphRefFiltered, 'filtered_graph')}/>
212248
</div>
213249
)}
214250
</div>
@@ -225,7 +261,7 @@ interface ExportButtonProps {
225261
label?: string;
226262
}
227263

228-
function ExportButton({ onClick, label = "Export Image" }: ExportButtonProps) {
264+
function ExportButton({onClick, label = "Export Image"}: ExportButtonProps) {
229265
return (
230266
<Button
231267
variant={'secondary'}

src/components/GraphvizProcessing.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ function calculateEdgeColors(outcomes: { [outcome: string]: number }): string {
304304
return 'digraph G {\n"Error" [label="No valid sequences found to display."];\n}';
305305
}
306306

307-
let dotString = 'digraph G {\ngraph [size="8,6!", dpi=72];\n';
307+
let dotString = 'digraph G {\ngraph [size="8,6!", dpi=150];\n';
308308
let totalSteps = selectedSequence.length//stepsInSelectedSequence.length;
309309
let steps = selectedSequence
310310

0 commit comments

Comments
 (0)