From 6b585057c96577a069187e76a1bf49c5fb2772d1 Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Mon, 24 Jun 2024 15:34:27 -0500 Subject: [PATCH 01/54] added new util file for grad and promotion filter --- bun.lockb | Bin 339201 -> 339009 bytes src/lib/GradPromUtils.ts | 7 +++++++ 2 files changed, 7 insertions(+) create mode 100644 src/lib/GradPromUtils.ts diff --git a/bun.lockb b/bun.lockb index d2b09d5a00849d40a7dd6cc06d717bb97e03db35..e433c00a595f1024f8e7d3af0fd230e33910419d 100755 GIT binary patch delta 1472 zcmYk5OKi+h6vpq&ooRJcwPti$9rb82S{+8KMYUR@^@w&N(|9br)hnbG78WvTLaZbf zm=!A_-FarP@dzS9#KtRPVIdYab`~Ojr~M>O{@=`ZfA`#T?>+ySkx#M5A7bbGf;SsS zGG{V@GnwGsjNjZH{N0lu?e_i4Lf*N6b1sZ5{*8n9nFffka`MH{=L{-m>s$jIj|qdVg<|&n48PT7g}iT?Ns7C{2G|WO0gt< zf~_FB#OC88Fl%|YSOdOPd8@=4VHY@zb!at=$1OnT0=$1~Q4hq{G@+NsVyzaZ@TYW> zbz%!)IkEL(i(prf#Rk<&_X(?3M!n`xmsK`^aJ|%=TD{9wIyFHkdVd8T-2^1+DN-B`(KL7274&4vUNJ z7Fz+kti$XP>wrB)w%J~>PW&^mePSyaUuCVmE;k6YI$h{C#wHIaG1-eDUS$i{dVQ)m z@S*Uw&qqU%7>SjrDp>fs$+rgbiLBp}Kb`g2{O7Fi3fm3h`KAHi+>pRlqwDAewVor} z=MK7uer3n^I6qXlG~gf9lsp<52Z_SgVPEb`0^3_s>9*A3bm7=>f9OW!K)p5Dx$pA| Z-|9n8e55Kl4n`Z#-b#iFx02!h&|i)RbD97E delta 1664 zcmXYxTW^d}6o&WAp0+xQR;Su(9oo@$RFxi;s-9FErw+y;E;Nxi)S=pli(bgI7fuP` z0uu?YB)AiY3~q!VA`x*ziSP&9=noLj>bxuS=6m}+4?c_gz1_jv{i%oDesHbx&IO!vS!gD-4zgI9Aub@s!$rk{sfHe3*x|Kh$3JIh z_ChdAwdD=59OdP}#>Hl(CapxSwsP6}WUUZku`vF-*le-cu!m|lFZG2I$vJFU!*ftG zTP)o?rO(B0PbetAXW$Ym`_6i;A_u__-LW6>C@Ca#*q0 zda+vAA7s%1vj$h7Uwjh#T%F3R!}n!7?w{xqT#28hTj&<6hlOGG4SU2^;q#Q&E4CVT zQEa1F1ME5tR`TqF@pFyn3M|TV!1I1P(}X@Ei_JRRj6bXCwurUBl44uMT48sQ#WtAL zTZ7t_H>kWe*f_F?i>eQVU``&@D|k$!yIAA1yz hRoS<4?m;YFy)5*`ho@p0FkE+Gx+avKuE`n<{RaxlehvTt diff --git a/src/lib/GradPromUtils.ts b/src/lib/GradPromUtils.ts new file mode 100644 index 0000000..3810d76 --- /dev/null +++ b/src/lib/GradPromUtils.ts @@ -0,0 +1,7 @@ +import { DataSet1 } from "./types"; + + +function promotedFilter(data: DataSet1): DataSet1 { + + return +} \ No newline at end of file From 05cdbe908c12e21cd9993b287cf0f3f1475c6b6d Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Tue, 25 Jun 2024 14:15:20 -0500 Subject: [PATCH 02/54] Filter Promoted and Graduated --- src/lib/GradPromUtils.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lib/GradPromUtils.ts b/src/lib/GradPromUtils.ts index 3810d76..55a8489 100644 --- a/src/lib/GradPromUtils.ts +++ b/src/lib/GradPromUtils.ts @@ -1,7 +1,11 @@ import { DataSet1 } from "./types"; -function promotedFilter(data: DataSet1): DataSet1 { - - return +export function filterPromGrad(data: DataSet1[], promOrGrad: "GRADUATED" | "PROMOTED" | "NOT_COMPLETED" | null): DataSet1[] { + if (promOrGrad !== null) { + return data.filter(item => item["CF (Workspace Progress Status)"] === promOrGrad); + } else { + // Return the original data if promOrGrad is null + return data; + } } \ No newline at end of file From 4b7129f2dabe870829791051fd534d1f749c6e07 Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Tue, 25 Jun 2024 14:52:34 -0500 Subject: [PATCH 03/54] added Filter Promoted and Graduated objects to context file --- src/App.tsx | 10 +++++++++- src/Context.tsx | 6 ++++++ src/lib/GradPromUtils.ts | 4 ++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 4831e4a..8a5a94a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,10 +7,11 @@ import DropZone from './components/DropZone'; import { Button } from './components/ui/button'; import { Context } from './Context'; import { processDataShopData } from './lib/dataProcessingUtils'; +import {filterPromGrad} from "@/lib/GradPromUtils"; function App() { - const { resetData, setGraphData, setLoading, data, setData, graphData, loading } = useContext(Context) + const { resetData, setGraphData, setLoading, setFilteredData, filteredData, data, setData, graphData, loading } = useContext(Context) const [showDropZone, setShowDropZone] = useState(true) const handleData = (data: GlobalDataType[]) => { @@ -22,6 +23,13 @@ function App() { setLoading(loading) } + const filterData = (filteredData: GlobalDataType[]) => { + const data: GlobalDataType[] = filterPromGrad(filteredData, "GRADUATED") + setFilteredData(data) + console.log(data) + } + + useEffect(() => { if (data) { const graphData: GraphData = processDataShopData(data) diff --git a/src/Context.tsx b/src/Context.tsx index 3e03b0f..2f8bc28 100644 --- a/src/Context.tsx +++ b/src/Context.tsx @@ -3,6 +3,8 @@ import { GlobalDataType, GraphData } from './lib/types'; interface ContextInterface { data: GlobalDataType[] | null; graphData: GraphData | null; + filteredData: GlobalDataType[] | null + setFilteredData: (filteredData: GlobalDataType[] | null) => void; loading: boolean; setLoading: (loading: boolean) => void; setData: (data: GlobalDataType[] | null) => void; @@ -13,6 +15,7 @@ interface ContextInterface { export const Context = createContext({} as ContextInterface); const initialState = { data: null, + filteredData: null, graphData: null, loading: false } @@ -22,6 +25,7 @@ interface ProviderProps { } export const Provider = ({ children }: ProviderProps) => { const [data, setData] = useState(initialState.data) + const [filteredData, setFilteredData] = useState(initialState.filteredData) const [graphData, setGraphData] = useState(initialState.graphData) const [loading, setLoading] = useState(initialState.loading) @@ -36,8 +40,10 @@ export const Provider = ({ children }: ProviderProps) => { item["CF (Workspace Progress Status)"] === promOrGrad); } else { From ac18cb67d2ab0be7959bc51f07b3d16a951e7039 Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:41:30 -0500 Subject: [PATCH 04/54] added Filter Promoted button to App.tsx --- src/App.tsx | 18 ++++++++++++++++-- src/lib/GradPromUtils.ts | 2 ++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 8a5a94a..ec7acea 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,7 +24,7 @@ function App() { } const filterData = (filteredData: GlobalDataType[]) => { - const data: GlobalDataType[] = filterPromGrad(filteredData, "GRADUATED") + const data: GlobalDataType[] = filterPromGrad(filteredData, "PROMOTED") setFilteredData(data) console.log(data) } @@ -36,6 +36,10 @@ function App() { setGraphData(graphData) } + // if (filteredData) { + // const graphData: GraphData = processDataShopData(filteredData) + // setGraphData(graphData) + // } }, [data]) return ( @@ -83,7 +87,17 @@ function App() { ) } - +
+ +
diff --git a/src/lib/GradPromUtils.ts b/src/lib/GradPromUtils.ts index 8b849c2..f947a3d 100644 --- a/src/lib/GradPromUtils.ts +++ b/src/lib/GradPromUtils.ts @@ -2,6 +2,8 @@ import {GlobalDataType} from "./types"; export function filterPromGrad(data: GlobalDataType[], promOrGrad: "GRADUATED" | "PROMOTED" | "NOT_COMPLETED" | null): GlobalDataType[] { + if (data == null) { + } if (promOrGrad !== null) { return data.filter(item => item["CF (Workspace Progress Status)"] === promOrGrad); } else { From 2f2178eea322a272ebcb9396f8221a1b01ffe7fa Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Thu, 27 Jun 2024 10:44:28 -0500 Subject: [PATCH 05/54] added Filter Promoted button to App.tsx --- .gitignore | 1 + package.json | 3 ++- src/App.tsx | 30 ++++++++++++++++++------------ src/lib/GradPromUtils.ts | 1 + 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 8b28018..c25b28a 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ public/sample_data .env src/lib/local_sample_data .vercel +bun.lockb \ No newline at end of file diff --git a/package.json b/package.json index ea5b795..04207cd 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "private": true, "version": "0.0.0", "type": "module", -"scripts": { + "scripts": { "dev": "vite", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", @@ -30,6 +30,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "d3": "^7.9.0", + "danfojs-node": "^1.1.2", "express": "^4.19.2", "html2canvas": "^1.4.1", "joi": "^17.13.1", diff --git a/src/App.tsx b/src/App.tsx index ec7acea..26fa595 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,18 +29,23 @@ function App() { console.log(data) } + // const handleFilteredData = (filteredData: GlobalDataType[]) => { + // setData(filteredData) + // setShowDropZone(false) + // const graphData: GraphData = processDataShopData(filteredData) + // setGraphData(graphData) + // } useEffect(() => { if (data) { const graphData: GraphData = processDataShopData(data) setGraphData(graphData) - } // if (filteredData) { // const graphData: GraphData = processDataShopData(filteredData) // setGraphData(graphData) // } - }, [data]) + }, [data]) //,filteredData return ( <> @@ -87,16 +92,17 @@ function App() { ) } -
- +
+
+ +
diff --git a/src/lib/GradPromUtils.ts b/src/lib/GradPromUtils.ts index f947a3d..2132835 100644 --- a/src/lib/GradPromUtils.ts +++ b/src/lib/GradPromUtils.ts @@ -2,6 +2,7 @@ import {GlobalDataType} from "./types"; export function filterPromGrad(data: GlobalDataType[], promOrGrad: "GRADUATED" | "PROMOTED" | "NOT_COMPLETED" | null): GlobalDataType[] { + // If no data has been imported into DropZone, do nothing if (data == null) { } if (promOrGrad !== null) { From 0a56cf313b7b2b7a5dd3807f9b3ed64a68b478ed Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Fri, 28 Jun 2024 14:40:26 -0500 Subject: [PATCH 06/54] rerendering loop error --- src/App.tsx | 163 +++++++++++++++++++++++------------ src/Switch.css | 47 ++++++++++ src/components/ui/switch.tsx | 21 +++++ 3 files changed, 178 insertions(+), 53 deletions(-) create mode 100644 src/Switch.css create mode 100644 src/components/ui/switch.tsx diff --git a/src/App.tsx b/src/App.tsx index 26fa595..6dfcd53 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import { Button } from './components/ui/button'; import { Context } from './Context'; import { processDataShopData } from './lib/dataProcessingUtils'; import {filterPromGrad} from "@/lib/GradPromUtils"; +import Switch from "@/components/ui/switch.tsx"; function App() { @@ -23,10 +24,11 @@ function App() { setLoading(loading) } - const filterData = (filteredData: GlobalDataType[]) => { - const data: GlobalDataType[] = filterPromGrad(filteredData, "PROMOTED") - setFilteredData(data) - console.log(data) + const filterData = (data: GlobalDataType[], promOrGrad:"PROMOTED"|"GRADUATED"|null) => { + // const filteredData: GlobalDataType[] = + setFilteredData(filterPromGrad(data, promOrGrad)) + // console.log(data) + return filteredData } // const handleFilteredData = (filteredData: GlobalDataType[]) => { @@ -40,12 +42,57 @@ function App() { if (data) { const graphData: GraphData = processDataShopData(data) setGraphData(graphData) + }}, [data]) + + useEffect(() => { + if (filteredData) { + const graphData: GraphData = processDataShopData(filteredData) + setGraphData(graphData) + }}, [filteredData]) + + const [isOn, setIsOn] = useState(false); + const [isSwitchEnabled, setIsSwitchEnabled] = useState(false); + const [filter, setFilter] = useState(null); + + const handleToggle = () => { + if (isSwitchEnabled){ + setIsOn(!isOn); } - // if (filteredData) { - // const graphData: GraphData = processDataShopData(filteredData) - // setGraphData(graphData) - // } - }, [data]) //,filteredData + }; + + const handleCheckboxChange = (event: React.ChangeEvent) => { + setIsSwitchEnabled(event.target.checked); + }; + + useEffect(() => { + if (isSwitchEnabled) { + const value = isOn ? "PROMOTED" : "GRADUATED"; + setFilter(value); + + setFilteredData(filterData(data!, value)) + console.log(filteredData) + } + + // else{ + // setFilter(null) + // } + // console.log(filter) + + }, [isSwitchEnabled, isOn]); + + const getValueBasedOnSwitch = () => { + if (isSwitchEnabled) { + console.log("Filter: " + filter) + setFilter(isOn ? "PROMOTED":"GRADUATED" ); + return filter + } + else { + // const noFilter = null + setFilter(null) + console.log("Filter: " + filter) + return filter + } + }; return ( <> @@ -62,52 +109,62 @@ function App() { Reset -
- - { - loading ? -
-
-

Loading...

-
+
+ + { + loading ? +
+
+

Loading...

+
+
+ : + ( + showDropZone && ( +
+ +
+ ) + + ) + + } + + + { + graphData && ( + <> + {/* Add suspense, lazy? */} + + + ) + } +
+ +
- : - ( - showDropZone && ( -
- -
- ) - - ) - - } - - - { - graphData && ( - <> - {/* Add suspense, lazy? */} - - - ) - } -
-
- -
-
-
+ + {/*
*/} + {/*
*/} + {/* {*/} + {/* filterData(data!);*/} + {/* setFilteredData(data);*/} + {/* // handleFilteredData(filteredData!)*/} + {/* }*/} + {/* }> Filter Promoted Data */} + {/*
*/} +
- - ) + + +) } export default App diff --git a/src/Switch.css b/src/Switch.css new file mode 100644 index 0000000..ee25e52 --- /dev/null +++ b/src/Switch.css @@ -0,0 +1,47 @@ +/* src/Switch.css */ +.switch-container { + display: inline-block; + cursor: pointer; +} + +.switch { + width: 50px; + height: 25px; + border-radius: 25px; + background-color: grey; + position: absolute; + left: 0px; + top: 350px; + transition: background-color 0.2s; +} + +.Promoted { + background-color: lightgreen; +} + +.Graduated { + background-color: deepskyblue; +} + +.switch-handle { + width: 23px; + height: 23px; + border-radius: 50%; + background-color: white; + position: absolute; + top: 1px; + left: 1px; + transition: left 0.2s; +} + +.Promoted .switch-handle { + left: 26px; +} + +.switch-display{ /*Not working*/ + display: none; +} + +.switch.disabled{ /*Not working*/ + visibility: hidden; +} diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx new file mode 100644 index 0000000..7e02d32 --- /dev/null +++ b/src/components/ui/switch.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import './../../Switch.css'; + + +interface SwitchProps { + isOn: boolean; + handleToggle: () => void; + filter: string|null; + isDisabled: boolean; + +} +const Switch: React.FC = ({ isOn, handleToggle }) => { + return ( +
+
+
+
+
+ ); +}; +export default Switch; \ No newline at end of file From fa4b15c75547dbea03e012cc43a30d4008f78589 Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Fri, 28 Jun 2024 16:41:51 -0500 Subject: [PATCH 07/54] Data won't filter and value/filter are opposites but using buttons doesn't crash the page --- src/App.tsx | 59 ++++++++++++++++++++++-------------- src/components/ui/switch.tsx | 7 +++-- src/lib/GradPromUtils.ts | 6 ++-- 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 6dfcd53..093af38 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import './App.css'; -import { useContext, useEffect, useState } from 'react'; +import {ChangeEventHandler, useContext, useEffect, useState} from 'react'; import { GlobalDataType, GraphData } from './lib/types'; import DirectedGraph from './components/DirectedGraph'; import DropZone from './components/DropZone'; @@ -12,7 +12,8 @@ import Switch from "@/components/ui/switch.tsx"; function App() { - const { resetData, setGraphData, setLoading, setFilteredData, filteredData, data, setData, graphData, loading } = useContext(Context) + const { resetData, setGraphData, setLoading, setFilteredData, filteredData, + data, setData, graphData, loading } = useContext(Context) const [showDropZone, setShowDropZone] = useState(true) const handleData = (data: GlobalDataType[]) => { @@ -24,11 +25,12 @@ function App() { setLoading(loading) } - const filterData = (data: GlobalDataType[], promOrGrad:"PROMOTED"|"GRADUATED"|null) => { + const filterData = (data: GlobalDataType[], filter:"PROMOTED"|"GRADUATED"|null|string) => { // const filteredData: GlobalDataType[] = - setFilteredData(filterPromGrad(data, promOrGrad)) + const f = filterPromGrad(data, filter) + setFilteredData(f) // console.log(data) - return filteredData + // return filteredData } // const handleFilteredData = (filteredData: GlobalDataType[]) => { @@ -52,7 +54,7 @@ function App() { const [isOn, setIsOn] = useState(false); const [isSwitchEnabled, setIsSwitchEnabled] = useState(false); - const [filter, setFilter] = useState(null); + const [filter, setFilter] = useState<"PROMOTED"|"GRADUATED"|null|string>("GRADUATED"); const handleToggle = () => { if (isSwitchEnabled){ @@ -64,12 +66,20 @@ function App() { setIsSwitchEnabled(event.target.checked); }; + // const handleFilterChange = (event: React.ChangeEvent) => { + // setFilter(event.target.value); + // }; + useEffect(() => { if (isSwitchEnabled) { - const value = isOn ? "PROMOTED" : "GRADUATED"; + let value = isOn ? "PROMOTED" : "GRADUATED"; setFilter(value); + console.log("Filter: " + filter) + let f = filterData(data!, value) + setFilteredData(f) + console.log("Filter: " + filter) + console.log("Value: " + value) - setFilteredData(filterData(data!, value)) console.log(filteredData) } @@ -80,19 +90,19 @@ function App() { }, [isSwitchEnabled, isOn]); - const getValueBasedOnSwitch = () => { - if (isSwitchEnabled) { - console.log("Filter: " + filter) - setFilter(isOn ? "PROMOTED":"GRADUATED" ); - return filter - } - else { - // const noFilter = null - setFilter(null) - console.log("Filter: " + filter) - return filter - } - }; + // const getValueBasedOnSwitch = () => { + // if (isSwitchEnabled) { + // console.log("Filter: " + filter) + // setFilter(isOn ? "PROMOTED":"GRADUATED" ); + // return filter + // } + // else { + // // const noFilter = null + // setFilter(null) + // console.log("Filter: " + filter) + // return filter + // } + // }; return ( <> @@ -145,8 +155,11 @@ function App() { Filter by Section Completion Status? - + +
{/*
*/} diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx index 7e02d32..ddff7e2 100644 --- a/src/components/ui/switch.tsx +++ b/src/components/ui/switch.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {ChangeEventHandler} from 'react'; import './../../Switch.css'; @@ -6,7 +6,10 @@ interface SwitchProps { isOn: boolean; handleToggle: () => void; filter: string|null; - isDisabled: boolean; + + isDisabled?: boolean; + + onChange?: ChangeEventHandler | undefined; } const Switch: React.FC = ({ isOn, handleToggle }) => { diff --git a/src/lib/GradPromUtils.ts b/src/lib/GradPromUtils.ts index 2132835..16c0f2a 100644 --- a/src/lib/GradPromUtils.ts +++ b/src/lib/GradPromUtils.ts @@ -1,10 +1,10 @@ import {GlobalDataType} from "./types"; -export function filterPromGrad(data: GlobalDataType[], promOrGrad: "GRADUATED" | "PROMOTED" | "NOT_COMPLETED" | null): GlobalDataType[] { +export function filterPromGrad(data: GlobalDataType[], promOrGrad: "GRADUATED" | "PROMOTED" | null| string): GlobalDataType[] { // If no data has been imported into DropZone, do nothing - if (data == null) { - } + // if (data == null) { + // } if (promOrGrad !== null) { return data.filter(item => item["CF (Workspace Progress Status)"] === promOrGrad); } else { From 1e1d6bbd3e85f7a80a6f017e4a7c454b0f9e58f5 Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Mon, 1 Jul 2024 09:00:59 -0500 Subject: [PATCH 08/54] Switch and checkbox correctly filter data --- src/App.tsx | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 093af38..7ae85b6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -28,7 +28,8 @@ function App() { const filterData = (data: GlobalDataType[], filter:"PROMOTED"|"GRADUATED"|null|string) => { // const filteredData: GlobalDataType[] = const f = filterPromGrad(data, filter) - setFilteredData(f) + // setFilteredData(f) + return f // console.log(data) // return filteredData } @@ -55,6 +56,8 @@ function App() { const [isOn, setIsOn] = useState(false); const [isSwitchEnabled, setIsSwitchEnabled] = useState(false); const [filter, setFilter] = useState<"PROMOTED"|"GRADUATED"|null|string>("GRADUATED"); + // Using Graduated as initial gives the first click the correct value but not after + // -- doesn't work if initial state is null const handleToggle = () => { if (isSwitchEnabled){ @@ -73,14 +76,15 @@ function App() { useEffect(() => { if (isSwitchEnabled) { let value = isOn ? "PROMOTED" : "GRADUATED"; - setFilter(value); - console.log("Filter: " + filter) - let f = filterData(data!, value) - setFilteredData(f) - console.log("Filter: " + filter) - console.log("Value: " + value) - - console.log(filteredData) + // setFilter(value); + // console.log("Filter: " + filter) + let f = filterData(data, value) // Why can't I use filter here instead of value? + + // console.log("Filter: " + filter) + + console.log("Filter: " + value) + + console.log(f) } // else{ @@ -158,6 +162,7 @@ function App() {
From 1ccadfca575f3fe0f69d0f4aae43184e47971da2 Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Mon, 1 Jul 2024 10:07:33 -0500 Subject: [PATCH 09/54] Placed components in more appropriate locations --- src/App.tsx | 48 ++++++++++++++++++++++++------------------------ src/Switch.css | 21 ++++++++++++++++++--- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 7ae85b6..79e8436 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import './App.css'; -import {ChangeEventHandler, useContext, useEffect, useState} from 'react'; +import {useContext, useEffect, useState} from 'react'; import { GlobalDataType, GraphData } from './lib/types'; import DirectedGraph from './components/DirectedGraph'; import DropZone from './components/DropZone'; @@ -9,6 +9,8 @@ import { Context } from './Context'; import { processDataShopData } from './lib/dataProcessingUtils'; import {filterPromGrad} from "@/lib/GradPromUtils"; import Switch from "@/components/ui/switch.tsx"; +import './Switch.css'; + function App() { @@ -76,9 +78,9 @@ function App() { useEffect(() => { if (isSwitchEnabled) { let value = isOn ? "PROMOTED" : "GRADUATED"; - // setFilter(value); + setFilter(value); // console.log("Filter: " + filter) - let f = filterData(data, value) // Why can't I use filter here instead of value? + let f = filterData(data!, value) // Why can't I use filter here instead of value? // console.log("Filter: " + filter) @@ -87,26 +89,19 @@ function App() { console.log(f) } - // else{ - // setFilter(null) - // } - // console.log(filter) + else{ + setData(data) + console.log(data) + } + }, [isSwitchEnabled, isOn]); - // const getValueBasedOnSwitch = () => { - // if (isSwitchEnabled) { - // console.log("Filter: " + filter) - // setFilter(isOn ? "PROMOTED":"GRADUATED" ); - // return filter - // } - // else { - // // const noFilter = null - // setFilter(null) - // console.log("Filter: " + filter) - // return filter - // } - // }; + const getValueBasedOnSwitch = () => { + if (isSwitchEnabled) { + return filter + }; + } return ( <> @@ -154,16 +149,21 @@ function App() { ) } -
+
+

+

+

+ +
diff --git a/src/Switch.css b/src/Switch.css index ee25e52..6fac023 100644 --- a/src/Switch.css +++ b/src/Switch.css @@ -11,7 +11,7 @@ background-color: grey; position: absolute; left: 0px; - top: 350px; + top: 40px; transition: background-color 0.2s; } @@ -42,6 +42,21 @@ display: none; } -.switch.disabled{ /*Not working*/ - visibility: hidden; +.switch.disabled.switch-display{ /*Not working*/ + opacity: 30; +} + +.checkbox{ + position: fixed; + left: 93px; + top: 50px; +} + +.checkbox.toggler__label{ + position: fixed; +} + +.tab { + display: inline-block; + margin-left: 10px; } From b29cdc6a948716c61ae33d77f4a5ebaaecb9eeb8 Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Mon, 1 Jul 2024 10:24:17 -0500 Subject: [PATCH 10/54] No crashing if no data but click filter checkbox --- src/App.tsx | 13 ++++++++----- src/Switch.css | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 79e8436..870eeab 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -28,10 +28,14 @@ function App() { } const filterData = (data: GlobalDataType[], filter:"PROMOTED"|"GRADUATED"|null|string) => { - // const filteredData: GlobalDataType[] = - const f = filterPromGrad(data, filter) - // setFilteredData(f) - return f + if (data){ + const f = filterPromGrad(data, filter) + // setFilteredData(f) + return f + } + else { + + } // console.log(data) // return filteredData } @@ -164,7 +168,6 @@ function App() {

-
{/*
*/} diff --git a/src/Switch.css b/src/Switch.css index 6fac023..0da78bf 100644 --- a/src/Switch.css +++ b/src/Switch.css @@ -11,7 +11,7 @@ background-color: grey; position: absolute; left: 0px; - top: 40px; + top: 35px; transition: background-color 0.2s; } From 2d9302c558b6a37c55922ac824d5268567f40e31 Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Mon, 1 Jul 2024 11:17:07 -0500 Subject: [PATCH 11/54] Move filtering to DirectedGraph.tsx. Filters data correctly, doesn't regraph --- src/App.tsx | 51 ++++++--------- src/components/DirectedGraph.tsx | 107 +++++++++++++++++++++++++++---- 2 files changed, 114 insertions(+), 44 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 870eeab..fe29534 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -86,15 +86,13 @@ function App() { // console.log("Filter: " + filter) let f = filterData(data!, value) // Why can't I use filter here instead of value? - // console.log("Filter: " + filter) - console.log("Filter: " + value) console.log(f) } else{ - setData(data) + setData(data!) console.log(data) } @@ -153,34 +151,25 @@ function App() { ) } -
- - -

-

-

- - -
- - {/*
*/} - {/*
*/} - {/* {*/} - {/* filterData(data!);*/} - {/* setFilteredData(data);*/} - {/* // handleFilteredData(filteredData!)*/} - {/* }*/} - {/* }> Filter Promoted Data */} - {/*
*/} + + + {/*
*/} + {/* */} + {/* */} + {/*

*/} + {/*

*/} + {/*

*/} + + {/* */} + {/*
*/} +
diff --git a/src/components/DirectedGraph.tsx b/src/components/DirectedGraph.tsx index dd2919f..44683c7 100644 --- a/src/components/DirectedGraph.tsx +++ b/src/components/DirectedGraph.tsx @@ -1,6 +1,6 @@ -import { useEffect, useRef, useState } from "react" +import {useContext, useEffect, useRef, useState} from "react" import { ForceGraph2D } from 'react-force-graph'; -import { GraphData, ToolTip, Node, Link, ContextMenuControls, LinkObject } from "@/lib/types"; +import {GraphData, ToolTip, Node, Link, ContextMenuControls, LinkObject, GlobalDataType} from "@/lib/types"; import * as d3 from "d3"; import html2canvas from 'html2canvas'; @@ -9,7 +9,10 @@ import { Cross2Icon } from "@radix-ui/react-icons"; import { Slider } from "@/components/ui/slider" import { addUnusedNodes, changeLinkThreshold, removeUnusedNodes } from "@/lib/dataProcessingUtils"; import ToggleTool from "@/components/ToggleTool"; - +import {filterPromGrad} from "@/lib/GradPromUtils"; +import Switch from "@/components/ui/switch"; +import "./../Switch.css" +import {Context} from "@/Context.tsx"; // TODO make sure the node info is changed interface DirectedGraphProps { @@ -33,6 +36,7 @@ export default function DirectedGraph({ graphData }: DirectedGraphProps) { const [, setRemovedNodeStorage] = useState([]); const [windowWidth, setWindowWidth] = useState(window.innerWidth); + const {data, setData} = useContext(Context) // save to photo const captureScreenshot = () => { const input = document.getElementById('graph') as HTMLElement; @@ -54,6 +58,60 @@ export default function DirectedGraph({ graphData }: DirectedGraphProps) { } + const filterData = (data: GlobalDataType[], filter:"PROMOTED"|"GRADUATED"|null|string) => { + if (data){ + const f = filterPromGrad(data, filter) + // setFilteredData(f) + return f + } + else { + + }} + + const [isOn, setIsOn] = useState(false); + const [isSwitchEnabled, setIsSwitchEnabled] = useState(false); + const [filter, setFilter] = useState<"PROMOTED"|"GRADUATED"|null|string>("GRADUATED"); + // Using Graduated as initial gives the first click the correct value but not after + // -- doesn't work if initial state is null + + const handleToggle = () => { + if (isSwitchEnabled){ + setIsOn(!isOn); + } + }; + + const handleCheckboxChange = (event: React.ChangeEvent) => { + setIsSwitchEnabled(event.target.checked); + }; + + // const handleFilterChange = (event: React.ChangeEvent) => { + // setFilter(event.target.value); + // }; + + useEffect(() => { + if (isSwitchEnabled) { + let value = isOn ? "PROMOTED" : "GRADUATED"; + setFilter(value); + // console.log("Filter: " + filter) + let f = filterData(data!, value) + console.log("Filter: " + value) + + console.log(f) + } + + else{ + setData(data) + console.log(data) + } + + + }, [isSwitchEnabled, isOn]); + + const getValueBasedOnSwitch = () => { + if (isSwitchEnabled) { + return filter + }; + } useEffect(() => { // resizing of window @@ -175,7 +233,6 @@ export default function DirectedGraph({ graphData }: DirectedGraphProps) { <> - { contextMenu.visible && (
- +
Node Info
@@ -224,7 +281,8 @@ export default function DirectedGraph({ graphData }: DirectedGraphProps) { )} {contextMenu.node?.cumulativeSelfLoops && (
- Cumulative Self Loops: {contextMenu.node.cumulativeSelfLoops} + Cumulative Self Loops: {contextMenu.node.cumulativeSelfLoops}
)} {contextMenu.node?.edgesIn && ( @@ -265,8 +323,6 @@ export default function DirectedGraph({ graphData }: DirectedGraphProps) { )} - -
) } @@ -358,7 +414,7 @@ export default function DirectedGraph({ graphData }: DirectedGraphProps) { nodeAutoColorBy={"problemId"} // cooldownTime={freezeNodes ? 0 : Infinity} // this controls the "steadiness" -- 1 is the most steady, infinity is the least onNodeClick={(node, event) => { - setContextMenu({ visible: true, x: event.clientX, y: event.clientY, node: node }); + setContextMenu({visible: true, x: event.clientX, y: event.clientY, node: node}); }} onNodeDragEnd={(node) => { if (pinnable) { @@ -440,6 +496,31 @@ export default function DirectedGraph({ graphData }: DirectedGraphProps) { />
+ +
+ + +

+

+

+ + +
- ) -} \ No newline at end of file + )} + + + + + + + + + From 5c55136ca95124990d52f5d8ed12d45d02ff7352 Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Mon, 1 Jul 2024 15:26:29 -0500 Subject: [PATCH 12/54] IT WORKS!! --- src/App.tsx | 86 +-- src/components/DirectedGraph.tsx | 944 ++++++++++++++++--------------- src/lib/dataProcessingUtils.ts | 2 +- 3 files changed, 518 insertions(+), 514 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index fe29534..ef5ba81 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,7 +14,7 @@ import './Switch.css'; function App() { - const { resetData, setGraphData, setLoading, setFilteredData, filteredData, + const { resetData, setGraphData, setLoading, //setFilteredData, filteredData, data, setData, graphData, loading } = useContext(Context) const [showDropZone, setShowDropZone] = useState(true) @@ -53,57 +53,57 @@ function App() { setGraphData(graphData) }}, [data]) - useEffect(() => { - if (filteredData) { - const graphData: GraphData = processDataShopData(filteredData) - setGraphData(graphData) - }}, [filteredData]) + // useEffect(() => { + // if (filteredData) { + // const graphData: GraphData = processDataShopData(filteredData) + // setGraphData(graphData) + // }}, [filteredData]) - const [isOn, setIsOn] = useState(false); - const [isSwitchEnabled, setIsSwitchEnabled] = useState(false); - const [filter, setFilter] = useState<"PROMOTED"|"GRADUATED"|null|string>("GRADUATED"); + // const [isOn, setIsOn] = useState(false); + // const [isSwitchEnabled, setIsSwitchEnabled] = useState(false); + // const [filter, setFilter] = useState<"PROMOTED"|"GRADUATED"|null|string>("GRADUATED"); // Using Graduated as initial gives the first click the correct value but not after // -- doesn't work if initial state is null - const handleToggle = () => { - if (isSwitchEnabled){ - setIsOn(!isOn); - } - }; - - const handleCheckboxChange = (event: React.ChangeEvent) => { - setIsSwitchEnabled(event.target.checked); - }; + // const handleToggle = () => { + // if (isSwitchEnabled){ + // setIsOn(!isOn); + // } + // }; + // + // const handleCheckboxChange = (event: React.ChangeEvent) => { + // setIsSwitchEnabled(event.target.checked); + // }; // const handleFilterChange = (event: React.ChangeEvent) => { // setFilter(event.target.value); // }; - useEffect(() => { - if (isSwitchEnabled) { - let value = isOn ? "PROMOTED" : "GRADUATED"; - setFilter(value); - // console.log("Filter: " + filter) - let f = filterData(data!, value) // Why can't I use filter here instead of value? - - console.log("Filter: " + value) - - console.log(f) - } - - else{ - setData(data!) - console.log(data) - } - - - }, [isSwitchEnabled, isOn]); - - const getValueBasedOnSwitch = () => { - if (isSwitchEnabled) { - return filter - }; - } + // useEffect(() => { + // if (isSwitchEnabled) { + // let value = isOn ? "PROMOTED" : "GRADUATED"; + // setFilter(value); + // // console.log("Filter: " + filter) + // let f = filterData(data!, value) // Why can't I use filter here instead of value? + // + // console.log("Filter: " + value) + // + // console.log(f) + // } + // + // else{ + // setData(data!) + // console.log(data) + // } + // + // + // }, [isSwitchEnabled, isOn]); + // + // const getValueBasedOnSwitch = () => { + // if (isSwitchEnabled) { + // return filter + // }; + // } return ( <> diff --git a/src/components/DirectedGraph.tsx b/src/components/DirectedGraph.tsx index 44683c7..dfbae79 100644 --- a/src/components/DirectedGraph.tsx +++ b/src/components/DirectedGraph.tsx @@ -7,66 +7,67 @@ import html2canvas from 'html2canvas'; import toast from "react-hot-toast"; import { Cross2Icon } from "@radix-ui/react-icons"; import { Slider } from "@/components/ui/slider" -import { addUnusedNodes, changeLinkThreshold, removeUnusedNodes } from "@/lib/dataProcessingUtils"; +import {addUnusedNodes, changeLinkThreshold, processDataShopData, removeUnusedNodes} from "@/lib/dataProcessingUtils"; import ToggleTool from "@/components/ToggleTool"; import {filterPromGrad} from "@/lib/GradPromUtils"; import Switch from "@/components/ui/switch"; import "./../Switch.css" import {Context} from "@/Context.tsx"; + // TODO make sure the node info is changed interface DirectedGraphProps { - graphData: GraphData; + graphData: GraphData; } export default function DirectedGraph({ graphData }: DirectedGraphProps) { - const initialTooltip: ToolTip = { display: false, text: '', x: 0, y: 0, fx: undefined, fy: undefined }; - const [tooltip, setTooltip] = useState(initialTooltip); - const [currentGraphData, setCurrentGraphData] = useState(graphData); - const forceGraphRef = useRef(null); // must be `any` because no typing is available for react-force-graph - const [orderByTime,] = useState(true); - const [pinnable,] = useState(false); - const [edgesThreshold, setEdgesThreshold] = useState((graphData.maxEdgeCount ?? 100) * 0.1); - const [previousEdgesThreshold, setPreviousEdgesThreshold] = useState(((graphData.maxEdgeCount ?? 100) * 0.1)); - const initialContextMenuControls: ContextMenuControls = { visible: false, x: 0, y: 0, node: null }; - - const [contextMenu, setContextMenu] = useState(initialContextMenuControls); - - const [, setRemovedNodeStorage] = useState([]); - const [windowWidth, setWindowWidth] = useState(window.innerWidth); - - const {data, setData} = useContext(Context) - // save to photo - const captureScreenshot = () => { - const input = document.getElementById('graph') as HTMLElement; - if (input) { - toast.success("Capturing screenshot") - html2canvas(input).then((canvas) => { - const imgData = canvas.toDataURL(); - const link = document.createElement('a'); - link.href = imgData; - link.download = 'graph-screenshot.png'; - link.click(); - }); - } - else { - toast.error("Could not capture screenshot", { - position: "top-center", - }) - } - - } - - const filterData = (data: GlobalDataType[], filter:"PROMOTED"|"GRADUATED"|null|string) => { - if (data){ - const f = filterPromGrad(data, filter) - // setFilteredData(f) - return f - } - else { - - }} + const initialTooltip: ToolTip = { display: false, text: '', x: 0, y: 0, fx: undefined, fy: undefined }; + const [tooltip, setTooltip] = useState(initialTooltip); + const [currentGraphData, setCurrentGraphData] = useState(graphData); + const forceGraphRef = useRef(null); // must be `any` because no typing is available for react-force-graph + const [orderByTime,] = useState(true); + const [pinnable,] = useState(false); + const [edgesThreshold, setEdgesThreshold] = useState((graphData.maxEdgeCount ?? 100) * 0.1); + const [previousEdgesThreshold, setPreviousEdgesThreshold] = useState(((graphData.maxEdgeCount ?? 100) * 0.1)); + const initialContextMenuControls: ContextMenuControls = { visible: false, x: 0, y: 0, node: null }; + + const [contextMenu, setContextMenu] = useState(initialContextMenuControls); + + const [, setRemovedNodeStorage] = useState([]); + const [windowWidth, setWindowWidth] = useState(window.innerWidth); + + const {data, setData} = useContext(Context) + // save to photo + const captureScreenshot = () => { + const input = document.getElementById('graph') as HTMLElement; + if (input) { + toast.success("Capturing screenshot") + html2canvas(input).then((canvas) => { + const imgData = canvas.toDataURL(); + const link = document.createElement('a'); + link.href = imgData; + link.download = 'graph-screenshot.png'; + link.click(); + }); + } + else { + toast.error("Could not capture screenshot", { + position: "top-center", + }) + } + + } + + const filterData = (data: GlobalDataType[], filter:"PROMOTED"|"GRADUATED"|null|string) => { + if (filter!=null){ + const f = filterPromGrad(data, filter) + // setFilteredData(f) + // setData(f) + // console.log(data) + setCurrentGraphData(processDataShopData(f)) + return f + }} const [isOn, setIsOn] = useState(false); const [isSwitchEnabled, setIsSwitchEnabled] = useState(false); @@ -75,13 +76,13 @@ export default function DirectedGraph({ graphData }: DirectedGraphProps) { // -- doesn't work if initial state is null const handleToggle = () => { - if (isSwitchEnabled){ - setIsOn(!isOn); - } + if (isSwitchEnabled){ + setIsOn(!isOn); + } }; const handleCheckboxChange = (event: React.ChangeEvent) => { - setIsSwitchEnabled(event.target.checked); + setIsSwitchEnabled(event.target.checked); }; // const handleFilterChange = (event: React.ChangeEvent) => { @@ -89,432 +90,435 @@ export default function DirectedGraph({ graphData }: DirectedGraphProps) { // }; useEffect(() => { - if (isSwitchEnabled) { - let value = isOn ? "PROMOTED" : "GRADUATED"; - setFilter(value); - // console.log("Filter: " + filter) - let f = filterData(data!, value) - console.log("Filter: " + value) + if (isSwitchEnabled) { + let value = isOn ? "PROMOTED" : "GRADUATED"; + setFilter(value); + // console.log("Filter: " + filter) + let f = filterData(data!, value) + console.log("Filter: " + value) + console.log(f) + // if (data) { + // const graphData: GraphData = processDataShopData(data) + // setGraphData(graphData) - console.log(f) - } + } - else{ - setData(data) - console.log(data) - } + else { + // setData(data) + console.log(data) + // }}, [data]) - }, [isSwitchEnabled, isOn]); + }}, [isSwitchEnabled, isOn]); const getValueBasedOnSwitch = () => { - if (isSwitchEnabled) { - return filter - }; + if (isSwitchEnabled) { + return filter + }; } - useEffect(() => { - // resizing of window - const handleResize = () => { - setWindowWidth(window.innerWidth); - }; - - window.addEventListener('resize', handleResize); - - return () => { - window.removeEventListener('resize', handleResize); - }; - }, []); - - - useEffect(() => { - // Ensure the forceGraphRef is current and the graph has been initialized - if (forceGraphRef.current) { - // Set the default zoom level, for example, to 1.5 - forceGraphRef.current.zoom(0.4, 500); // The second parameter (500) represents the duration of the zoom transition in milliseconds - } - }, []); - - // NODE PHYSICS - useEffect(() => { - if (forceGraphRef.current !== null) { - forceGraphRef.current.d3Force('collision', d3.forceCollide(() => (100)).strength(0.75)); - // ordering - if (orderByTime) { - forceGraphRef.current.d3Force('rank', d3.forceY((node: Node) => (node.rank ?? 0) * 100).strength(0.5)); - forceGraphRef.current.d3Force('x', d3.forceX(windowWidth / 2).strength(0.5)); - } - else { - forceGraphRef.current.d3Force('rank', null); - } - } - }, [currentGraphData, orderByTime, windowWidth]); - - - useEffect(() => { - const updateMousePosition = (event: { clientX: number; clientY: number; }) => { - if (tooltip.display) { - setTooltip({ ...tooltip, x: event.clientX, y: event.clientY }); - } - }; - window.addEventListener("mousemove", updateMousePosition); - - return () => { - window.removeEventListener("mousemove", updateMousePosition); - }; - }, [tooltip]); - - const handleNodeHover = (node: Node | null) => { - if (node) { - // setTooltip({ display: true, text: `${node.id}`, x: tooltip.x, y: tooltip.y, fx: node.fx, fy: node.fy }); - } else { - setTooltip((prev) => ({ ...prev, display: false })); - } - }; - - const handleLinkHover = (link: Link | null) => { - if (link) { - setTooltip({ display: true, text: `${link.numOfTransitions + " paths" ?? "no paths"}`, x: tooltip.x, y: tooltip.y, fx: undefined, fy: undefined }); - } - else { - setTooltip((prev) => ({ ...prev, display: false })); - } - - } - - const handleThresholdChangeRemove = () => { - const newLinks: LinkObject[] = changeLinkThreshold(graphData.links as LinkObject[], edgesThreshold); - const { newNodes, removedNodes } = removeUnusedNodes(currentGraphData.nodes, newLinks); - setRemovedNodeStorage(removedNodes); - setCurrentGraphData({ nodes: newNodes, links: newLinks }); - } - - const handleThresholdChangeAdd = () => { - const newLinks: LinkObject[] = changeLinkThreshold(graphData.links as LinkObject[], edgesThreshold); - const newNodes: Node[] = addUnusedNodes(currentGraphData.nodes, newLinks); - setCurrentGraphData({ nodes: newNodes, links: newLinks }); - } - - const handleThresholdChange = (chosenNum: number, previousValue: number) => { - if (chosenNum > previousValue) { - handleThresholdChangeRemove() - } - else { - handleThresholdChangeAdd() - } - } - - useEffect(() => { - // threshold logic - if ((edgesThreshold !== previousEdgesThreshold && forceGraphRef.current !== null) && edgesThreshold >= 0) { - if (edgesThreshold !== previousEdgesThreshold && forceGraphRef.current !== null) { - // forceGraphRef.current.pauseAnimation(); - handleThresholdChange(edgesThreshold, previousEdgesThreshold); - // forceGraphRef.current.resumeAnimation(); - } - } - }, [edgesThreshold]) - - useEffect(() => { - const timer = setTimeout(() => { - if (graphData && graphData.nodes.length > 0) { - const initialThreshold = (graphData.maxEdgeCount ?? 100) * 0.1; - setEdgesThreshold(initialThreshold); // Ensure the initial threshold is set correctly. - // Assuming handleThresholdChange is correctly implemented to handle the initial state - handleThresholdChange(initialThreshold, previousEdgesThreshold); - } - }, 200); // Delay execution by 500ms to ensure graphData is loaded before applying logic. - - // Cleanup function to clear the timeout if the component unmounts - return () => clearTimeout(timer); - }, [graphData]); // Depend on graphData to ensure it's loaded before applying logic. - - return ( - <> - - - { - contextMenu.visible && ( -
{ - // Prevent the browser context menu from appearing - e.preventDefault(); - }} - > -
- -
- Node Info -
- -
-
- Node ID: - {contextMenu.node?.id ?? "No ID"} -
- {contextMenu.node?.problemId && ( -
- Problem ID: {contextMenu.node.problemId} -
- - )} - {contextMenu.node?.selfLoops && ( -
- Self Loops: {contextMenu.node.selfLoops} -
- )} - {contextMenu.node?.cumulativeSelfLoops && ( -
- Cumulative Self Loops: {contextMenu.node.cumulativeSelfLoops} -
- )} - {contextMenu.node?.edgesIn && ( -
- Edges In: {contextMenu.node.edgesIn} -
- )} - {contextMenu.node?.edgesOut && ( -
- Edges Out: {contextMenu.node.edgesOut} -
- )} - - {contextMenu.node?.cumulativeEdgesOut && ( -
- Students leaving: {contextMenu.node.cumulativeEdgesOut} -
- )} - {contextMenu.node?.cumulativeEdgesIn && ( -
- Students entering: {contextMenu.node.cumulativeEdgesIn} -
- )} - {contextMenu.node?.rank && ( -
- Average Step Rank: {contextMenu.node.rank.toFixed(1)} -
- )} - {contextMenu.node?.times_errored && ( -
- Times Errored: {contextMenu.node.times_errored} -
- )} - {contextMenu.node?.rank && ( -
- Rank: {contextMenu.node.rank ?? ""} -
- )} - - -
- ) - } - - {tooltip.display && ( -
- {tooltip.text ?? ""} -
- )} - -
-
- -
- Edges Threshold (%): - { - const currentValue = edgesThreshold; - const newValue = (parseInt(e.target.value) / 100) * (graphData.maxEdgeCount ?? 100); - setEdgesThreshold(newValue) - setPreviousEdgesThreshold(currentValue); - }} - /> - - { - // Convert the percentage back to an absolute number for edgesThreshold - const newValue = (value[0] / 100) * (graphData.maxEdgeCount ?? 100); - setEdgesThreshold(newValue); - }} - onValueCommit={(value: number[]) => { - // This can be used for actions upon releasing the slider, similar to onValueChange - const newValue = (value[0] / 100) * (graphData.maxEdgeCount ?? 100); - setEdgesThreshold(newValue); - setPreviousEdgesThreshold(edgesThreshold); // Update previous threshold if needed - }} - /> - - - -
- -
- -
- - link.curvature || 0} - nodeAutoColorBy={"problemId"} - // cooldownTime={freezeNodes ? 0 : Infinity} // this controls the "steadiness" -- 1 is the most steady, infinity is the least - onNodeClick={(node, event) => { - setContextMenu({visible: true, x: event.clientX, y: event.clientY, node: node}); - }} - onNodeDragEnd={(node) => { - if (pinnable) { - if (node.border !== true) { - toast.success("Node pinned", { - icon: "📌", - position: "bottom-right", - }) - } - node.fx = node.x; - node.fy = node.y; - node.border = true; - } - - }} - onNodeRightClick={(node, event) => { - console.log("Node right clicked: ", node); - event.preventDefault(); - - }} - onLinkClick={(link) => { - console.log("Link clicked: ", link.source, link.target); - }} - onLinkRightClick={(link) => { - console.log("Link right clicked: ", link.source, link.target); - }} - nodeCanvasObject={(node, ctx, globalScale) => { - const label = node.label; - const fontSize = 14 / globalScale; - const radius = 14 / globalScale; - node.radius = radius; - ctx.beginPath(); - switch (node.shape) { - case 'circle': - ctx.arc(node.x!, node.y!, radius, 0, 2 * Math.PI, false); - break; - case 'triangle': - ctx.beginPath(); - ctx.moveTo(node.x!, node.y! - radius); - ctx.lineTo(node.x! + radius, node.y! + radius); - ctx.lineTo(node.x! - radius, node.y! + radius); - ctx.closePath(); - break; - case 'square': - ctx.rect(node.x! - radius, node.y! - radius, radius * 2, radius * 2); - break; - default: - ctx.arc(node.x!, node.y!, radius, 0, 2 * Math.PI, false); - - } - - ctx.fillStyle = node.color ?? 'blue'; - ctx.fill(); - - // border - if (node.border) { - ctx.lineWidth = 5 / globalScale; - ctx.strokeStyle = 'blue'; - ctx.stroke(); - } - - const labelOffsetY = radius + fontSize; - ctx.font = `${fontSize}px Sans-Serif`; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = 'black'; - ctx.fillText(label, node.x!, node.y! + labelOffsetY); - }} - linkDirectionalArrowLength={20} - // linkDirectionalParticles={2} - // linkDirectionalParticleWidth={7} - linkDirectionalArrowColor={(link) => link.color || ''} - linkDirectionalArrowRelPos={1} - linkWidth={link => link.width || 1} - d3VelocityDecay={0.8} - // d3AlphaDecay={0.05} - minZoom={0.4} - maxZoom={2} - /> -
-
- -
- - -

-

-

- - -
- - )} + useEffect(() => { + // resizing of window + const handleResize = () => { + setWindowWidth(window.innerWidth); + }; + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + + useEffect(() => { + // Ensure the forceGraphRef is current and the graph has been initialized + if (forceGraphRef.current) { + // Set the default zoom level, for example, to 1.5 + forceGraphRef.current.zoom(0.4, 500); // The second parameter (500) represents the duration of the zoom transition in milliseconds + } + }, []); + + // NODE PHYSICS + useEffect(() => { + if (forceGraphRef.current !== null) { + forceGraphRef.current.d3Force('collision', d3.forceCollide(() => (100)).strength(0.75)); + // ordering + if (orderByTime) { + forceGraphRef.current.d3Force('rank', d3.forceY((node: Node) => (node.rank ?? 0) * 100).strength(0.5)); + forceGraphRef.current.d3Force('x', d3.forceX(windowWidth / 2).strength(0.5)); + } + else { + forceGraphRef.current.d3Force('rank', null); + } + } + }, [currentGraphData, orderByTime, windowWidth]); + + + useEffect(() => { + const updateMousePosition = (event: { clientX: number; clientY: number; }) => { + if (tooltip.display) { + setTooltip({ ...tooltip, x: event.clientX, y: event.clientY }); + } + }; + window.addEventListener("mousemove", updateMousePosition); + + return () => { + window.removeEventListener("mousemove", updateMousePosition); + }; + }, [tooltip]); + + const handleNodeHover = (node: Node | null) => { + if (node) { + // setTooltip({ display: true, text: `${node.id}`, x: tooltip.x, y: tooltip.y, fx: node.fx, fy: node.fy }); + } else { + setTooltip((prev) => ({ ...prev, display: false })); + } + }; + + const handleLinkHover = (link: Link | null) => { + if (link) { + setTooltip({ display: true, text: `${link.numOfTransitions + " paths" ?? "no paths"}`, x: tooltip.x, y: tooltip.y, fx: undefined, fy: undefined }); + } + else { + setTooltip((prev) => ({ ...prev, display: false })); + } + + } + + const handleThresholdChangeRemove = () => { + const newLinks: LinkObject[] = changeLinkThreshold(graphData.links as LinkObject[], edgesThreshold); + const { newNodes, removedNodes } = removeUnusedNodes(currentGraphData.nodes, newLinks); + setRemovedNodeStorage(removedNodes); + setCurrentGraphData({ nodes: newNodes, links: newLinks }); + } + + const handleThresholdChangeAdd = () => { + const newLinks: LinkObject[] = changeLinkThreshold(graphData.links as LinkObject[], edgesThreshold); + const newNodes: Node[] = addUnusedNodes(currentGraphData.nodes, newLinks); + setCurrentGraphData({ nodes: newNodes, links: newLinks }); + } + + const handleThresholdChange = (chosenNum: number, previousValue: number) => { + if (chosenNum > previousValue) { + handleThresholdChangeRemove() + } + else { + handleThresholdChangeAdd() + } + } + + useEffect(() => { + // threshold logic + if ((edgesThreshold !== previousEdgesThreshold && forceGraphRef.current !== null) && edgesThreshold >= 0) { + if (edgesThreshold !== previousEdgesThreshold && forceGraphRef.current !== null) { + // forceGraphRef.current.pauseAnimation(); + handleThresholdChange(edgesThreshold, previousEdgesThreshold); + // forceGraphRef.current.resumeAnimation(); + } + } + }, [edgesThreshold]) + + useEffect(() => { + const timer = setTimeout(() => { + if (graphData && graphData.nodes.length > 0) { + const initialThreshold = (graphData.maxEdgeCount ?? 100) * 0.1; + setEdgesThreshold(initialThreshold); // Ensure the initial threshold is set correctly. + // Assuming handleThresholdChange is correctly implemented to handle the initial state + handleThresholdChange(initialThreshold, previousEdgesThreshold); + } + }, 200); // Delay execution by 500ms to ensure graphData is loaded before applying logic. + + // Cleanup function to clear the timeout if the component unmounts + return () => clearTimeout(timer); + }, [graphData]); // Depend on graphData to ensure it's loaded before applying logic. + + return ( + <> + + + { + contextMenu.visible && ( +
{ + // Prevent the browser context menu from appearing + e.preventDefault(); + }} + > +
+ +
+ Node Info +
+ +
+
+ Node ID: + {contextMenu.node?.id ?? "No ID"} +
+ {contextMenu.node?.problemId && ( +
+ Problem ID: {contextMenu.node.problemId} +
+ + )} + {contextMenu.node?.selfLoops && ( +
+ Self Loops: {contextMenu.node.selfLoops} +
+ )} + {contextMenu.node?.cumulativeSelfLoops && ( +
+ Cumulative Self Loops: {contextMenu.node.cumulativeSelfLoops} +
+ )} + {contextMenu.node?.edgesIn && ( +
+ Edges In: {contextMenu.node.edgesIn} +
+ )} + {contextMenu.node?.edgesOut && ( +
+ Edges Out: {contextMenu.node.edgesOut} +
+ )} + + {contextMenu.node?.cumulativeEdgesOut && ( +
+ Students leaving: {contextMenu.node.cumulativeEdgesOut} +
+ )} + {contextMenu.node?.cumulativeEdgesIn && ( +
+ Students entering: {contextMenu.node.cumulativeEdgesIn} +
+ )} + {contextMenu.node?.rank && ( +
+ Average Step Rank: {contextMenu.node.rank.toFixed(1)} +
+ )} + {contextMenu.node?.times_errored && ( +
+ Times Errored: {contextMenu.node.times_errored} +
+ )} + {contextMenu.node?.rank && ( +
+ Rank: {contextMenu.node.rank ?? ""} +
+ )} + + +
+ ) + } + + {tooltip.display && ( +
+ {tooltip.text ?? ""} +
+ )} + +
+
+ +
+ Edges Threshold (%): + { + const currentValue = edgesThreshold; + const newValue = (parseInt(e.target.value) / 100) * (graphData.maxEdgeCount ?? 100); + setEdgesThreshold(newValue) + setPreviousEdgesThreshold(currentValue); + }} + /> + + { + // Convert the percentage back to an absolute number for edgesThreshold + const newValue = (value[0] / 100) * (graphData.maxEdgeCount ?? 100); + setEdgesThreshold(newValue); + }} + onValueCommit={(value: number[]) => { + // This can be used for actions upon releasing the slider, similar to onValueChange + const newValue = (value[0] / 100) * (graphData.maxEdgeCount ?? 100); + setEdgesThreshold(newValue); + setPreviousEdgesThreshold(edgesThreshold); // Update previous threshold if needed + }} + /> + + + +
+ +
+ +
+ + link.curvature || 0} + nodeAutoColorBy={"problemId"} + // cooldownTime={freezeNodes ? 0 : Infinity} // this controls the "steadiness" -- 1 is the most steady, infinity is the least + onNodeClick={(node, event) => { + setContextMenu({visible: true, x: event.clientX, y: event.clientY, node: node}); + }} + onNodeDragEnd={(node) => { + if (pinnable) { + if (node.border !== true) { + toast.success("Node pinned", { + icon: "📌", + position: "bottom-right", + }) + } + node.fx = node.x; + node.fy = node.y; + node.border = true; + } + + }} + onNodeRightClick={(node, event) => { + console.log("Node right clicked: ", node); + event.preventDefault(); + + }} + onLinkClick={(link) => { + console.log("Link clicked: ", link.source, link.target); + }} + onLinkRightClick={(link) => { + console.log("Link right clicked: ", link.source, link.target); + }} + nodeCanvasObject={(node, ctx, globalScale) => { + const label = node.label; + const fontSize = 14 / globalScale; + const radius = 14 / globalScale; + node.radius = radius; + ctx.beginPath(); + switch (node.shape) { + case 'circle': + ctx.arc(node.x!, node.y!, radius, 0, 2 * Math.PI, false); + break; + case 'triangle': + ctx.beginPath(); + ctx.moveTo(node.x!, node.y! - radius); + ctx.lineTo(node.x! + radius, node.y! + radius); + ctx.lineTo(node.x! - radius, node.y! + radius); + ctx.closePath(); + break; + case 'square': + ctx.rect(node.x! - radius, node.y! - radius, radius * 2, radius * 2); + break; + default: + ctx.arc(node.x!, node.y!, radius, 0, 2 * Math.PI, false); + + } + + ctx.fillStyle = node.color ?? 'blue'; + ctx.fill(); + + // border + if (node.border) { + ctx.lineWidth = 5 / globalScale; + ctx.strokeStyle = 'blue'; + ctx.stroke(); + } + + const labelOffsetY = radius + fontSize; + ctx.font = `${fontSize}px Sans-Serif`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = 'black'; + ctx.fillText(label, node.x!, node.y! + labelOffsetY); + }} + linkDirectionalArrowLength={20} + // linkDirectionalParticles={2} + // linkDirectionalParticleWidth={7} + linkDirectionalArrowColor={(link) => link.color || ''} + linkDirectionalArrowRelPos={1} + linkWidth={link => link.width || 1} + d3VelocityDecay={0.8} + // d3AlphaDecay={0.05} + minZoom={0.4} + maxZoom={2} + /> +
+
+ +
+ + +

+

+

+ + +
+ + )} diff --git a/src/lib/dataProcessingUtils.ts b/src/lib/dataProcessingUtils.ts index dbadc9d..d50b229 100644 --- a/src/lib/dataProcessingUtils.ts +++ b/src/lib/dataProcessingUtils.ts @@ -18,7 +18,7 @@ export function processDataShopData(data: GlobalDataType[]): GraphData{ ----- NOTES ----- The data is already sorted by Time **within each anon student id** so we can use this to calculate both the rank and the next node in the sequence. The data has a problem name and then each step name ordered sequentially. The graph terminates when the step name is `null` and Selection = 'Done Button' - So we can filter the data by Problem Name and then group by student in order to contstruct the paths. Then we overlay all of them to get the full graph. + So we can filter the data by Problem Name and then group by student in order to construct the paths. Then we overlay all of them to get the full graph. We can count the instances of each transition to get the width of the links. Right now, the sample data is from one specific problem, so the eventual API will have to fetch data based on the problem name rather than getting the whole dataset From 1b029e256a6fc4dfcf8c42a4ca371d0462d7e768 Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Mon, 1 Jul 2024 15:26:29 -0500 Subject: [PATCH 13/54] IT WORKS!! --- src/App.tsx | 135 +++-- src/components/DirectedGraph.tsx | 893 +++++++++++++++++-------------- src/lib/dataProcessingUtils.ts | 2 +- 3 files changed, 566 insertions(+), 464 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 870eeab..ef5ba81 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,7 +14,7 @@ import './Switch.css'; function App() { - const { resetData, setGraphData, setLoading, setFilteredData, filteredData, + const { resetData, setGraphData, setLoading, //setFilteredData, filteredData, data, setData, graphData, loading } = useContext(Context) const [showDropZone, setShowDropZone] = useState(true) @@ -53,59 +53,57 @@ function App() { setGraphData(graphData) }}, [data]) - useEffect(() => { - if (filteredData) { - const graphData: GraphData = processDataShopData(filteredData) - setGraphData(graphData) - }}, [filteredData]) + // useEffect(() => { + // if (filteredData) { + // const graphData: GraphData = processDataShopData(filteredData) + // setGraphData(graphData) + // }}, [filteredData]) - const [isOn, setIsOn] = useState(false); - const [isSwitchEnabled, setIsSwitchEnabled] = useState(false); - const [filter, setFilter] = useState<"PROMOTED"|"GRADUATED"|null|string>("GRADUATED"); + // const [isOn, setIsOn] = useState(false); + // const [isSwitchEnabled, setIsSwitchEnabled] = useState(false); + // const [filter, setFilter] = useState<"PROMOTED"|"GRADUATED"|null|string>("GRADUATED"); // Using Graduated as initial gives the first click the correct value but not after // -- doesn't work if initial state is null - const handleToggle = () => { - if (isSwitchEnabled){ - setIsOn(!isOn); - } - }; - - const handleCheckboxChange = (event: React.ChangeEvent) => { - setIsSwitchEnabled(event.target.checked); - }; + // const handleToggle = () => { + // if (isSwitchEnabled){ + // setIsOn(!isOn); + // } + // }; + // + // const handleCheckboxChange = (event: React.ChangeEvent) => { + // setIsSwitchEnabled(event.target.checked); + // }; // const handleFilterChange = (event: React.ChangeEvent) => { // setFilter(event.target.value); // }; - useEffect(() => { - if (isSwitchEnabled) { - let value = isOn ? "PROMOTED" : "GRADUATED"; - setFilter(value); - // console.log("Filter: " + filter) - let f = filterData(data!, value) // Why can't I use filter here instead of value? - - // console.log("Filter: " + filter) - - console.log("Filter: " + value) - - console.log(f) - } - - else{ - setData(data) - console.log(data) - } - - - }, [isSwitchEnabled, isOn]); - - const getValueBasedOnSwitch = () => { - if (isSwitchEnabled) { - return filter - }; - } + // useEffect(() => { + // if (isSwitchEnabled) { + // let value = isOn ? "PROMOTED" : "GRADUATED"; + // setFilter(value); + // // console.log("Filter: " + filter) + // let f = filterData(data!, value) // Why can't I use filter here instead of value? + // + // console.log("Filter: " + value) + // + // console.log(f) + // } + // + // else{ + // setData(data!) + // console.log(data) + // } + // + // + // }, [isSwitchEnabled, isOn]); + // + // const getValueBasedOnSwitch = () => { + // if (isSwitchEnabled) { + // return filter + // }; + // } return ( <> @@ -153,34 +151,25 @@ function App() { ) } -
- - -

-

-

- - -
- - {/*
*/} - {/*
*/} - {/* {*/} - {/* filterData(data!);*/} - {/* setFilteredData(data);*/} - {/* // handleFilteredData(filteredData!)*/} - {/* }*/} - {/* }> Filter Promoted Data */} - {/*
*/} + + + {/*
*/} + {/* */} + {/* */} + {/*

*/} + {/*

*/} + {/*

*/} + + {/* */} + {/*
*/} +
diff --git a/src/components/DirectedGraph.tsx b/src/components/DirectedGraph.tsx index dd2919f..f0faea3 100644 --- a/src/components/DirectedGraph.tsx +++ b/src/components/DirectedGraph.tsx @@ -7,344 +7,432 @@ import html2canvas from 'html2canvas'; import toast from "react-hot-toast"; import { Cross2Icon } from "@radix-ui/react-icons"; import { Slider } from "@/components/ui/slider" -import { addUnusedNodes, changeLinkThreshold, removeUnusedNodes } from "@/lib/dataProcessingUtils"; +import {addUnusedNodes, changeLinkThreshold, processDataShopData, removeUnusedNodes} from "@/lib/dataProcessingUtils"; import ToggleTool from "@/components/ToggleTool"; +import {filterPromGrad} from "@/lib/GradPromUtils"; +import Switch from "@/components/ui/switch"; +import "./../Switch.css" +import {Context} from "@/Context.tsx"; // TODO make sure the node info is changed interface DirectedGraphProps { - graphData: GraphData; + graphData: GraphData; } export default function DirectedGraph({ graphData }: DirectedGraphProps) { - const initialTooltip: ToolTip = { display: false, text: '', x: 0, y: 0, fx: undefined, fy: undefined }; - const [tooltip, setTooltip] = useState(initialTooltip); - const [currentGraphData, setCurrentGraphData] = useState(graphData); - const forceGraphRef = useRef(null); // must be `any` because no typing is available for react-force-graph - const [orderByTime,] = useState(true); - const [pinnable,] = useState(false); - const [edgesThreshold, setEdgesThreshold] = useState((graphData.maxEdgeCount ?? 100) * 0.1); - const [previousEdgesThreshold, setPreviousEdgesThreshold] = useState(((graphData.maxEdgeCount ?? 100) * 0.1)); - const initialContextMenuControls: ContextMenuControls = { visible: false, x: 0, y: 0, node: null }; + const initialTooltip: ToolTip = { display: false, text: '', x: 0, y: 0, fx: undefined, fy: undefined }; + const [tooltip, setTooltip] = useState(initialTooltip); + const [currentGraphData, setCurrentGraphData] = useState(graphData); + const forceGraphRef = useRef(null); // must be `any` because no typing is available for react-force-graph + const [orderByTime,] = useState(true); + const [pinnable,] = useState(false); + const [edgesThreshold, setEdgesThreshold] = useState((graphData.maxEdgeCount ?? 100) * 0.1); + const [previousEdgesThreshold, setPreviousEdgesThreshold] = useState(((graphData.maxEdgeCount ?? 100) * 0.1)); + const initialContextMenuControls: ContextMenuControls = { visible: false, x: 0, y: 0, node: null }; - const [contextMenu, setContextMenu] = useState(initialContextMenuControls); + const [contextMenu, setContextMenu] = useState(initialContextMenuControls); - const [, setRemovedNodeStorage] = useState([]); - const [windowWidth, setWindowWidth] = useState(window.innerWidth); + const [, setRemovedNodeStorage] = useState([]); + const [windowWidth, setWindowWidth] = useState(window.innerWidth); - // save to photo - const captureScreenshot = () => { - const input = document.getElementById('graph') as HTMLElement; - if (input) { - toast.success("Capturing screenshot") - html2canvas(input).then((canvas) => { - const imgData = canvas.toDataURL(); - const link = document.createElement('a'); - link.href = imgData; - link.download = 'graph-screenshot.png'; - link.click(); - }); - } - else { - toast.error("Could not capture screenshot", { - position: "top-center", - }) - } - - } - - - useEffect(() => { - // resizing of window - const handleResize = () => { - setWindowWidth(window.innerWidth); - }; - - window.addEventListener('resize', handleResize); - - return () => { - window.removeEventListener('resize', handleResize); - }; - }, []); - - - useEffect(() => { - // Ensure the forceGraphRef is current and the graph has been initialized - if (forceGraphRef.current) { - // Set the default zoom level, for example, to 1.5 - forceGraphRef.current.zoom(0.4, 500); // The second parameter (500) represents the duration of the zoom transition in milliseconds - } - }, []); - - // NODE PHYSICS - useEffect(() => { - if (forceGraphRef.current !== null) { - forceGraphRef.current.d3Force('collision', d3.forceCollide(() => (100)).strength(0.75)); - // ordering - if (orderByTime) { - forceGraphRef.current.d3Force('rank', d3.forceY((node: Node) => (node.rank ?? 0) * 100).strength(0.5)); - forceGraphRef.current.d3Force('x', d3.forceX(windowWidth / 2).strength(0.5)); - } - else { - forceGraphRef.current.d3Force('rank', null); - } - } - }, [currentGraphData, orderByTime, windowWidth]); - - - useEffect(() => { - const updateMousePosition = (event: { clientX: number; clientY: number; }) => { - if (tooltip.display) { - setTooltip({ ...tooltip, x: event.clientX, y: event.clientY }); - } - }; - window.addEventListener("mousemove", updateMousePosition); - - return () => { - window.removeEventListener("mousemove", updateMousePosition); - }; - }, [tooltip]); - - const handleNodeHover = (node: Node | null) => { - if (node) { - // setTooltip({ display: true, text: `${node.id}`, x: tooltip.x, y: tooltip.y, fx: node.fx, fy: node.fy }); - } else { - setTooltip((prev) => ({ ...prev, display: false })); - } - }; - - const handleLinkHover = (link: Link | null) => { - if (link) { - setTooltip({ display: true, text: `${link.numOfTransitions + " paths" ?? "no paths"}`, x: tooltip.x, y: tooltip.y, fx: undefined, fy: undefined }); - } - else { - setTooltip((prev) => ({ ...prev, display: false })); - } - - } - - const handleThresholdChangeRemove = () => { - const newLinks: LinkObject[] = changeLinkThreshold(graphData.links as LinkObject[], edgesThreshold); - const { newNodes, removedNodes } = removeUnusedNodes(currentGraphData.nodes, newLinks); - setRemovedNodeStorage(removedNodes); - setCurrentGraphData({ nodes: newNodes, links: newLinks }); - } - - const handleThresholdChangeAdd = () => { - const newLinks: LinkObject[] = changeLinkThreshold(graphData.links as LinkObject[], edgesThreshold); - const newNodes: Node[] = addUnusedNodes(currentGraphData.nodes, newLinks); - setCurrentGraphData({ nodes: newNodes, links: newLinks }); - } - - const handleThresholdChange = (chosenNum: number, previousValue: number) => { - if (chosenNum > previousValue) { - handleThresholdChangeRemove() - } - else { - handleThresholdChangeAdd() - } - } - - useEffect(() => { - // threshold logic - if ((edgesThreshold !== previousEdgesThreshold && forceGraphRef.current !== null) && edgesThreshold >= 0) { - if (edgesThreshold !== previousEdgesThreshold && forceGraphRef.current !== null) { - // forceGraphRef.current.pauseAnimation(); - handleThresholdChange(edgesThreshold, previousEdgesThreshold); - // forceGraphRef.current.resumeAnimation(); - } - } - }, [edgesThreshold]) - - useEffect(() => { - const timer = setTimeout(() => { - if (graphData && graphData.nodes.length > 0) { - const initialThreshold = (graphData.maxEdgeCount ?? 100) * 0.1; - setEdgesThreshold(initialThreshold); // Ensure the initial threshold is set correctly. - // Assuming handleThresholdChange is correctly implemented to handle the initial state - handleThresholdChange(initialThreshold, previousEdgesThreshold); - } - }, 200); // Delay execution by 500ms to ensure graphData is loaded before applying logic. - - // Cleanup function to clear the timeout if the component unmounts - return () => clearTimeout(timer); - }, [graphData]); // Depend on graphData to ensure it's loaded before applying logic. - - return ( - <> - - - - { - contextMenu.visible && ( -
{ - // Prevent the browser context menu from appearing - e.preventDefault(); - }} - > -
- -
- Node Info -
- -
-
- Node ID: - {contextMenu.node?.id ?? "No ID"} -
- {contextMenu.node?.problemId && ( -
- Problem ID: {contextMenu.node.problemId} -
- - )} - {contextMenu.node?.selfLoops && ( -
- Self Loops: {contextMenu.node.selfLoops} -
- )} - {contextMenu.node?.cumulativeSelfLoops && ( -
- Cumulative Self Loops: {contextMenu.node.cumulativeSelfLoops} -
- )} - {contextMenu.node?.edgesIn && ( -
- Edges In: {contextMenu.node.edgesIn} -
- )} - {contextMenu.node?.edgesOut && ( -
- Edges Out: {contextMenu.node.edgesOut} -
- )} - - {contextMenu.node?.cumulativeEdgesOut && ( -
- Students leaving: {contextMenu.node.cumulativeEdgesOut} -
- )} - {contextMenu.node?.cumulativeEdgesIn && ( -
- Students entering: {contextMenu.node.cumulativeEdgesIn} -
- )} - {contextMenu.node?.rank && ( -
- Average Step Rank: {contextMenu.node.rank.toFixed(1)} -
- )} - {contextMenu.node?.times_errored && ( -
- Times Errored: {contextMenu.node.times_errored} -
- )} - {contextMenu.node?.rank && ( -
- Rank: {contextMenu.node.rank ?? ""} -
- )} - - - - -
- ) - } - - {tooltip.display && ( -
- {tooltip.text ?? ""} -
- )} - -
-
- -
- Edges Threshold (%): - { - const currentValue = edgesThreshold; - const newValue = (parseInt(e.target.value) / 100) * (graphData.maxEdgeCount ?? 100); - setEdgesThreshold(newValue) - setPreviousEdgesThreshold(currentValue); - }} - /> - - { - // Convert the percentage back to an absolute number for edgesThreshold - const newValue = (value[0] / 100) * (graphData.maxEdgeCount ?? 100); - setEdgesThreshold(newValue); - }} - onValueCommit={(value: number[]) => { - // This can be used for actions upon releasing the slider, similar to onValueChange - const newValue = (value[0] / 100) * (graphData.maxEdgeCount ?? 100); - setEdgesThreshold(newValue); - setPreviousEdgesThreshold(edgesThreshold); // Update previous threshold if needed - }} - /> - - - -
- -
- -
+ const {data, setData} = useContext(Context) + // save to photo + const captureScreenshot = () => { + const input = document.getElementById('graph') as HTMLElement; + if (input) { + toast.success("Capturing screenshot") + html2canvas(input).then((canvas) => { + const imgData = canvas.toDataURL(); + const link = document.createElement('a'); + link.href = imgData; + link.download = 'graph-screenshot.png'; + link.click(); + }); + } + else { + toast.error("Could not capture screenshot", { + position: "top-center", + }) + } + + } + + const filterData = (data: GlobalDataType[], filter:"PROMOTED"|"GRADUATED"|null|string) => { + if (filter!=null){ + const f = filterPromGrad(data, filter) + // setFilteredData(f) + // setData(f) + // console.log(data) + setCurrentGraphData(processDataShopData(f)) + return f + }} + + const [isOn, setIsOn] = useState(false); + const [isSwitchEnabled, setIsSwitchEnabled] = useState(false); + const [filter, setFilter] = useState<"PROMOTED"|"GRADUATED"|null|string>("GRADUATED"); + // Using Graduated as initial gives the first click the correct value but not after + // -- doesn't work if initial state is null + + const handleToggle = () => { + if (isSwitchEnabled){ + setIsOn(!isOn); + } + }; + + const handleCheckboxChange = (event: React.ChangeEvent) => { + setIsSwitchEnabled(event.target.checked); + }; + + // const handleFilterChange = (event: React.ChangeEvent) => { + // setFilter(event.target.value); + // }; + + useEffect(() => { + if (isSwitchEnabled) { + let value = isOn ? "PROMOTED" : "GRADUATED"; + setFilter(value); + // console.log("Filter: " + filter) + let f = filterData(data!, value) + console.log("Filter: " + value) + console.log(f) + // if (data) { + // const graphData: GraphData = processDataShopData(data) + // setGraphData(graphData) + + } + + else { + // setData(data) + console.log(data) + // }}, [data]) + + + }}, [isSwitchEnabled, isOn]); + + const getValueBasedOnSwitch = () => { + if (isSwitchEnabled) { + return filter + }; + } + + useEffect(() => { + // resizing of window + const handleResize = () => { + setWindowWidth(window.innerWidth); + }; + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + + useEffect(() => { + // Ensure the forceGraphRef is current and the graph has been initialized + if (forceGraphRef.current) { + // Set the default zoom level, for example, to 1.5 + forceGraphRef.current.zoom(0.4, 500); // The second parameter (500) represents the duration of the zoom transition in milliseconds + } + }, []); + + // NODE PHYSICS + useEffect(() => { + if (forceGraphRef.current !== null) { + forceGraphRef.current.d3Force('collision', d3.forceCollide(() => (100)).strength(0.75)); + // ordering + if (orderByTime) { + forceGraphRef.current.d3Force('rank', d3.forceY((node: Node) => (node.rank ?? 0) * 100).strength(0.5)); + forceGraphRef.current.d3Force('x', d3.forceX(windowWidth / 2).strength(0.5)); + } + else { + forceGraphRef.current.d3Force('rank', null); + } + } + }, [currentGraphData, orderByTime, windowWidth]); + + + useEffect(() => { + const updateMousePosition = (event: { clientX: number; clientY: number; }) => { + if (tooltip.display) { + setTooltip({ ...tooltip, x: event.clientX, y: event.clientY }); + } + }; + window.addEventListener("mousemove", updateMousePosition); + + return () => { + window.removeEventListener("mousemove", updateMousePosition); + }; + }, [tooltip]); + + const handleNodeHover = (node: Node | null) => { + if (node) { + // setTooltip({ display: true, text: `${node.id}`, x: tooltip.x, y: tooltip.y, fx: node.fx, fy: node.fy }); + } else { + setTooltip((prev) => ({ ...prev, display: false })); + } + }; + + const handleLinkHover = (link: Link | null) => { + if (link) { + setTooltip({ display: true, text: `${link.numOfTransitions + " paths" ?? "no paths"}`, x: tooltip.x, y: tooltip.y, fx: undefined, fy: undefined }); + } + else { + setTooltip((prev) => ({ ...prev, display: false })); + } + + } + + const handleThresholdChangeRemove = () => { + const newLinks: LinkObject[] = changeLinkThreshold(graphData.links as LinkObject[], edgesThreshold); + const { newNodes, removedNodes } = removeUnusedNodes(currentGraphData.nodes, newLinks); + setRemovedNodeStorage(removedNodes); + setCurrentGraphData({ nodes: newNodes, links: newLinks }); + } + + const handleThresholdChangeAdd = () => { + const newLinks: LinkObject[] = changeLinkThreshold(graphData.links as LinkObject[], edgesThreshold); + const newNodes: Node[] = addUnusedNodes(currentGraphData.nodes, newLinks); + setCurrentGraphData({ nodes: newNodes, links: newLinks }); + } + + const handleThresholdChange = (chosenNum: number, previousValue: number) => { + if (chosenNum > previousValue) { + handleThresholdChangeRemove() + } + else { + handleThresholdChangeAdd() + } + } + + useEffect(() => { + // threshold logic + if ((edgesThreshold !== previousEdgesThreshold && forceGraphRef.current !== null) && edgesThreshold >= 0) { + if (edgesThreshold !== previousEdgesThreshold && forceGraphRef.current !== null) { + // forceGraphRef.current.pauseAnimation(); + handleThresholdChange(edgesThreshold, previousEdgesThreshold); + // forceGraphRef.current.resumeAnimation(); + } + } + }, [edgesThreshold]) + + useEffect(() => { + const timer = setTimeout(() => { + if (graphData && graphData.nodes.length > 0) { + const initialThreshold = (graphData.maxEdgeCount ?? 100) * 0.1; + setEdgesThreshold(initialThreshold); // Ensure the initial threshold is set correctly. + // Assuming handleThresholdChange is correctly implemented to handle the initial state + handleThresholdChange(initialThreshold, previousEdgesThreshold); + } + }, 200); // Delay execution by 500ms to ensure graphData is loaded before applying logic. + + // Cleanup function to clear the timeout if the component unmounts + return () => clearTimeout(timer); + }, [graphData]); // Depend on graphData to ensure it's loaded before applying logic. + + return ( + <> + + + { + contextMenu.visible && ( +
{ + // Prevent the browser context menu from appearing + e.preventDefault(); + }} + > +
+ +
+ Node Info +
+ +
+
+ Node ID: + {contextMenu.node?.id ?? "No ID"} +
+ {contextMenu.node?.problemId && ( +
+ Problem ID: {contextMenu.node.problemId} +
+ + )} + {contextMenu.node?.selfLoops && ( +
+ Self Loops: {contextMenu.node.selfLoops} +
+ )} + {contextMenu.node?.cumulativeSelfLoops && ( +
+ Cumulative Self Loops: {contextMenu.node.cumulativeSelfLoops} +
+ )} + {contextMenu.node?.edgesIn && ( +
+ Edges In: {contextMenu.node.edgesIn} +
+ )} + {contextMenu.node?.edgesOut && ( +
+ Edges Out: {contextMenu.node.edgesOut} +
+ )} + + {contextMenu.node?.cumulativeEdgesOut && ( +
+ Students leaving: {contextMenu.node.cumulativeEdgesOut} +
+ )} + {contextMenu.node?.cumulativeEdgesIn && ( +
+ Students entering: {contextMenu.node.cumulativeEdgesIn} +
+ )} + {contextMenu.node?.rank && ( +
+ Average Step Rank: {contextMenu.node.rank.toFixed(1)} +
+ )} + {contextMenu.node?.times_errored && ( +
+ Times Errored: {contextMenu.node.times_errored} +
+ )} + {contextMenu.node?.rank && ( +
+ Rank: {contextMenu.node.rank ?? ""} +
+ )} + + +
+ ) + } + + {tooltip.display && ( +
+ {tooltip.text ?? ""} +
+ )} + +
+
+ +
+ Edges Threshold (%): + { + const currentValue = edgesThreshold; + const newValue = (parseInt(e.target.value) / 100) * (graphData.maxEdgeCount ?? 100); + setEdgesThreshold(newValue) + setPreviousEdgesThreshold(currentValue); + }} + /> + + { + // Convert the percentage back to an absolute number for edgesThreshold + const newValue = (value[0] / 100) * (graphData.maxEdgeCount ?? 100); + setEdgesThreshold(newValue); + }} + onValueCommit={(value: number[]) => { + // This can be used for actions upon releasing the slider, similar to onValueChange + const newValue = (value[0] / 100) * (graphData.maxEdgeCount ?? 100); + setEdgesThreshold(newValue); + setPreviousEdgesThreshold(edgesThreshold); // Update previous threshold if needed + }} + /> + + + +
+ +
+ +
+ + link.curvature || 0} + nodeAutoColorBy={"problemId"} + // cooldownTime={freezeNodes ? 0 : Infinity} // this controls the "steadiness" -- 1 is the most steady, infinity is the least + onNodeClick={(node, event) => { + setContextMenu({visible: true, x: event.clientX, y: event.clientY, node: node}); + }} + onNodeDragEnd={(node) => { + if (pinnable) { + if (node.border !== true) { + toast.success("Node pinned", { + icon: "📌", + position: "bottom-right", + }) + } + node.fx = node.x; + node.fy = node.y; + node.border = true; + } { - console.log("Node right clicked: ", node); - event.preventDefault(); + }} + onNodeRightClick={(node, event) => { + console.log("Node right clicked: ", node); + event.preventDefault(); + + }} + onLinkClick={(link) => { + console.log("Link clicked: ", link.source, link.target); + }} + onLinkRightClick={(link) => { + console.log("Link right clicked: ", link.source, link.target); + }} + nodeCanvasObject={(node, ctx, globalScale) => { + const label = node.label; + const fontSize = 14 / globalScale; + const radius = 14 / globalScale; + node.radius = radius; + ctx.beginPath(); + switch (node.shape) { + case 'circle': + ctx.arc(node.x!, node.y!, radius, 0, 2 * Math.PI, false); + break; + case 'triangle': + ctx.beginPath(); + ctx.moveTo(node.x!, node.y! - radius); + ctx.lineTo(node.x! + radius, node.y! + radius); + ctx.lineTo(node.x! - radius, node.y! + radius); + ctx.closePath(); + break; + case 'square': + ctx.rect(node.x! - radius, node.y! - radius, radius * 2, radius * 2); + break; + default: + ctx.arc(node.x!, node.y!, radius, 0, 2 * Math.PI, false); + + } + + ctx.fillStyle = node.color ?? 'blue'; + ctx.fill(); + + // border + if (node.border) { + ctx.lineWidth = 5 / globalScale; + ctx.strokeStyle = 'blue'; + ctx.stroke(); + } + + const labelOffsetY = radius + fontSize; + ctx.font = `${fontSize}px Sans-Serif`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = 'black'; + ctx.fillText(label, node.x!, node.y! + labelOffsetY); + }} + linkDirectionalArrowLength={20} + // linkDirectionalParticles={2} + // linkDirectionalParticleWidth={7} + linkDirectionalArrowColor={(link) => link.color || ''} + linkDirectionalArrowRelPos={1} + linkWidth={link => link.width || 1} + d3VelocityDecay={0.8} + // d3AlphaDecay={0.05} + minZoom={0.4} + maxZoom={2} + /> +
+
+ +
+ + +

+

+

+ + +
+ + )} + + + + - }} - onLinkClick={(link) => { - console.log("Link clicked: ", link.source, link.target); - }} - onLinkRightClick={(link) => { - console.log("Link right clicked: ", link.source, link.target); - }} - nodeCanvasObject={(node, ctx, globalScale) => { - const label = node.label; - const fontSize = 14 / globalScale; - const radius = 14 / globalScale; - node.radius = radius; - ctx.beginPath(); - switch (node.shape) { - case 'circle': - ctx.arc(node.x!, node.y!, radius, 0, 2 * Math.PI, false); - break; - case 'triangle': - ctx.beginPath(); - ctx.moveTo(node.x!, node.y! - radius); - ctx.lineTo(node.x! + radius, node.y! + radius); - ctx.lineTo(node.x! - radius, node.y! + radius); - ctx.closePath(); - break; - case 'square': - ctx.rect(node.x! - radius, node.y! - radius, radius * 2, radius * 2); - break; - default: - ctx.arc(node.x!, node.y!, radius, 0, 2 * Math.PI, false); - } - ctx.fillStyle = node.color ?? 'blue'; - ctx.fill(); - // border - if (node.border) { - ctx.lineWidth = 5 / globalScale; - ctx.strokeStyle = 'blue'; - ctx.stroke(); - } - const labelOffsetY = radius + fontSize; - ctx.font = `${fontSize}px Sans-Serif`; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = 'black'; - ctx.fillText(label, node.x!, node.y! + labelOffsetY); - }} - linkDirectionalArrowLength={20} - // linkDirectionalParticles={2} - // linkDirectionalParticleWidth={7} - linkDirectionalArrowColor={(link) => link.color || ''} - linkDirectionalArrowRelPos={1} - linkWidth={link => link.width || 1} - d3VelocityDecay={0.8} - // d3AlphaDecay={0.05} - minZoom={0.4} - maxZoom={2} - /> -
-
- - ) -} \ No newline at end of file diff --git a/src/lib/dataProcessingUtils.ts b/src/lib/dataProcessingUtils.ts index dbadc9d..d50b229 100644 --- a/src/lib/dataProcessingUtils.ts +++ b/src/lib/dataProcessingUtils.ts @@ -18,7 +18,7 @@ export function processDataShopData(data: GlobalDataType[]): GraphData{ ----- NOTES ----- The data is already sorted by Time **within each anon student id** so we can use this to calculate both the rank and the next node in the sequence. The data has a problem name and then each step name ordered sequentially. The graph terminates when the step name is `null` and Selection = 'Done Button' - So we can filter the data by Problem Name and then group by student in order to contstruct the paths. Then we overlay all of them to get the full graph. + So we can filter the data by Problem Name and then group by student in order to construct the paths. Then we overlay all of them to get the full graph. We can count the instances of each transition to get the width of the links. Right now, the sample data is from one specific problem, so the eventual API will have to fetch data based on the problem name rather than getting the whole dataset From d42a0c006ea9f2df6bcf1aaefde6a020c6bc126a Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Tue, 2 Jul 2024 09:35:55 -0500 Subject: [PATCH 14/54] change one variable name --- src/components/DirectedGraph.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/DirectedGraph.tsx b/src/components/DirectedGraph.tsx index dfbae79..bd08b9d 100644 --- a/src/components/DirectedGraph.tsx +++ b/src/components/DirectedGraph.tsx @@ -61,12 +61,12 @@ export default function DirectedGraph({ graphData }: DirectedGraphProps) { const filterData = (data: GlobalDataType[], filter:"PROMOTED"|"GRADUATED"|null|string) => { if (filter!=null){ - const f = filterPromGrad(data, filter) + const filteredData = filterPromGrad(data, filter) // setFilteredData(f) // setData(f) // console.log(data) - setCurrentGraphData(processDataShopData(f)) - return f + setCurrentGraphData(processDataShopData(filteredData)) + return filteredData }} const [isOn, setIsOn] = useState(false); @@ -92,7 +92,7 @@ export default function DirectedGraph({ graphData }: DirectedGraphProps) { useEffect(() => { if (isSwitchEnabled) { let value = isOn ? "PROMOTED" : "GRADUATED"; - setFilter(value); + // setFilter(value); // console.log("Filter: " + filter) let f = filterData(data!, value) console.log("Filter: " + value) From d64abba61c4f6ba78fcfcb120036d4cd75d76eb9 Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Tue, 9 Jul 2024 12:46:44 -0500 Subject: [PATCH 15/54] Switch back data if filtering off --- src/components/DirectedGraph.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/DirectedGraph.tsx b/src/components/DirectedGraph.tsx index bd08b9d..5bf8890 100644 --- a/src/components/DirectedGraph.tsx +++ b/src/components/DirectedGraph.tsx @@ -98,14 +98,14 @@ export default function DirectedGraph({ graphData }: DirectedGraphProps) { console.log("Filter: " + value) console.log(f) // if (data) { - // const graphData: GraphData = processDataShopData(data) - // setGraphData(graphData) + const graphData: GraphData = processDataShopData(f!) + setCurrentGraphData(graphData) } else { - // setData(data) - console.log(data) + const graphData: GraphData = processDataShopData(data!) + setCurrentGraphData(graphData) // }}, [data]) From 8ebfc886d065172ff27dafd6afb89ceac8d14838 Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:59:24 -0500 Subject: [PATCH 16/54] label change corrected --- src/components/DirectedGraph.tsx | 4 ++-- src/lib/types.ts | 4 +++- src/lib/utils.ts | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/DirectedGraph.tsx b/src/components/DirectedGraph.tsx index 5bf8890..5477e48 100644 --- a/src/components/DirectedGraph.tsx +++ b/src/components/DirectedGraph.tsx @@ -92,11 +92,11 @@ export default function DirectedGraph({ graphData }: DirectedGraphProps) { useEffect(() => { if (isSwitchEnabled) { let value = isOn ? "PROMOTED" : "GRADUATED"; - // setFilter(value); + setFilter(value); // console.log("Filter: " + filter) let f = filterData(data!, value) console.log("Filter: " + value) - console.log(f) + // console.log(f) // if (data) { const graphData: GraphData = processDataShopData(f!) setCurrentGraphData(graphData) diff --git a/src/lib/types.ts b/src/lib/types.ts index a2116c7..4233d0b 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -20,7 +20,9 @@ export type DataSet1 = { "Step Name": string | null; "Attempt At Step": number; "Is Last Attempt": boolean | null; - Outcome: "OK" | "BUG" | "INITIAL_HINT" | "HINT_LEVEL_CHANGE" | "ERROR"; + Outcome: "OK" | "JIT" | "ERROR" | "INITIAL_HINT" | "HINT_LEVEL_CHANGE" | "FREEBIE_JIT" + //"OK" | "BUG" | "INITIAL_HINT" | "HINT_LEVEL_CHANGE" | "ERROR"; + Selection: "Done Button" | null; Action: "Attempt" | "Done" | "Hint Request" | "Hint Level Change"; Input: string | null; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 67db809..689f0b0 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -12,7 +12,9 @@ const validation = Joi.array().items( Joi.object({ 'Problem Name': Joi.string().required(), 'Step Name': Joi.string().allow('', null).required(), - 'Outcome': Joi.string().valid('OK', 'BUG', 'INITIAL_HINT', 'HINT_LEVEL_CHANGE', 'ERROR').required(), + 'Outcome': Joi.string().valid('OK', 'JIT', 'ERROR', 'INITIAL_HINT', 'HINT_LEVEL_CHANGE', 'FREEBIE_JIT').required(), + + // 'Outcome': Joi.string().valid('OK', 'BUG', 'INITIAL_HINT', 'HINT_LEVEL_CHANGE', 'ERROR').required(), }).unknown() ); From 22505997745186b62e3265e502b8c203b7d03182 Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Tue, 23 Jul 2024 11:56:57 -0500 Subject: [PATCH 17/54] Parallel processing attempt v1 --- package.json | 1 + src/App.tsx | 67 +------- src/components/DropZone.tsx | 293 +++++++++++++++++++++++---------- src/lib/dataProcessingUtils.ts | 3 + src/lib/fileWorker.ts | 25 +++ tsconfig.json | 7 +- 6 files changed, 246 insertions(+), 150 deletions(-) create mode 100644 src/lib/fileWorker.ts diff --git a/package.json b/package.json index 04207cd..61a059c 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@types/timsort": "^0.3.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "comlink": "^4.4.1", "d3": "^7.9.0", "danfojs-node": "^1.1.2", "express": "^4.19.2", diff --git a/src/App.tsx b/src/App.tsx index ef5ba81..9904549 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,18 +3,17 @@ import {useContext, useEffect, useState} from 'react'; import { GlobalDataType, GraphData } from './lib/types'; import DirectedGraph from './components/DirectedGraph'; import DropZone from './components/DropZone'; -// import { NavBar } from './components/NavBar'; import { Button } from './components/ui/button'; import { Context } from './Context'; import { processDataShopData } from './lib/dataProcessingUtils'; import {filterPromGrad} from "@/lib/GradPromUtils"; -import Switch from "@/components/ui/switch.tsx"; + import './Switch.css'; function App() { - const { resetData, setGraphData, setLoading, //setFilteredData, filteredData, + const { resetData, setGraphData, setLoading, data, setData, graphData, loading } = useContext(Context) const [showDropZone, setShowDropZone] = useState(true) @@ -36,16 +35,8 @@ function App() { else { } - // console.log(data) - // return filteredData - } - // const handleFilteredData = (filteredData: GlobalDataType[]) => { - // setData(filteredData) - // setShowDropZone(false) - // const graphData: GraphData = processDataShopData(filteredData) - // setGraphData(graphData) - // } + } useEffect(() => { if (data) { @@ -53,57 +44,7 @@ function App() { setGraphData(graphData) }}, [data]) - // useEffect(() => { - // if (filteredData) { - // const graphData: GraphData = processDataShopData(filteredData) - // setGraphData(graphData) - // }}, [filteredData]) - - // const [isOn, setIsOn] = useState(false); - // const [isSwitchEnabled, setIsSwitchEnabled] = useState(false); - // const [filter, setFilter] = useState<"PROMOTED"|"GRADUATED"|null|string>("GRADUATED"); - // Using Graduated as initial gives the first click the correct value but not after - // -- doesn't work if initial state is null - - // const handleToggle = () => { - // if (isSwitchEnabled){ - // setIsOn(!isOn); - // } - // }; - // - // const handleCheckboxChange = (event: React.ChangeEvent) => { - // setIsSwitchEnabled(event.target.checked); - // }; - - // const handleFilterChange = (event: React.ChangeEvent) => { - // setFilter(event.target.value); - // }; - - // useEffect(() => { - // if (isSwitchEnabled) { - // let value = isOn ? "PROMOTED" : "GRADUATED"; - // setFilter(value); - // // console.log("Filter: " + filter) - // let f = filterData(data!, value) // Why can't I use filter here instead of value? - // - // console.log("Filter: " + value) - // - // console.log(f) - // } - // - // else{ - // setData(data!) - // console.log(data) - // } - // - // - // }, [isSwitchEnabled, isOn]); - // - // const getValueBasedOnSwitch = () => { - // if (isSwitchEnabled) { - // return filter - // }; - // } + return ( <> diff --git a/src/components/DropZone.tsx b/src/components/DropZone.tsx index 8e3a80c..71efc57 100644 --- a/src/components/DropZone.tsx +++ b/src/components/DropZone.tsx @@ -1,118 +1,241 @@ -import { useCallback, useState } from 'react'; +// import { useCallback, useState } from 'react'; +// import { Accept, useDropzone } from 'react-dropzone'; +// import { GlobalDataType } from '@/lib/types'; +// import { parseData } from '@/lib/utils'; +// import { Label } from "@/components/ui/label" +// import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" +// import toast from 'react-hot-toast'; +// +// interface DropZoneProps { +// afterDrop: (data: GlobalDataType[]) => void, +// onLoadingChange: (loading: boolean) => void +// } +// +// export default function DropZone({ afterDrop, onLoadingChange }: DropZoneProps) { +// const delimiters = ["tsv", "csv", "pipe"]; +// const [errorMessage, setErrorMessage] = useState(""); +// +// const [fileType, setFileType] = useState(delimiters[0]) +// +// const onDrop = useCallback((acceptedFiles: File[]) => { +// onLoadingChange(true); +// +// acceptedFiles.forEach((file: File) => { +// const reader = new FileReader(); +// +// reader.onabort = () => console.warn('file reading was aborted'); +// reader.onerror = () => console.error('file reading has failed'); +// reader.onload = () => { +// const textStr = reader.result; +// let delimiter: string; +// switch (fileType) { +// case 'tsv': +// delimiter = '\t'; +// break; +// case 'csv': +// delimiter = ','; +// break; +// case 'pipe': +// delimiter = '|'; +// break; +// default: +// delimiter = '\t'; +// break; +// } +// const array: GlobalDataType[] | null = parseData(textStr, delimiter); +// console.log("Array: ", array); +// // array is null when there is an error in the file structure or content +// if (!array) { +// +// toast.error("Invalid file structure or content") +// console.log("Error state before: ", errorMessage); +// setErrorMessage("Invalid file structure or content"); +// console.log("Error state after: ", errorMessage); +// +// // the below prints, but the above does not execute. Why? +// // console.error("!!!Invalid file structure or content"); +// } +// else { +// afterDrop(array); +// } +// +// +// onLoadingChange(false); +// }; +// reader.readAsText(file); +// console.log("File: ", file); +// +// }); +// }, [fileType, afterDrop, onLoadingChange]); +// +// const acceptedFileTypes: Accept = { +// 'text/plain': ['.txt', '.csv', '.tsv', '.json', '.tsv', '.pipe'], +// } +// +// +// +// const { getRootProps, getInputProps, isDragActive, isFocused, isDragReject } = useDropzone({ +// onDrop, +// accept: acceptedFileTypes, +// // validator: validateData +// }); +// +// +// +// const fileTypeOptions = [ +// { +// label: 'Tab Separated', +// value: delimiters.find((delimiter) => delimiter === 'tsv') as string +// }, +// { +// label: 'Comma Separated', +// value: delimiters.find((delimiter) => delimiter === 'csv') as string +// }, +// { +// label: 'Pipe Separated', +// value: delimiters.find((delimiter) => delimiter === 'pipe') as string +// }, +// // { +// // label: 'JSON', +// // value: delimiters.find((delimiter) => delimiter === 'json') as string +// // } +// ] +// return ( +// <> +//
+//
+// File Type +//
+// { +// setFileType(e) +// +// }}> +// {fileTypeOptions.map((option, index) => ( +//
+// +// +//
+// ))} +//
+//
+//
+// +// { +// !isDragActive ? +//
+//

Drag 'n' drop some files here, or click to select files

+//
+// : +//
+//

Drag 'n' drop some files here, or click to select files

+//
+// } +// +//
+// {errorMessage &&

{errorMessage}

} +//
+//
+// +// +// +// ); +// } + +import { useCallback, useState, useEffect } from 'react'; import { Accept, useDropzone } from 'react-dropzone'; import { GlobalDataType } from '@/lib/types'; -import { parseData } from '@/lib/utils'; -import { Label } from "@/components/ui/label" -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" +import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import toast from 'react-hot-toast'; +import * as Comlink from 'comlink'; interface DropZoneProps { - afterDrop: (data: GlobalDataType[]) => void, - onLoadingChange: (loading: boolean) => void + afterDrop: (data: GlobalDataType[]) => void; + onLoadingChange: (loading: boolean) => void; +} + +// Define the worker API interface +interface WorkerApi { + parseData(text: string, delimiter: string): Promise; } export default function DropZone({ afterDrop, onLoadingChange }: DropZoneProps) { const delimiters = ["tsv", "csv", "pipe"]; const [errorMessage, setErrorMessage] = useState(""); + const [fileType, setFileType] = useState(delimiters[0]); + const [worker, setWorker] = useState(null); - const [fileType, setFileType] = useState(delimiters[0]) + useEffect(() => { + // Initialize the Comlink worker + const workerInstance = new Worker(new URL('./fileWorker.ts', import.meta.url)); + const proxy = Comlink.wrap(workerInstance); + setWorker(proxy); - const onDrop = useCallback((acceptedFiles: File[]) => { + return () => { + workerInstance.terminate(); + }; + }, []); + + const onDrop = useCallback(async (acceptedFiles: File[]) => { onLoadingChange(true); - - acceptedFiles.forEach((file: File) => { + + const delimiter = fileType === 'tsv' ? '\t' : fileType === 'csv' ? ',' : '|'; + + for (const file of acceptedFiles) { const reader = new FileReader(); reader.onabort = () => console.warn('file reading was aborted'); reader.onerror = () => console.error('file reading has failed'); - reader.onload = () => { - const textStr = reader.result; - let delimiter: string; - switch (fileType) { - case 'tsv': - delimiter = '\t'; - break; - case 'csv': - delimiter = ','; - break; - case 'pipe': - delimiter = '|'; - break; - default: - delimiter = '\t'; - break; - } - const array: GlobalDataType[] | null = parseData(textStr, delimiter); - console.log("Array: ", array); - // array is null when there is an error in the file structure or content - if (!array) { + reader.onload = async () => { + const textStr = reader.result as string; - toast.error("Invalid file structure or content") - console.log("Error state before: ", errorMessage); + try { + if (worker) { + const array: GlobalDataType[] | null = await worker.parseData(textStr, delimiter); + if (array) { + afterDrop(array); + } else { + throw new Error("Invalid file structure or content"); + } + } + } catch (error) { + console.error(error.message); + toast.error("Invalid file structure or content"); setErrorMessage("Invalid file structure or content"); - console.log("Error state after: ", errorMessage); - - // the below prints, but the above does not execute. Why? - // console.error("!!!Invalid file structure or content"); - } - else { - afterDrop(array); + } finally { + onLoadingChange(false); } - - - onLoadingChange(false); }; reader.readAsText(file); - console.log("File: ", file); - - }); - }, [fileType, afterDrop, onLoadingChange]); + } + }, [fileType, afterDrop, onLoadingChange, worker]); const acceptedFileTypes: Accept = { - 'text/plain': ['.txt', '.csv', '.tsv', '.json', '.tsv', '.pipe'], + 'text/plain': ['.txt', '.csv', '.tsv', '.json', '.pipe'], } - - - const { getRootProps, getInputProps, isDragActive, isFocused, isDragReject } = useDropzone({ + const { getRootProps, getInputProps, isDragActive, isFocused } = useDropzone({ onDrop, accept: acceptedFileTypes, - // validator: validateData }); - - const fileTypeOptions = [ - { - label: 'Tab Separated', - value: delimiters.find((delimiter) => delimiter === 'tsv') as string - }, - { - label: 'Comma Separated', - value: delimiters.find((delimiter) => delimiter === 'csv') as string - }, - { - label: 'Pipe Separated', - value: delimiters.find((delimiter) => delimiter === 'pipe') as string - }, - // { - // label: 'JSON', - // value: delimiters.find((delimiter) => delimiter === 'json') as string - // } - ] + { label: 'Tab Separated', value: 'tsv' }, + { label: 'Comma Separated', value: 'csv' }, + { label: 'Pipe Separated', value: 'pipe' }, + ]; + return ( <>
-
- File Type -
- { - setFileType(e) - - }}> +
File Type
+ {fileTypeOptions.map((option, index) => (
- +
))} @@ -125,12 +248,12 @@ export default function DropZone({ afterDrop, onLoadingChange }: DropZoneProps) { !isDragActive ? -
-

Drag 'n' drop some files here, or click to select files

+
+

Drag 'n' drop some files here, or click to select files

: -
-

Drag 'n' drop some files here, or click to select files

+
+

Drop the files here...

} @@ -138,8 +261,6 @@ export default function DropZone({ afterDrop, onLoadingChange }: DropZoneProps) {errorMessage &&

{errorMessage}

}
- - ); -} \ No newline at end of file +} diff --git a/src/lib/dataProcessingUtils.ts b/src/lib/dataProcessingUtils.ts index d50b229..9338259 100644 --- a/src/lib/dataProcessingUtils.ts +++ b/src/lib/dataProcessingUtils.ts @@ -1,5 +1,6 @@ import { IncomingData, Node, Link, GraphData, LinkObject, NodeObject, IncomingDataRaw, GlobalDataType } from "@/lib/types"; import _ from 'lodash'; +import * as Comlink from 'comlink'; // ----------------- DATA PROCESSING UTILS DataShop Data ----------------- @@ -131,6 +132,8 @@ export function processDataShopData(data: GlobalDataType[]): GraphData{ maxEdgeCount, }; } + +// Comlink.expose({ processDataShopData }); // ----------------- DATA PROCESSING UTILS Athena Data ----------------- diff --git a/src/lib/fileWorker.ts b/src/lib/fileWorker.ts new file mode 100644 index 0000000..1d04ec1 --- /dev/null +++ b/src/lib/fileWorker.ts @@ -0,0 +1,25 @@ +// Import Comlink from CDN or local source +import {GlobalDataType} from "@/lib/types.ts"; + +importScripts('https://unpkg.com/comlink/dist/umd/comlink.js'); + +// Define the data processing logic +function parseData(text: string, delimiter: string): GlobalDataType[] | null { + // Example parsing logic + // Split the text into lines, then split each line into columns based on the delimiter + const lines = text.split('\n'); + return lines.map(line => line.split(delimiter)).filter(parts => parts.length > 0); +} + +// Define the API that will be exposed to the main thread +interface WorkerApi { + parseData(text: string, delimiter: string): Promise; +} + +// Create an object that implements the WorkerApi interface +const api: WorkerApi = { + parseData: (text, delimiter) => Promise.resolve(parseData(text, delimiter)) +}; + +// Expose the API to the main thread using Comlink +Comlink.expose(api); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index d0c45be..cb37fc0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,12 @@ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable", + "webworker" + ], "module": "ESNext", "skipLibCheck": true, "baseUrl": ".", From 469a1d060ef04657bc5326f95c158178f145352a Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:51:39 -0500 Subject: [PATCH 18/54] Graphviz implementation in PAT! --- src/App.tsx | 107 ++++-------- src/Switch.css | 47 ++++++ src/components/FilterComponent.tsx | 24 +++ src/components/GraphvizParent.tsx | 112 +++++++++++-- src/components/Upload.tsx | 27 ++++ src/components/graphvizProcessing.ts | 233 +++++++++++++++++++++++++++ src/components/selfLoopSwitch.tsx | 19 +++ src/components/slider.tsx | 32 ++++ src/lib/types.ts | 3 +- 9 files changed, 515 insertions(+), 89 deletions(-) create mode 100644 src/Switch.css create mode 100644 src/components/FilterComponent.tsx create mode 100644 src/components/Upload.tsx create mode 100644 src/components/graphvizProcessing.ts create mode 100644 src/components/selfLoopSwitch.tsx create mode 100644 src/components/slider.tsx diff --git a/src/App.tsx b/src/App.tsx index 6b2959f..cc768c2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,81 +1,34 @@ import './App.css'; -import {useContext, useEffect, useState} from 'react'; -import {GlobalDataType, GraphData} from './lib/types'; -import DirectedGraph from './components/DirectedGraph'; -import DropZone from './components/DropZone'; -// import { NavBar } from './components/NavBar'; -import {Button} from './components/ui/button'; -import {Context} from './Context'; -import {processDataShopData} from './lib/dataProcessingUtils'; -import Loading from './components/Loading'; - -function App() { - - const {resetData, setGraphData, setLoading, data, setData, graphData, loading} = useContext(Context) - const [showDropZone, setShowDropZone] = useState(true) - - const handleData = (data: GlobalDataType[]) => { - setData(data) - setShowDropZone(false) - } - - const handleLoading = (loading: boolean) => { - setLoading(loading) - } - +import React, {useEffect, useState} from 'react'; +import Upload from "@/components/Upload.tsx"; +import GraphvizParent from "@/components/GraphvizParent.tsx"; +// import GraphContainer from './components/GraphContainer'; +import FilterComponent from './components/FilterComponent.tsx'; +import SelfLoopSwitch from './components/selfLoopSwitch.tsx'; +import Slider from './components/slider.tsx'; + +const App: React.FC = () => { + const [csvData, setCsvData] = useState(''); + const [filter, setFilter] = useState(''); + const [selfLoops, setSelfLoops] = useState(true); + const [minVisits, setMinVisits] = useState(30); + + const handleToggle = () => setSelfLoops(!selfLoops); + const handleSlider = (value: number) => setMinVisits(value); + const handleDataProcessed = (uploadedCsvData: string) => setCsvData(uploadedCsvData); useEffect(() => { - if (data) { - const graphData: GraphData = processDataShopData(data) - setGraphData(graphData) - - } - }, [data]) + }, []); return ( - <> -
- {/* */} - - -
- { - loading ? - - : - ( - showDropZone && ( -
- -
- ) - - ) - - } - - - { - graphData && ( - <> - {/* TODO: Swap DirectedGraph for your new component */} - - - ) - } - -
-
- - ) -} - -export default App +
+

Path Analysis Tool

+ + + + + +
+ ); +}; + +export default App; \ No newline at end of file diff --git a/src/Switch.css b/src/Switch.css new file mode 100644 index 0000000..10e312c --- /dev/null +++ b/src/Switch.css @@ -0,0 +1,47 @@ +/* src/Switch.css */ +.switch-container { + display: inline-block; + cursor: pointer; +} + +.switch { + width: 50px; + height: 25px; + border-radius: 25px; + background-color: grey; + position: relative; + left: 0px; + top: 0px; + transition: background-color 0.2s; +} + +.true { + background-color: #80d580; +} + +.false { + background-color: #b20da7; +} + +.switch-handle { + width: 23px; + height: 23px; + border-radius: 50%; + background-color: white; + position: relative; + top: 1px; + left: 1px; + transition: left 0.1s; +} + +.true .switch-handle { + left: 26px; +} + +.switch-display{ /*Not working*/ + display: none; +} + +.switch.disabled{ /*Not working*/ + visibility: hidden; +} diff --git a/src/components/FilterComponent.tsx b/src/components/FilterComponent.tsx new file mode 100644 index 0000000..9af8f5f --- /dev/null +++ b/src/components/FilterComponent.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +interface FilterComponentProps { + onFilterChange: (filter: string) => void; +} + +const FilterComponent: React.FC = ({ onFilterChange }) => { + const handleFilterChange = (event: React.ChangeEvent) => { + onFilterChange(event.target.value); + }; + + return ( +
+ + +
+ ); +}; + +export default FilterComponent; \ No newline at end of file diff --git a/src/components/GraphvizParent.tsx b/src/components/GraphvizParent.tsx index 3a044f2..4be6159 100644 --- a/src/components/GraphvizParent.tsx +++ b/src/components/GraphvizParent.tsx @@ -1,33 +1,123 @@ -import {useState} from "react"; +import {useEffect, useState} from "react"; import ErrorBoundary from "@/components/errorBoundary.tsx"; import Graphviz from 'graphviz-react'; +import { + loadAndSortData, + createStepSequences, + createOutcomeSequences, + countEdges, + normalizeThicknesses, + generateDotString +} from './graphvizProcessing'; + interface GraphvizParentProps { - csvData: string + csvData: string; + filter: string; + selfLoops: boolean; + minVisits: number; } -export default function GraphvizParent({csvData}: GraphvizParentProps) { +const GraphvizParent: React.FC = ({ csvData, filter, selfLoops, minVisits }) => { const [dotString, setDotString] = useState(''); const [filteredDotString, setFilteredDotString] = useState(''); - const [filter, setFilter] = useState(''); // State for the selected filter - const [csvData, setCsvData] = useState(''); // State to store raw CSV data - const [selfLoops, setSelfLoops] = useState(true) + // Process the CSV data initially and when filter changes + useEffect(() => { + if (!csvData) return; // Skip if no CSV data is available + + const sortedData = loadAndSortData(csvData); + + // Generate the unfiltered graph + const stepSequences = createStepSequences(sortedData, selfLoops); + console.log(stepSequences) + const outcomeSequences = createOutcomeSequences(sortedData); + + const {edgeCounts, totalNodeEdges, ratioEdges, edgeOutcomeCounts} = countEdges(stepSequences, outcomeSequences); + + const normalizedThicknesses = normalizeThicknesses(ratioEdges, 10); + + const mostCommonSequenceKey = Object.keys(stepSequences) + .reduce((a, b) => stepSequences[a].length > stepSequences[b].length ? a : b); + + const mostCommonSequence = stepSequences[mostCommonSequenceKey]; + const dotStr = generateDotString( + normalizedThicknesses, + mostCommonSequence, + ratioEdges, + edgeOutcomeCounts, + edgeCounts, + totalNodeEdges, + 1, + minVisits + ); + setDotString(dotStr); + console.log(dotString) + // Generate the filtered graph if a filter is set + if (filter) { + //TODO: Add qualifier to show difference between two graphs (maybe border color to show if + // something increased or decreased) + //TODO: Make order/ranking same in both graphs + const filteredData = sortedData.filter(row => row['CF (Workspace Progress Status)'] === filter); + console.log(filteredData); + + const filteredStepSequences = createStepSequences(filteredData, selfLoops); + console.log(filteredStepSequences) + const filteredOutcomeSequences = createOutcomeSequences(filteredData); + + const { + edgeCounts: filteredEdgeCounts, totalNodeEdges: filteredTotalNodeEdges, + ratioEdges: filteredRatioEdges, edgeOutcomeCounts: filteredEdgeOutcomeCounts + } + = countEdges(filteredStepSequences, filteredOutcomeSequences); + + const filteredNormalizedThicknesses = normalizeThicknesses(filteredRatioEdges, 10); + + const filteredMostCommonSequenceKey = Object.keys(filteredStepSequences) + .reduce((a, b) => filteredStepSequences[a].length > filteredStepSequences[b].length ? a : b); + + const filteredMostCommonSequence = filteredStepSequences[filteredMostCommonSequenceKey]; + const filteredDotStr = generateDotString( + filteredNormalizedThicknesses, + filteredMostCommonSequence, + filteredRatioEdges, + filteredEdgeOutcomeCounts, + filteredEdgeCounts, + filteredTotalNodeEdges, + 1, + minVisits + ); + let edge: string; + for (edge in edgeCounts) { + if (edgeCounts[edge] != filteredEdgeCounts[edge]) { + console.log(edge, filteredEdgeCounts[edge] - edgeCounts[edge]) + if (isNaN(filteredEdgeCounts[edge] - edgeCounts[edge])) { + console.log("NaN: " + edge, filteredEdgeCounts[edge], edgeCounts[edge]) + } + } + } + // console.log(filteredDotStr) + setFilteredDotString(filteredDotStr); + console.log(filteredDotString) + } else { + setFilteredDotString(null); // Clear filtered graph if no filter is set + } + + }, [csvData, filter, selfLoops, minVisits]); - // logic here return ( - <> +
- {dotString && } {filteredDotString && }
- +
) -} \ No newline at end of file +} +export default GraphvizParent \ No newline at end of file diff --git a/src/components/Upload.tsx b/src/components/Upload.tsx new file mode 100644 index 0000000..5da7667 --- /dev/null +++ b/src/components/Upload.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +interface UploadProps { + onDataProcessed: (csvData: string) => void; +} + +const Upload: React.FC = ({ onDataProcessed }) => { + const handleFileUpload = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + const csvData = e.target?.result as string; + onDataProcessed(csvData); + }; + reader.readAsText(file); + } + }; + + return ( +
+ +
+ ); +}; + +export default Upload; \ No newline at end of file diff --git a/src/components/graphvizProcessing.ts b/src/components/graphvizProcessing.ts new file mode 100644 index 0000000..14381bd --- /dev/null +++ b/src/components/graphvizProcessing.ts @@ -0,0 +1,233 @@ +import Papa from 'papaparse'; + +interface CSVRow { + 'Session Id': string; + 'Time': string; + 'Step Name': string; + 'Outcome': string; + 'CF (Workspace Progress Status)': string; + +} + +// Function to load and sort data +export const loadAndSortData = (csvData: string): CSVRow[] => { + const parsedData = Papa.parse(csvData, { + header: true, + skipEmptyLines: true + }).data; + + // Step 2: Transform data to replace NaN values + const transformedData = parsedData.map(row => { + return { + 'Session Id': row['Session Id'], + 'Time': row['Time'], + 'Step Name': row['Step Name'] || 'DoneButton', //TODO: Nan only shows up on selection "Done Button" + 'Outcome': row['Outcome'], + 'CF (Workspace Progress Status)': row['CF (Workspace Progress Status)'], + }; + }); + + + return transformedData.sort((a, b) => { + if (a['Session Id'] === b['Session Id']) { + return new Date(a['Time']).getTime() - new Date(b['Time']).getTime(); + } + return a['Session Id'].localeCompare(b['Session Id']); + }); +}; + + +export const createStepSequences = (sortedData: CSVRow[], selfLoops:boolean): { [key: string]: string[] } => { + return sortedData.reduce((acc, row) => { + const sessionId = row['Session Id']; + if (!acc[sessionId]) { + acc[sessionId] = []; + } + const stepName = row['Step Name']; + + if (!selfLoops) { + if (!acc[sessionId].includes(stepName)) { + acc[sessionId].push(stepName); + } + } else { + acc[sessionId].push(stepName); + } + + // console.log(sessionId, acc[sessionId]) + return acc; + }, {} as { [key: string]: string[] }); +}; + +// Function to create outcome sequences +export const createOutcomeSequences = (sortedData: CSVRow[]): { [key: string]: string[] } => { + // console.log(sortedData) + return sortedData.reduce((acc, row) => { + const sessionId = row['Session Id']; + if (!acc[sessionId]) { + acc[sessionId] = []; + } + acc[sessionId].push(row['Outcome']); + return acc; + }, {} as { [key: string]: string[] }); +}; + +interface EdgeCounts { + edgeCounts: { [key: string]: number }; + totalNodeEdges: { [key: string]: number }; + ratioEdges: { [key: string]: number }; + edgeOutcomeCounts: { [key: string]: { [outcome: string]: number } }; +} + +// Function to count edges +export const countEdges = ( + stepSequences: { [key: string]: string[] }, + outcomeSequences: { [key: string]: string[] } +): EdgeCounts => { + const edgeCounts: { [key: string]: number } = {}; + const totalNodeEdges: { [key: string]: number } = {}; + const ratioEdges: { [key: string]: number } = {}; + const edgeOutcomeCounts: { [key: string]: { [outcome: string]: number } } = {}; + + Object.keys(stepSequences).forEach((sessionId) => { + const steps = stepSequences[sessionId]; + const outcomes = outcomeSequences[sessionId]; + + if (steps.length < 2) return; + + for (let i = 0; i < steps.length - 1; i++) { + const currentStep = steps[i]; + const nextStep = steps[i + 1]; + const outcome = outcomes[i + 1]; + + const edgeKey = `${currentStep}->${nextStep}`; + edgeCounts[edgeKey] = (edgeCounts[edgeKey] || 0) + 1; + edgeOutcomeCounts[edgeKey] = edgeOutcomeCounts[edgeKey] || {}; + edgeOutcomeCounts[edgeKey][outcome] = (edgeOutcomeCounts[edgeKey][outcome] || 0) + 1; + totalNodeEdges[currentStep] = (totalNodeEdges[currentStep] || 0) + 1; + } + }); + + Object.keys(edgeCounts).forEach((edge) => { + const [start] = edge.split('->'); + ratioEdges[edge] = edgeCounts[edge] / (totalNodeEdges[start] || 0); + }); + // console.log("edgeOutcomeCounts: ", edgeOutcomeCounts, "\nratioEdges: ", ratioEdges ) + return { edgeCounts, totalNodeEdges, ratioEdges, edgeOutcomeCounts }; +}; + +export function normalizeThicknesses( + ratioEdges: { [key: string]: number }, + maxThickness: number +): { [key: string]: number } { + const normalized: { [key: string]: number } = {}; + const maxRatio = Math.max(...Object.values(ratioEdges), 1); // Avoid division by zero + + Object.keys(ratioEdges).forEach((edge) => { + const ratio = ratioEdges[edge]; + + + normalized[edge] = (ratio / maxRatio) * maxThickness; + }); + + return normalized; +} + +function calculateColor(rank: number, totalSteps: number): string { + // Calculate the ratio between 0 and 1 + const ratio = rank / totalSteps; + + const white = { r: 255, g: 255, b: 255 }; // White color + const lightBlue = { r: 0, g: 166, b: 255 }; // Light Blue color + + // Interpolate between white and light blue + const r = Math.round(white.r * (1 - ratio) + lightBlue.r * ratio); + const g = Math.round(white.g * (1 - ratio) + lightBlue.g * ratio); + const b = Math.round(white.b * (1 - ratio) + lightBlue.b * ratio); + + // Convert RGB to hexadecimal + const toHex = (value: number) => value.toString(16).padStart(2, '0'); + const color = `#${toHex(r)}${toHex(g)}${toHex(b)}`; + + + // console.log("Color: ", r, g, b, toHex(r), toHex(g), toHex(b), color); + + return color; +} + +function calculateEdgeColors(outcomes: { [outcome: string]: number }): string { + const colorMap: { [key: string]: string } = { + 'ERROR': '#ff0000', // Red + 'OK': '#00ff00', // Green + 'INITIAL_HINT': '#0000ff', // Blue + 'HINT_LEVEL_CHANGE': '#0000ff', // Blue + 'JIT': '#ffff00', // Yellow + 'FREEBIE_JIT': '#ffff00' // Yellow + }; + + if (Object.keys(outcomes).length === 0) { + return '#00000000'; // Transparent black + } + + const totalCount = Object.values(outcomes).reduce((sum, count) => sum + count, 0); + let weightedR = 0, weightedG = 0, weightedB = 0; + + Object.entries(outcomes).forEach(([outcome, count]) => { + const color = colorMap[outcome] || '#000000'; // Default to black if outcome is not found + const [r, g, b] = [1, 3, 5].map(i => parseInt(color.slice(i, i + 2), 16)); // Extract RGB values + const weight = count / totalCount; + weightedR += r * weight; + weightedG += g * weight; + weightedB += b * weight; + }); + + // Convert RGB values to hex and add alpha transparency + return `#${Math.round(weightedR).toString(16).padStart(2, '0')}${Math.round(weightedG).toString(16).padStart(2, '0')}${Math.round(weightedB).toString(16).padStart(2, '0')}90`; +} + +export function generateDotString( + normalizedThicknesses: { [key: string]: number }, + mostCommonSequence: string[], + ratioEdges: { [key: string]: number }, + edgeOutcomeCounts: EdgeCounts['edgeOutcomeCounts'], + edgeCounts: EdgeCounts['edgeCounts'], + totalNodeEdges: EdgeCounts['totalNodeEdges'], + threshold: number, + min_visits: number, +): string { + let dotString = 'digraph G {\n'; + const totalSteps = mostCommonSequence.length; + // console.log(mostCommonSequence, totalSteps) + for (let rank = 0; rank < totalSteps; rank++) { + const step = mostCommonSequence[rank]; + const color = calculateColor(rank + 1, totalSteps); + const node_tooltip = `Rank:\n\t\t ${rank + 1}\nColor:\n\t\t ${color}`; + + dotString += ` "${step}" [rank=${rank + 1}, style=filled, fillcolor="${color}", tooltip="${node_tooltip}"];\n`; + } + + for (const edge of Object.keys(normalizedThicknesses)) { + if (normalizedThicknesses[edge] >= threshold) { + const [currentStep, nextStep] = edge.split('->');// as EdgeKey; + const thickness = normalizedThicknesses[edge]; + const outcomes = edgeOutcomeCounts[edge] || {}; + const edgeCount = edgeCounts[edge] || 0; + const totalCount = totalNodeEdges[currentStep] || 0; + const color = calculateEdgeColors(outcomes); + const outcomesStr = Object.entries(outcomes) + .map(([outcome, count]) => `${outcome}: ${count}`) + .join('\n\t\t '); + if (edgeCount > min_visits){ + const tooltip = `${currentStep} to ${nextStep}\n` + + `- Edge Count: \n\t\t ${edgeCount}\n` + + `- Total Count for ${currentStep}: \n\t\t${totalCount}\n` + + `- Ratio: \n\t\t${((ratioEdges[edge] || 0) * 100).toFixed(2)}% of students at ${currentStep} go to ${nextStep}\n` + + `- Outcomes: \n\t\t ${outcomesStr}\n` + + `- Color Codes: \n\t\t Hex: ${color}\n\t\t RGB: ${[parseInt(color.substring(1, 3), 16), parseInt(color.substring(3, 5), 16), parseInt(color.substring(5, 7), 16)]}`; + + dotString += ` "${currentStep}" -> "${nextStep}" [penwidth=${thickness}, color="${color}", tooltip="${tooltip}"];\n`; + }} + } + + dotString += '}'; + return dotString; +} diff --git a/src/components/selfLoopSwitch.tsx b/src/components/selfLoopSwitch.tsx new file mode 100644 index 0000000..7054ac1 --- /dev/null +++ b/src/components/selfLoopSwitch.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import './../Switch.css'; // Import the CSS for styling + +interface SwitchProps { + isOn: boolean; + handleToggle: () => void; +} + +const Switch: React.FC = ({ isOn, handleToggle }) => { + return ( +
+ +
+
+
+
+ ); +}; +export default Switch; \ No newline at end of file diff --git a/src/components/slider.tsx b/src/components/slider.tsx new file mode 100644 index 0000000..7ece586 --- /dev/null +++ b/src/components/slider.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +interface SliderProps { + min: number; + max: number; + step?: number; + value: number; + onChange: (value: number) => void; +} + +const Slider: React.FC = ({ min, max, step = 1, value, onChange }) => { + const handleChange = (event: React.ChangeEvent) => { + onChange(Number(event.target.value)); + }; + + return ( +
+ +

Minimum # of Edge Visits for Visualization: {value}

+
+ ); +}; + +export default Slider; diff --git a/src/lib/types.ts b/src/lib/types.ts index a2116c7..22e48e9 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -20,7 +20,8 @@ export type DataSet1 = { "Step Name": string | null; "Attempt At Step": number; "Is Last Attempt": boolean | null; - Outcome: "OK" | "BUG" | "INITIAL_HINT" | "HINT_LEVEL_CHANGE" | "ERROR"; + Outcome: "OK" | "JIT" | "ERROR" | "INITIAL_HINT" | "HINT_LEVEL_CHANGE" | "FREEBIE_JIT" + Selection: "Done Button" | null; Action: "Attempt" | "Done" | "Hint Request" | "Hint Level Change"; Input: string | null; From c2b471f20c84c6e66b7c42c93b86ec7c3bd3b54e Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:52:01 -0500 Subject: [PATCH 19/54] Graphviz implementation in PAT! --- src/App.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index cc768c2..2eb3d1f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,6 @@ import './App.css'; import React, {useEffect, useState} from 'react'; import Upload from "@/components/Upload.tsx"; import GraphvizParent from "@/components/GraphvizParent.tsx"; -// import GraphContainer from './components/GraphContainer'; import FilterComponent from './components/FilterComponent.tsx'; import SelfLoopSwitch from './components/selfLoopSwitch.tsx'; import Slider from './components/slider.tsx'; From 016751fe4d816cb77a411dacc676ffa9ae7bf761 Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Wed, 21 Aug 2024 15:40:10 -0500 Subject: [PATCH 20/54] Added dropdown to pick sequence to color by --- src/App.tsx | 29 ++- src/GraphvizContainer.css | 21 +++ src/components/GraphvizParent.tsx | 159 ++++++++++------ ...vizProcessing.ts => GraphvizProcessing.ts} | 177 +++++++++++++----- src/components/SequenceSelector.tsx | 31 +++ src/components/slider.tsx | 100 +++++++--- 6 files changed, 377 insertions(+), 140 deletions(-) create mode 100644 src/GraphvizContainer.css rename src/components/{graphvizProcessing.ts => GraphvizProcessing.ts} (52%) create mode 100644 src/components/SequenceSelector.tsx diff --git a/src/App.tsx b/src/App.tsx index 2eb3d1f..881c7c8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,31 +1,42 @@ import './App.css'; -import React, {useEffect, useState} from 'react'; +import React, {useCallback, useState} from 'react'; import Upload from "@/components/Upload.tsx"; import GraphvizParent from "@/components/GraphvizParent.tsx"; import FilterComponent from './components/FilterComponent.tsx'; import SelfLoopSwitch from './components/selfLoopSwitch.tsx'; import Slider from './components/slider.tsx'; +import SequenceSelector from "@/components/SequenceSelector.tsx"; const App: React.FC = () => { const [csvData, setCsvData] = useState(''); const [filter, setFilter] = useState(''); const [selfLoops, setSelfLoops] = useState(true); const [minVisits, setMinVisits] = useState(30); - + const [topSequences, setTopSequences] = useState([]); + const [selectedSequence, setSelectedSequence] = useState(topSequences[0]); + const handleSelectSequence = (sequence: string) => { + setSelectedSequence(selectedSequence); + // Add your logic here to update the node coloring based on the selected sequence + console.log(`Selected sequence: ${sequence}`); + }; const handleToggle = () => setSelfLoops(!selfLoops); const handleSlider = (value: number) => setMinVisits(value); const handleDataProcessed = (uploadedCsvData: string) => setCsvData(uploadedCsvData); - useEffect(() => { + const handleTopSequencesUpdate = useCallback((sequences: string[]) => { + setTopSequences(sequences); + console.log("AHHHH: "+topSequences) + }, [topSequences]) - }, []); return (

Path Analysis Tool

- - - - - + + + + + +
); }; diff --git a/src/GraphvizContainer.css b/src/GraphvizContainer.css new file mode 100644 index 0000000..a3ea593 --- /dev/null +++ b/src/GraphvizContainer.css @@ -0,0 +1,21 @@ +.graphviz-container { + display: flex; + justify-content: center; + align-items: center; + flex-direction: row; /* Change to row to align items horizontally */ + width: 100%; /* Ensure the container takes full width */ +} + +.graphs { + display: flex; + flex-direction: row; /* Ensure the graphs are aligned in a row */ + justify-content: space-around; + align-items: center; + gap: 20px; + width: 100%; /* Ensure the graphs container takes full width */ +} + +.graphs > div { + flex: 1; /* Allow the graphs to scale according to available space */ + max-width: 600px; /* Set a max-width for each graph */ +} diff --git a/src/components/GraphvizParent.tsx b/src/components/GraphvizParent.tsx index 4be6159..7bd9734 100644 --- a/src/components/GraphvizParent.tsx +++ b/src/components/GraphvizParent.tsx @@ -7,117 +7,156 @@ import { createOutcomeSequences, countEdges, normalizeThicknesses, - generateDotString -} from './graphvizProcessing'; + generateDotString, getTopSequences, +} from './GraphvizProcessing'; +import '../GraphvizContainer.css'; +import SequenceSelector from './SequenceSelector'; interface GraphvizParentProps { csvData: string; filter: string; selfLoops: boolean; minVisits: number; + onTopSequencesUpdate: (sequences: string[]) => void; + // selectedSequence: string; } -const GraphvizParent: React.FC = ({ csvData, filter, selfLoops, minVisits }) => { +const GraphvizParent: React.FC = ({ + csvData, + filter, + selfLoops, + minVisits, + onTopSequencesUpdate, + // selectedSequence, + }) => { const [dotString, setDotString] = useState(''); const [filteredDotString, setFilteredDotString] = useState(''); + const [topSequences, setTopSequences] = useState([]); + const [selectedSequence, setSelectedSequence] = useState(''); - - // Process the CSV data initially and when filter changes useEffect(() => { - if (!csvData) return; // Skip if no CSV data is available + if (!csvData) return; // Skip processing if no CSV data is available + // Step 1: Load and sort the data const sortedData = loadAndSortData(csvData); - // Generate the unfiltered graph + // Step 2: Generate the sequences for steps and outcomes const stepSequences = createStepSequences(sortedData, selfLoops); - console.log(stepSequences) const outcomeSequences = createOutcomeSequences(sortedData); - const {edgeCounts, totalNodeEdges, ratioEdges, edgeOutcomeCounts} = countEdges(stepSequences, outcomeSequences); - - const normalizedThicknesses = normalizeThicknesses(ratioEdges, 10); - - const mostCommonSequenceKey = Object.keys(stepSequences) - .reduce((a, b) => stepSequences[a].length > stepSequences[b].length ? a : b); - - const mostCommonSequence = stepSequences[mostCommonSequenceKey]; + // Step 3: Count edges and normalize thicknesses + const { + edgeCounts, + totalNodeEdges, + ratioEdges, + edgeOutcomeCounts, + maxEdgeCount, + top5Sequences + } = countEdges(stepSequences, outcomeSequences); + setTopSequences(top5Sequences) + setSelectedSequence(top5Sequences[0]) + const normalizedThicknesses = normalizeThicknesses(edgeCounts, maxEdgeCount, 10); + + // Step 4: Find the most common sequences + // const mostCommonSequenceKey = Object.keys(stepSequences) + // .reduce((a, b) => stepSequences[a].length > stepSequences[b].length ? a : b); + // const mostCommonSequence = stepSequences[mostCommonSequenceKey]; + // const topSequences = Object.keys(stepSequences) + // .sort((a, b) => stepSequences[b].length - stepSequences[a].length) + // .slice(0, 5); + // console.log(stepSequences) + // const top5Sequences = getTopSequences(stepSequences,5) + onTopSequencesUpdate(top5Sequences); + + setTopSequences(top5Sequences) + console.log("GVP67:" + topSequences) + // Call the update function to pass top sequences to App component + // Step 5: Generate the DOT string for the unfiltered graph const dotStr = generateDotString( normalizedThicknesses, - mostCommonSequence, + // mostCommonSequence, ratioEdges, edgeOutcomeCounts, edgeCounts, totalNodeEdges, 1, - minVisits + minVisits, + selectedSequence ); setDotString(dotStr); - console.log(dotString) - // Generate the filtered graph if a filter is set + + // Step 6: Generate the filtered graph if a filter is provided if (filter) { - //TODO: Add qualifier to show difference between two graphs (maybe border color to show if - // something increased or decreased) - //TODO: Make order/ranking same in both graphs const filteredData = sortedData.filter(row => row['CF (Workspace Progress Status)'] === filter); - console.log(filteredData); - const filteredStepSequences = createStepSequences(filteredData, selfLoops); - console.log(filteredStepSequences) const filteredOutcomeSequences = createOutcomeSequences(filteredData); const { - edgeCounts: filteredEdgeCounts, totalNodeEdges: filteredTotalNodeEdges, - ratioEdges: filteredRatioEdges, edgeOutcomeCounts: filteredEdgeOutcomeCounts - } - = countEdges(filteredStepSequences, filteredOutcomeSequences); - - const filteredNormalizedThicknesses = normalizeThicknesses(filteredRatioEdges, 10); - - const filteredMostCommonSequenceKey = Object.keys(filteredStepSequences) - .reduce((a, b) => filteredStepSequences[a].length > filteredStepSequences[b].length ? a : b); - - const filteredMostCommonSequence = filteredStepSequences[filteredMostCommonSequenceKey]; + edgeCounts: filteredEdgeCounts, + totalNodeEdges: filteredTotalNodeEdges, + ratioEdges: filteredRatioEdges, + edgeOutcomeCounts: filteredEdgeOutcomeCounts, + maxEdgeCount: filteredMaxEdgeCount, + top5Sequences, + } = countEdges(filteredStepSequences, filteredOutcomeSequences); + + const filteredNormalizedThicknesses = normalizeThicknesses(filteredEdgeCounts, filteredMaxEdgeCount, 10); + // const filteredMostCommonSequenceKey = Object.keys(filteredStepSequences) + // .reduce((a, b) => filteredStepSequences[a].length > filteredStepSequences[b].length ? a : b); + // const filteredMostCommonSequence = filteredStepSequences[filteredMostCommonSequenceKey]; + + // Generate the DOT string for the filtered graph const filteredDotStr = generateDotString( filteredNormalizedThicknesses, - filteredMostCommonSequence, + // filteredMostCommonSequence, filteredRatioEdges, filteredEdgeOutcomeCounts, filteredEdgeCounts, filteredTotalNodeEdges, 1, - minVisits + minVisits, + selectedSequence ); - let edge: string; - for (edge in edgeCounts) { - if (edgeCounts[edge] != filteredEdgeCounts[edge]) { - console.log(edge, filteredEdgeCounts[edge] - edgeCounts[edge]) - if (isNaN(filteredEdgeCounts[edge] - edgeCounts[edge])) { - console.log("NaN: " + edge, filteredEdgeCounts[edge], edgeCounts[edge]) - } - } - } - // console.log(filteredDotStr) + setFilteredDotString(filteredDotStr); - console.log(filteredDotString) } else { setFilteredDotString(null); // Clear filtered graph if no filter is set } - }, [csvData, filter, selfLoops, minVisits]); - + }, [csvData, filter, selfLoops, minVisits, selectedSequence]); + const handleSequenceSelect = (sequence: string) => { + setSelectedSequence(sequence); + }; return ( -
+
+ {/**/} -
- {dotString && } - - {filteredDotString && - } +
+ +
+ {dotString && ( + + )} + {filteredDotString && ( + + )} +
- ) + ); } -export default GraphvizParent \ No newline at end of file + +export default GraphvizParent; diff --git a/src/components/graphvizProcessing.ts b/src/components/GraphvizProcessing.ts similarity index 52% rename from src/components/graphvizProcessing.ts rename to src/components/GraphvizProcessing.ts index 14381bd..d1d5200 100644 --- a/src/components/graphvizProcessing.ts +++ b/src/components/GraphvizProcessing.ts @@ -1,4 +1,5 @@ import Papa from 'papaparse'; +import {get} from "lodash"; interface CSVRow { 'Session Id': string; @@ -6,28 +7,28 @@ interface CSVRow { 'Step Name': string; 'Outcome': string; 'CF (Workspace Progress Status)': string; - } // Function to load and sort data export const loadAndSortData = (csvData: string): CSVRow[] => { - const parsedData = Papa.parse(csvData, { + // Step 1: Parse the CSV data using PapaParse + const parsedData = Papa.parse(csvData, { header: true, skipEmptyLines: true }).data; - // Step 2: Transform data to replace NaN values + // Step 2: Transform data to replace missing Step Names with a default value const transformedData = parsedData.map(row => { return { 'Session Id': row['Session Id'], 'Time': row['Time'], - 'Step Name': row['Step Name'] || 'DoneButton', //TODO: Nan only shows up on selection "Done Button" + 'Step Name': row['Step Name'] || 'DoneButton', // Default value for missing Step Names 'Outcome': row['Outcome'], 'CF (Workspace Progress Status)': row['CF (Workspace Progress Status)'], }; }); - + // Step 3: Sort the transformed data by Session Id and Time return transformedData.sort((a, b) => { if (a['Session Id'] === b['Session Id']) { return new Date(a['Time']).getTime() - new Date(b['Time']).getTime(); @@ -36,8 +37,9 @@ export const loadAndSortData = (csvData: string): CSVRow[] => { }); }; - -export const createStepSequences = (sortedData: CSVRow[], selfLoops:boolean): { [key: string]: string[] } => { +// Function to create step sequences from sorted data +export const createStepSequences = (sortedData: CSVRow[], selfLoops: boolean): { [key: string]: string[] } => { + // Iterate over sorted data to build step sequences return sortedData.reduce((acc, row) => { const sessionId = row['Session Id']; if (!acc[sessionId]) { @@ -45,22 +47,18 @@ export const createStepSequences = (sortedData: CSVRow[], selfLoops:boolean): { } const stepName = row['Step Name']; - if (!selfLoops) { - if (!acc[sessionId].includes(stepName)) { - acc[sessionId].push(stepName); - } - } else { + // Add step to sequence based on whether self-loops are allowed + if (selfLoops || acc[sessionId].length === 0 || acc[sessionId][acc[sessionId].length - 1] !== stepName) { acc[sessionId].push(stepName); } - // console.log(sessionId, acc[sessionId]) return acc; }, {} as { [key: string]: string[] }); }; -// Function to create outcome sequences +// Function to create outcome sequences from sorted data export const createOutcomeSequences = (sortedData: CSVRow[]): { [key: string]: string[] } => { - // console.log(sortedData) + // Iterate over sorted data to build outcome sequences return sortedData.reduce((acc, row) => { const sessionId = row['Session Id']; if (!acc[sessionId]) { @@ -71,6 +69,49 @@ export const createOutcomeSequences = (sortedData: CSVRow[]): { [key: string]: s }, {} as { [key: string]: string[] }); }; +// export function getTopSequences(stepSequences: any, topN: number = 5) { +// // const topSequences = Object.values(stepSequences) +// // .sort((a, b) => stepSequences[b].length - stepSequences[a].length) +// // .slice(0, topN); +// +// const sequenceCounts = Object.entries(stepSequences).reduce((acc: any, [key, value]) => { +// acc[key] = value.count; +// return acc; +// }, {}); +// console.log("counts: " + sequenceCounts[stepSequences[0]]) +// const sortedSequences = Object.entries(sequenceCounts) +// .sort(([, a], [, b]) => b - a) +// .slice(0, topN); +// console.log("sorted sequences: " + sequenceCounts[sortedSequences[0]]) +// // return sortedSequences.map(([sequence]) => sequence); +// return topSequences +// } +export function getTopSequences(stepSequences: any, topN: number = 5) { + // Create a frequency map to count how many times each unique sequence (list) occurs + const sequenceCounts: { [sequence: string]: number } = {}; + + // Iterate over the values (which are lists) of the stepSequences dictionary + Object.values(stepSequences).forEach((sequenceList: string[]) => { + const sequenceKey = JSON.stringify(sequenceList); // Convert the list to a string key + + if (sequenceCounts[sequenceKey]) { + sequenceCounts[sequenceKey]++; + } else { + sequenceCounts[sequenceKey] = 1; + } + }); + + // Sort the sequences based on their counts in descending order and take the top N + const sortedSequences = Object.entries(sequenceCounts) + .sort(([, countA], [, countB]) => countB - countA) + .slice(0, topN); + + // Instead of returning the actual sequences, return "Count 1" through "Count 5" + // const topSequences = sortedSequences.map((sequence, index) => `Count ${index + 1}`); + const topSequences = sortedSequences.map(([sequenceKey, index]) => JSON.parse(sequenceKey, 'Count ${index + 1}')); + return topSequences; +} + interface EdgeCounts { edgeCounts: { [key: string]: number }; totalNodeEdges: { [key: string]: number }; @@ -78,18 +119,30 @@ interface EdgeCounts { edgeOutcomeCounts: { [key: string]: { [outcome: string]: number } }; } -// Function to count edges +// Function to count edges between steps export const countEdges = ( stepSequences: { [key: string]: string[] }, outcomeSequences: { [key: string]: string[] } -): EdgeCounts => { +): { + totalNodeEdges: { [p: string]: number }; + edgeOutcomeCounts: { [p: string]: { [p: string]: number } }; + maxEdgeCount: number; + ratioEdges: { [p: string]: number }; + edgeCounts: { [p: string]: number }; + top5Sequences: string[]; +} => { const edgeCounts: { [key: string]: number } = {}; const totalNodeEdges: { [key: string]: number } = {}; const ratioEdges: { [key: string]: number } = {}; const edgeOutcomeCounts: { [key: string]: { [outcome: string]: number } } = {}; + let maxEdgeCount = 0; + + const top5Sequences = getTopSequences(stepSequences, 5) + // Process edges for all sequences Object.keys(stepSequences).forEach((sessionId) => { const steps = stepSequences[sessionId]; + // console.log(steps) const outcomes = outcomeSequences[sessionId]; if (steps.length < 2) return; @@ -104,6 +157,11 @@ export const countEdges = ( edgeOutcomeCounts[edgeKey] = edgeOutcomeCounts[edgeKey] || {}; edgeOutcomeCounts[edgeKey][outcome] = (edgeOutcomeCounts[edgeKey][outcome] || 0) + 1; totalNodeEdges[currentStep] = (totalNodeEdges[currentStep] || 0) + 1; + + // Track the maximum edge count + if (edgeCounts[edgeKey] > maxEdgeCount) { + maxEdgeCount = edgeCounts[edgeKey]; + } } }); @@ -111,49 +169,63 @@ export const countEdges = ( const [start] = edge.split('->'); ratioEdges[edge] = edgeCounts[edge] / (totalNodeEdges[start] || 0); }); - // console.log("edgeOutcomeCounts: ", edgeOutcomeCounts, "\nratioEdges: ", ratioEdges ) - return { edgeCounts, totalNodeEdges, ratioEdges, edgeOutcomeCounts }; + + return {edgeCounts, totalNodeEdges, ratioEdges, edgeOutcomeCounts, maxEdgeCount, top5Sequences}; }; + +// // Function to normalize edge thicknesses based on their ratio +// export function normalizeThicknesses( +// ratioEdges: { [key: string]: number }, +// maxThickness: number +// ): { [key: string]: number } { +// const normalized: { [key: string]: number } = {}; +// const maxRatio = Math.max(...Object.values(ratioEdges), 1); // Avoid division by zero +// +// // Scale edge thicknesses to a maximum value +// Object.keys(ratioEdges).forEach((edge) => { +// const ratio = ratioEdges[edge]; +// normalized[edge] = (ratio / maxRatio) * maxThickness; +// }); +// +// return normalized; +// } + +// // Function to normalize edge thicknesses based the full graph export function normalizeThicknesses( - ratioEdges: { [key: string]: number }, + edgeCounts: { [key: string]: number }, + maxEdgeCount: number, maxThickness: number ): { [key: string]: number } { const normalized: { [key: string]: number } = {}; - const maxRatio = Math.max(...Object.values(ratioEdges), 1); // Avoid division by zero - Object.keys(ratioEdges).forEach((edge) => { - const ratio = ratioEdges[edge]; - - - normalized[edge] = (ratio / maxRatio) * maxThickness; + Object.keys(edgeCounts).forEach((edge) => { + const count = edgeCounts[edge]; + normalized[edge] = (count / maxEdgeCount) * maxThickness; }); return normalized; } + +// Function to calculate the color of a node based on its rank in the most common sequence function calculateColor(rank: number, totalSteps: number): string { - // Calculate the ratio between 0 and 1 const ratio = rank / totalSteps; - const white = { r: 255, g: 255, b: 255 }; // White color - const lightBlue = { r: 0, g: 166, b: 255 }; // Light Blue color + const white = {r: 255, g: 255, b: 255}; + const lightBlue = {r: 0, g: 166, b: 255}; - // Interpolate between white and light blue const r = Math.round(white.r * (1 - ratio) + lightBlue.r * ratio); const g = Math.round(white.g * (1 - ratio) + lightBlue.g * ratio); const b = Math.round(white.b * (1 - ratio) + lightBlue.b * ratio); - // Convert RGB to hexadecimal const toHex = (value: number) => value.toString(16).padStart(2, '0'); const color = `#${toHex(r)}${toHex(g)}${toHex(b)}`; - - // console.log("Color: ", r, g, b, toHex(r), toHex(g), toHex(b), color); - return color; } +// Function to calculate the color of an edge based on its outcome distribution function calculateEdgeColors(outcomes: { [outcome: string]: number }): string { const colorMap: { [key: string]: string } = { 'ERROR': '#ff0000', // Red @@ -184,30 +256,38 @@ function calculateEdgeColors(outcomes: { [outcome: string]: number }): string { return `#${Math.round(weightedR).toString(16).padStart(2, '0')}${Math.round(weightedG).toString(16).padStart(2, '0')}${Math.round(weightedB).toString(16).padStart(2, '0')}90`; } +// Function to generate a Graphviz DOT string for visualization export function generateDotString( normalizedThicknesses: { [key: string]: number }, - mostCommonSequence: string[], + // mostCommonSequence: string[], ratioEdges: { [key: string]: number }, edgeOutcomeCounts: EdgeCounts['edgeOutcomeCounts'], edgeCounts: EdgeCounts['edgeCounts'], totalNodeEdges: EdgeCounts['totalNodeEdges'], threshold: number, min_visits: number, + selectedSequence: string ): string { + const stepsInSelectedSequence = selectedSequence//.split('->'); + // console.log(mostCommonSequence) + console.log("selectedSequence" + selectedSequence) + // console.log(selectedSequence[stepsInSelectedSequence]) + const totalSteps = selectedSequence.length; + console.log("totalSteps" + totalSteps) + // Create node definitions in the DOT string let dotString = 'digraph G {\n'; - const totalSteps = mostCommonSequence.length; - // console.log(mostCommonSequence, totalSteps) for (let rank = 0; rank < totalSteps; rank++) { - const step = mostCommonSequence[rank]; - const color = calculateColor(rank + 1, totalSteps); + const step = stepsInSelectedSequence[rank]; + const color = calculateColor(rank, totalSteps); const node_tooltip = `Rank:\n\t\t ${rank + 1}\nColor:\n\t\t ${color}`; dotString += ` "${step}" [rank=${rank + 1}, style=filled, fillcolor="${color}", tooltip="${node_tooltip}"];\n`; } + // Create edge definitions in the DOT string based on normalized thickness and thresholds for (const edge of Object.keys(normalizedThicknesses)) { if (normalizedThicknesses[edge] >= threshold) { - const [currentStep, nextStep] = edge.split('->');// as EdgeKey; + const [currentStep, nextStep] = edge.split('->'); const thickness = normalizedThicknesses[edge]; const outcomes = edgeOutcomeCounts[edge] || {}; const edgeCount = edgeCounts[edge] || 0; @@ -216,18 +296,21 @@ export function generateDotString( const outcomesStr = Object.entries(outcomes) .map(([outcome, count]) => `${outcome}: ${count}`) .join('\n\t\t '); - if (edgeCount > min_visits){ + + if (edgeCount > min_visits) { const tooltip = `${currentStep} to ${nextStep}\n` - + `- Edge Count: \n\t\t ${edgeCount}\n` - + `- Total Count for ${currentStep}: \n\t\t${totalCount}\n` - + `- Ratio: \n\t\t${((ratioEdges[edge] || 0) * 100).toFixed(2)}% of students at ${currentStep} go to ${nextStep}\n` - + `- Outcomes: \n\t\t ${outcomesStr}\n` - + `- Color Codes: \n\t\t Hex: ${color}\n\t\t RGB: ${[parseInt(color.substring(1, 3), 16), parseInt(color.substring(3, 5), 16), parseInt(color.substring(5, 7), 16)]}`; + + `- Edge Count: \n\t\t ${edgeCount}\n` + + `- Total Count for ${currentStep}: \n\t\t${totalCount}\n` + + `- Ratio: \n\t\t${((ratioEdges[edge] || 0) * 100).toFixed(2)}% of students at ${currentStep} go to ${nextStep}\n` + + `- Outcomes: \n\t\t ${outcomesStr}\n` + + `- Color Codes: \n\t\t Hex: ${color}\n\t\t RGB: ${[parseInt(color.substring(1, 3), 16), parseInt(color.substring(3, 5), 16), parseInt(color.substring(5, 7), 16)]}`; dotString += ` "${currentStep}" -> "${nextStep}" [penwidth=${thickness}, color="${color}", tooltip="${tooltip}"];\n`; - }} + } + } } dotString += '}'; return dotString; } + diff --git a/src/components/SequenceSelector.tsx b/src/components/SequenceSelector.tsx new file mode 100644 index 0000000..df0e9fd --- /dev/null +++ b/src/components/SequenceSelector.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +interface SequenceSelectorProps { + sequences: string[]; + selectedSequence: string; + onSequenceSelect: (sequence: string) => void; +} + +const SequenceSelector: React.FC = ({ + sequences, + selectedSequence, + onSequenceSelect, +}) => { + if (sequences.length === 0) { + return
No sequences available
; // Display a message when no sequences are present + } + + return ( +
+ +
+ ); +}; + +export default SequenceSelector; diff --git a/src/components/slider.tsx b/src/components/slider.tsx index 7ece586..f495e8b 100644 --- a/src/components/slider.tsx +++ b/src/components/slider.tsx @@ -1,32 +1,84 @@ +// import React from 'react'; +// +// interface SliderProps { +// min: number; +// max: number; +// step?: number; +// value: number; +// onChange: (value: number) => void; +// } +// +// const Slider: React.FC = ({ min, max, step = 1, value, onChange }) => { +// const handleChange = (event: React.ChangeEvent) => { +// onChange(Number(event.target.value)); +// }; +// +// return ( +//
+// +//

Minimum # of Edge Visits to Display: {value}

+//
+// ); +// }; +// +// export default Slider; + + import React from 'react'; interface SliderProps { - min: number; - max: number; - step?: number; - value: number; - onChange: (value: number) => void; + min: number; + max: number; + step?: number; + value: number; + onChange: (value: number) => void; } -const Slider: React.FC = ({ min, max, step = 1, value, onChange }) => { - const handleChange = (event: React.ChangeEvent) => { - onChange(Number(event.target.value)); - }; - - return ( -
- -

Minimum # of Edge Visits for Visualization: {value}

-
- ); +const Slider: React.FC = ({min, max, step = 1, value, onChange}) => { + const handleSliderChange = (event: React.ChangeEvent) => { + const newValue = Number(event.target.value); + onChange(newValue); + }; + + const handleInputChange = (event: React.ChangeEvent) => { + const newValue = Number(event.target.value); + if (newValue >= min && newValue <= max) { + onChange(newValue); + } + }; + + return ( +
+

Minimum # of Edge Visits to Display: {value}

+ + + + +
+ ); }; export default Slider; From 0a3b4742d6c55f98e5606a11b167d19b40d6d5b6 Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Fri, 23 Aug 2024 13:47:41 -0500 Subject: [PATCH 21/54] Sequence coloring isn't working but everything (i think) is close to working again --- src/App.tsx | 45 +- src/GraphvizContainer.css | 3 + src/components/GraphvizParent.tsx | 662 +++++++++++++++++++++++---- src/components/GraphvizProcessing.ts | 390 ++++++++++++++-- src/components/SequenceSelector.tsx | 4 +- 5 files changed, 966 insertions(+), 138 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 881c7c8..f247273 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import './App.css'; -import React, {useCallback, useState} from 'react'; +import React, { useCallback, useState } from 'react'; import Upload from "@/components/Upload.tsx"; import GraphvizParent from "@/components/GraphvizParent.tsx"; import FilterComponent from './components/FilterComponent.tsx'; @@ -11,34 +11,41 @@ const App: React.FC = () => { const [csvData, setCsvData] = useState(''); const [filter, setFilter] = useState(''); const [selfLoops, setSelfLoops] = useState(true); - const [minVisits, setMinVisits] = useState(30); - const [topSequences, setTopSequences] = useState([]); - const [selectedSequence, setSelectedSequence] = useState(topSequences[0]); - const handleSelectSequence = (sequence: string) => { - setSelectedSequence(selectedSequence); - // Add your logic here to update the node coloring based on the selected sequence + const [minVisits, setMinVisits] = useState(10); + const [topSequences, setTopSequences] = useState([]); + const [selectedSequence, setSelectedSequence] = useState(topSequences[0]); + + const handleSelectSequence = (selectedSequence: string[]) => { + setSelectedSequence(selectedSequence); // Fix: Use the correct parameter to update the state console.log(`Selected sequence: ${sequence}`); }; + const handleToggle = () => setSelfLoops(!selfLoops); const handleSlider = (value: number) => setMinVisits(value); const handleDataProcessed = (uploadedCsvData: string) => setCsvData(uploadedCsvData); - const handleTopSequencesUpdate = useCallback((sequences: string[]) => { - setTopSequences(sequences); - console.log("AHHHH: "+topSequences) - }, [topSequences]) + + // Fix: Remove `topSequences` dependency from `useCallback` to avoid unnecessary re-creations + // const handleTopSequencesUpdate = useCallback((sequences: string[][]) => { + // setTopSequences(sequences); + // }, [selectedSequence]); return (

Path Analysis Tool

- - - - - - + + + + + +
); }; -export default App; \ No newline at end of file +export default App; diff --git a/src/GraphvizContainer.css b/src/GraphvizContainer.css index a3ea593..0986275 100644 --- a/src/GraphvizContainer.css +++ b/src/GraphvizContainer.css @@ -4,6 +4,9 @@ align-items: center; flex-direction: row; /* Change to row to align items horizontally */ width: 100%; /* Ensure the container takes full width */ + margin-top: -20px; /* Move the container up by 20px */ + position: relative; + transform: translateY(-20px) translateX(30px); /* Move the container up by 20px */ } .graphs { diff --git a/src/components/GraphvizParent.tsx b/src/components/GraphvizParent.tsx index 7bd9734..a086925 100644 --- a/src/components/GraphvizParent.tsx +++ b/src/components/GraphvizParent.tsx @@ -1,24 +1,505 @@ -import {useEffect, useState} from "react"; -import ErrorBoundary from "@/components/errorBoundary.tsx"; -import Graphviz from 'graphviz-react'; +// import {useEffect, useState} from "react"; +// import ErrorBoundary from "@/components/errorBoundary.tsx"; +// import Graphviz from 'graphviz-react'; +// import { +// loadAndSortData, +// createStepSequences, +// createOutcomeSequences, +// countEdges, +// normalizeThicknesses, +// generateDotString, calculateColor +// } from './GraphvizProcessing'; +// import '../GraphvizContainer.css'; +// +// // import SequenceSelector from './SequenceSelector'; +// +// interface GraphvizParentProps { +// csvData: string; +// filter: string; +// selfLoops: boolean; +// minVisits: number; +// onTopSequencesUpdate: (sequences: string[]) => void; +// // selectedSequence: string; +// } +// +// const GraphvizParent: React.FC = ({ +// csvData, +// filter, +// selfLoops, +// minVisits, +// onTopSequencesUpdate, +// // selectedSequence, +// }) => { +// const [dotString, setDotString] = useState(''); +// const [filteredDotString, setFilteredDotString] = useState(''); +// const [topSequences, setTopSequences] = useState([]); +// const [selectedSequence, setSelectedSequence] = useState([]); +// +// // Function to update the node colors based on the selected sequence +// +// const applySequenceColors = (dotStr: string, sequence: string[]) => { +// return dotStr.replace(/(\w+ \[)(.*?\])/g, (match, p1, p2) => { +// const nodeName = match.match(/(\w+) \[/)?.[1]; +// +// // Find the rank of the node in the selected sequence +// const rank = sequence.indexOf(nodeName!) + 1; // +1 because index is 0-based +// const totalSteps = sequence.length; +// +// // Only calculate color if the node exists in the sequence +// if (rank > 0) { +// const color = calculateColor(rank, totalSteps); +// return `${p1}style=filled, fillcolor="${color}", ${p2}`; +// } else { +// return match; // return the original string if node is not in sequence +// } +// }); +// }; +// +// +// useEffect(() => { +// if (!csvData) return; // Skip processing if no CSV data is available +// +// // Step 1: Load and sort the data +// const sortedData = loadAndSortData(csvData); +// +// // Step 2: Generate the sequences for steps and outcomes +// const stepSequences = createStepSequences(sortedData, selfLoops); +// const outcomeSequences = createOutcomeSequences(sortedData); +// +// // Step 3: Count edges and normalize thicknesses +// const { +// edgeCounts, +// totalNodeEdges, +// ratioEdges, +// edgeOutcomeCounts, +// maxEdgeCount, +// top5Sequences +// } = countEdges(stepSequences, outcomeSequences); +// setTopSequences(top5Sequences) +// console.log('TEST: ' + top5Sequences) +// setSelectedSequence(top5Sequences[0]) +// const normalizedThicknesses = normalizeThicknesses(edgeCounts, maxEdgeCount, 10); +// +// // Step 4: Find the most common sequences +// // const mostCommonSequenceKey = Object.keys(stepSequences) +// // .reduce((a, b) => stepSequences[a].length > stepSequences[b].length ? a : b); +// // const mostCommonSequence = stepSequences[mostCommonSequenceKey]; +// // const topSequences = Object.keys(stepSequences) +// // .sort((a, b) => stepSequences[b].length - stepSequences[a].length) +// // .slice(0, 5); +// // console.log(stepSequences) +// // const top5Sequences = getTopSequences(stepSequences,5) +// onTopSequencesUpdate(top5Sequences); +// +// setTopSequences(top5Sequences) +// console.log("GVP67:" + topSequences) +// // Call the update function to pass top sequences to App component +// // Step 5: Generate the DOT string for the unfiltered graph +// let dotStr = generateDotString( +// normalizedThicknesses, +// // mostCommonSequence, +// ratioEdges, +// edgeOutcomeCounts, +// edgeCounts, +// totalNodeEdges, +// 1, +// minVisits, +// selectedSequence +// ); +// // Step 5: Apply initial color based on the selected sequence +// +// if (selectedSequence) { +// +// dotStr = applySequenceColors(dotStr, selectedSequence); +// +// } +// +// setDotString(dotStr); +// +// // Step 6: Generate the filtered graph if a filter is provided +// if (filter) { +// const filteredData = sortedData.filter(row => row['CF (Workspace Progress Status)'] === filter); +// const filteredStepSequences = createStepSequences(filteredData, selfLoops); +// const filteredOutcomeSequences = createOutcomeSequences(filteredData); +// +// const { +// edgeCounts: filteredEdgeCounts, +// totalNodeEdges: filteredTotalNodeEdges, +// ratioEdges: filteredRatioEdges, +// edgeOutcomeCounts: filteredEdgeOutcomeCounts, +// maxEdgeCount: filteredMaxEdgeCount, +// top5Sequences, +// } = countEdges(filteredStepSequences, filteredOutcomeSequences); +// +// const filteredNormalizedThicknesses = normalizeThicknesses(filteredEdgeCounts, filteredMaxEdgeCount, 10); +// // const filteredMostCommonSequenceKey = Object.keys(filteredStepSequences) +// // .reduce((a, b) => filteredStepSequences[a].length > filteredStepSequences[b].length ? a : b); +// // const filteredMostCommonSequence = filteredStepSequences[filteredMostCommonSequenceKey]; +// +// // Generate the DOT string for the filtered graph +// let filteredDotStr = generateDotString( +// filteredNormalizedThicknesses, +// // filteredMostCommonSequence, +// filteredRatioEdges, +// filteredEdgeOutcomeCounts, +// filteredEdgeCounts, +// filteredTotalNodeEdges, +// 1, +// minVisits, +// selectedSequence +// ); +// if (selectedSequence != top5Sequences[0]) { +// filteredDotStr = applySequenceColors(dotStr, selectedSequence); +// } +// setFilteredDotString(filteredDotStr); +// } else { +// setFilteredDotString(null); // Clear filtered graph if no filter is set +// } +// +// }, [csvData, filter, selfLoops, minVisits, selectedSequence]); +// +// const handleSequenceSelect = (sequence: string) => { +// setSelectedSequence(sequence); +// }; +// +// return ( +//
+// {/**/} +// +//
+// +//
+// {dotString && ( +// +// )} +// {filteredDotString && ( +// +// )} +//
+//
+//
+//
+// ); +// } +// +// export default GraphvizParent; + + +// import {useEffect, useState} from "react"; +// import ErrorBoundary from "@/components/errorBoundary.tsx"; +// import Graphviz from 'graphviz-react'; +// import { +// loadAndSortData, +// createStepSequences, +// createOutcomeSequences, +// countEdges, +// normalizeThicknesses, +// generateDotString, calculateColor +// } from './GraphvizProcessing'; +// import '../GraphvizContainer.css'; +// +// interface GraphvizParentProps { +// csvData: string; +// filter: string; +// selfLoops: boolean; +// minVisits: number; +// onTopSequencesUpdate: (sequences: string[]) => void; +// } +// +// const GraphvizParent: React.FC = ({ +// csvData, +// filter, +// selfLoops, +// minVisits, +// onTopSequencesUpdate, +// }) => { +// const [dotString, setDotString] = useState(''); +// const [filteredDotString, setFilteredDotString] = useState(null); +// const [topSequences, setTopSequences] = useState([]); +// const [selectedSequence, setSelectedSequence] = useState([]); +// +// const applySequenceColors = (dotStr: string, sequence: string[]) => { +// return dotStr.replace(/(\w+ \[)(.*?\])/g, (match, p1, p2) => { +// const nodeName = match.match(/(\w+) \[/)?.[1]; +// const rank = sequence.indexOf(nodeName!) + 1; +// const totalSteps = sequence.length; +// +// if (rank > 0) { +// const color = calculateColor(rank, totalSteps); +// return `${p1}style=filled, fillcolor="${color}", ${p2}`; +// } else { +// return match; +// } +// }); +// }; +// +// useEffect(() => { +// if (!csvData) return; +// +// const sortedData = loadAndSortData(csvData); +// const stepSequences = createStepSequences(sortedData, selfLoops); +// const outcomeSequences = createOutcomeSequences(sortedData); +// +// const { +// edgeCounts, +// totalNodeEdges, +// ratioEdges, +// edgeOutcomeCounts, +// maxEdgeCount, +// top5Sequences, +// } = countEdges(stepSequences, outcomeSequences); +// +// setTopSequences(top5Sequences); +// setSelectedSequence(top5Sequences[0][0]); +// +// const normalizedThicknesses = normalizeThicknesses(edgeCounts, maxEdgeCount, 10); +// +// let dotStr = generateDotString( +// normalizedThicknesses, +// ratioEdges, +// edgeOutcomeCounts, +// edgeCounts, +// totalNodeEdges, +// 1, +// minVisits, +// top5Sequences[0] // Use the first sequence directly here +// ); +// +// dotStr = applySequenceColors(dotStr, top5Sequences[0]); +// setDotString(dotStr); +// +// if (filter) { +// const filteredData = sortedData.filter(row => row['CF (Workspace Progress Status)'] === filter); +// const filteredStepSequences = createStepSequences(filteredData, selfLoops); +// const filteredOutcomeSequences = createOutcomeSequences(filteredData); +// +// const { +// edgeCounts: filteredEdgeCounts, +// totalNodeEdges: filteredTotalNodeEdges, +// ratioEdges: filteredRatioEdges, +// edgeOutcomeCounts: filteredEdgeOutcomeCounts, +// maxEdgeCount: filteredMaxEdgeCount, +// top5Sequences: filteredTop5Sequences, +// } = countEdges(filteredStepSequences, filteredOutcomeSequences); +// +// const filteredNormalizedThicknesses = normalizeThicknesses(filteredEdgeCounts, filteredMaxEdgeCount, 10); +// +// let filteredDotStr = generateDotString( +// filteredNormalizedThicknesses, +// filteredRatioEdges, +// filteredEdgeOutcomeCounts, +// filteredEdgeCounts, +// filteredTotalNodeEdges, +// 1, +// minVisits, +// selectedSequence // Apply selected sequence here +// ); +// +// filteredDotStr = applySequenceColors(filteredDotStr, selectedSequence); +// setFilteredDotString(filteredDotStr); +// } else { +// setFilteredDotString(null); +// } +// +// }, [csvData, filter, selfLoops, minVisits]); +// +// return ( +//
+// +//
+// {dotString && ( +// +// )} +// {filteredDotString && ( +// +// )} +//
+//
+//
+// ); +// } +// +// export default GraphvizParent; + +// import React, {useEffect, useState} from 'react'; +// import { +// loadAndSortData, +// createStepSequences, +// createOutcomeSequences, +// countEdges, +// normalizeThicknesses, +// generateDotString, +// calculateColor +// } from './GraphvizProcessing'; +// import ErrorBoundary from "@/components/errorBoundary.tsx"; +// import Graphviz from "graphviz-react"; +// +// +// const GraphvizParent: React.FC = () => { +// const [selectedSequence, setSelectedSequence] = useState([]); +// const [dotString, setDotString] = useState(''); +// const [filteredDotString, setFilteredDotString] = useState(null); // Use null for no filter +// const [edgeCounts, setEdgeCounts] = useState({}); +// const [maxEdgeCount, setMaxEdgeCount] = useState(0); +// const [ratioEdges, setRatioEdges] = useState({}); +// const [edgeOutcomeCounts, setEdgeOutcomeCounts] = useState({}); +// const [totalNodeEdges, setTotalNodeEdges] = useState< {[p: string]: number; }>({}); +// const [filterCriteria, setFilterCriteria] = useState(null); // Track filter criteria +// const [csvData, setCsvData] = useState(''); // State for CSV data +// const [selfLoops, setSelfLoops] = useState(false); // State for self-loops +// const [minVisits, setMinVisits] = useState(0); // State for minimum visits +// +// const applySequenceColors = (dotStr: string, sequence: string[]) => { +// return dotStr.replace(/(\w+ \[)(.*?\])/g, (match, p1, p2) => { +// const nodeName = match.match(/(\w+) \[/)?.[1]; +// const rank = sequence.indexOf(nodeName!) + 1; +// const totalSteps = sequence.length; +// +// if (rank > 0) { +// const color = calculateColor(rank, totalSteps); +// return `${p1}style=filled, fillcolor="${color}", ${p2}`; +// } else { +// return match; +// } +// }); +// }; +// useEffect(() => { +// const fetchData = async () => { +// if (!csvData) return; +// else setCsvData(csvData); +// const sortedData = loadAndSortData(csvData); +// const stepSequences = createStepSequences(sortedData, selfLoops); +// const outcomeSequences = createOutcomeSequences(sortedData); +// +// const { edgeCounts, totalNodeEdges, ratioEdges, edgeOutcomeCounts, maxEdgeCount, top5Sequences } = countEdges(stepSequences, outcomeSequences); +// +// setEdgeCounts(edgeCounts); +// setTotalNodeEdges(totalNodeEdges); +// setRatioEdges(ratioEdges); +// setEdgeOutcomeCounts(edgeOutcomeCounts); +// setMaxEdgeCount(maxEdgeCount); +// +// // Set the initial selected sequence +// const initialSequence = top5Sequences[0][0]; // Adjust based on your top sequences structure +// setSelectedSequence(initialSequence); +// +// // Generate the initial DOT string +// const normalizedThicknesses = normalizeThicknesses(edgeCounts, maxEdgeCount, 10); +// let newDotString = generateDotString(normalizedThicknesses, ratioEdges, edgeOutcomeCounts, edgeCounts, totalNodeEdges, 0, minVisits, initialSequence); +// newDotString = applySequenceColors(newDotString, initialSequence); +// setDotString(newDotString); +// }; +// +// fetchData(); +// }, []); +// +// // Effect to regenerate the non-filtered DOT string when dependencies change +// useEffect(() => { +// const normalizedThicknesses = normalizeThicknesses(edgeCounts, maxEdgeCount, 10); +// let newDotString = generateDotString(normalizedThicknesses, ratioEdges, edgeOutcomeCounts, edgeCounts, totalNodeEdges, 0, minVisits, selectedSequence); +// newDotString = applySequenceColors(newDotString, selectedSequence); +// setDotString(newDotString); +// }, [selectedSequence, edgeCounts, maxEdgeCount, ratioEdges, edgeOutcomeCounts, totalNodeEdges, minVisits, selfLoops]); +// +// // Effect to regenerate filtered DOT string when filterCriteria or selfLoops change +// useEffect(() => { +// if (filterCriteria) { +// const filteredData = csvData.filter(row => row['CF (Workspace Progress Status)'] === filterCriteria); +// const filteredStepSequences = createStepSequences(filteredData, selfLoops); +// const filteredOutcomeSequences = createOutcomeSequences(filteredData); +// +// const { +// edgeCounts: filteredEdgeCounts, +// totalNodeEdges: filteredTotalNodeEdges, +// ratioEdges: filteredRatioEdges, +// edgeOutcomeCounts: filteredEdgeOutcomeCounts, +// maxEdgeCount: filteredMaxEdgeCount, +// } = countEdges(filteredStepSequences, filteredOutcomeSequences); +// +// const filteredNormalizedThicknesses = normalizeThicknesses(filteredEdgeCounts, filteredMaxEdgeCount, 10); +// +// let filteredDotStr = generateDotString( +// filteredNormalizedThicknesses, +// filteredRatioEdges, +// filteredEdgeOutcomeCounts, +// filteredEdgeCounts, +// filteredTotalNodeEdges, +// 1, +// minVisits, +// selectedSequence // Apply selected sequence here +// ); +// +// filteredDotStr = applySequenceColors(filteredDotStr, selectedSequence); +// setFilteredDotString(filteredDotStr); +// } else { +// setFilteredDotString(null); // Set to null if no filter is applied +// } +// }, [csvData, filterCriteria, selfLoops, minVisits, selectedSequence]); +// +// +// return ( +//
+// +//
+// {dotString && ( +// +// )} +// {filteredDotString && ( +// +// )} +//
+//
+//
+// ); +// } +// +// export default GraphvizParent; + +import React, {useEffect, useState} from 'react'; import { - loadAndSortData, + generateDotString, + normalizeThicknesses, + countEdges, createStepSequences, createOutcomeSequences, - countEdges, - normalizeThicknesses, - generateDotString, getTopSequences, + loadAndSortData } from './GraphvizProcessing'; -import '../GraphvizContainer.css'; -import SequenceSelector from './SequenceSelector'; +import Graphviz from "graphviz-react"; +import ErrorBoundary from "@/components/errorBoundary.tsx"; interface GraphvizParentProps { csvData: string; - filter: string; + filter: string | null; selfLoops: boolean; minVisits: number; - onTopSequencesUpdate: (sequences: string[]) => void; - // selectedSequence: string; + selectedSequence: string[]; +} + +export interface SequenceCount { + sequence: string[]; // or whatever type your steps are (e.g., number[]) + count: number; } const GraphvizParent: React.FC = ({ @@ -26,25 +507,19 @@ const GraphvizParent: React.FC = ({ filter, selfLoops, minVisits, - onTopSequencesUpdate, - // selectedSequence, + }) => { - const [dotString, setDotString] = useState(''); - const [filteredDotString, setFilteredDotString] = useState(''); - const [topSequences, setTopSequences] = useState([]); - const [selectedSequence, setSelectedSequence] = useState(''); + const [dotString, setDotString] = useState(null); + const [filteredDotString, setFilteredDotString] = useState(null); + const [top5Sequences, setTop5Sequences] = useState('') + const [selectedSequence, setSelectedSequence] = useState([]) + const [selectedSequenceIndex, setSelectedSequenceIndex] = useState(0); useEffect(() => { - if (!csvData) return; // Skip processing if no CSV data is available - - // Step 1: Load and sort the data const sortedData = loadAndSortData(csvData); - - // Step 2: Generate the sequences for steps and outcomes const stepSequences = createStepSequences(sortedData, selfLoops); const outcomeSequences = createOutcomeSequences(sortedData); - // Step 3: Count edges and normalize thicknesses const { edgeCounts, totalNodeEdges, @@ -53,40 +528,32 @@ const GraphvizParent: React.FC = ({ maxEdgeCount, top5Sequences } = countEdges(stepSequences, outcomeSequences); - setTopSequences(top5Sequences) - setSelectedSequence(top5Sequences[0]) + + const formattedTop5Sequences = top5Sequences.map(([sequenceKey, count]) => ({ + sequence: JSON.parse(sequenceKey), + count, + })); + setTop5Sequences(formattedTop5Sequences); + const normalizedThicknesses = normalizeThicknesses(edgeCounts, maxEdgeCount, 10); - // Step 4: Find the most common sequences - // const mostCommonSequenceKey = Object.keys(stepSequences) - // .reduce((a, b) => stepSequences[a].length > stepSequences[b].length ? a : b); - // const mostCommonSequence = stepSequences[mostCommonSequenceKey]; - // const topSequences = Object.keys(stepSequences) - // .sort((a, b) => stepSequences[b].length - stepSequences[a].length) - // .slice(0, 5); - // console.log(stepSequences) - // const top5Sequences = getTopSequences(stepSequences,5) - onTopSequencesUpdate(top5Sequences); - - setTopSequences(top5Sequences) - console.log("GVP67:" + topSequences) - // Call the update function to pass top sequences to App component - // Step 5: Generate the DOT string for the unfiltered graph - const dotStr = generateDotString( + let generatedDotStr = generateDotString( normalizedThicknesses, - // mostCommonSequence, ratioEdges, edgeOutcomeCounts, edgeCounts, totalNodeEdges, 1, minVisits, - selectedSequence + formattedTop5Sequences[selectedSequenceIndex]?.sequence || [] ); - setDotString(dotStr); - // Step 6: Generate the filtered graph if a filter is provided + setDotString(generatedDotStr); + }, [csvData, selfLoops, minVisits, selectedSequence]); + + useEffect(() => { if (filter) { + const sortedData = loadAndSortData(csvData); const filteredData = sortedData.filter(row => row['CF (Workspace Progress Status)'] === filter); const filteredStepSequences = createStepSequences(filteredData, selfLoops); const filteredOutcomeSequences = createOutcomeSequences(filteredData); @@ -97,66 +564,85 @@ const GraphvizParent: React.FC = ({ ratioEdges: filteredRatioEdges, edgeOutcomeCounts: filteredEdgeOutcomeCounts, maxEdgeCount: filteredMaxEdgeCount, - top5Sequences, + top5Sequences } = countEdges(filteredStepSequences, filteredOutcomeSequences); - + const formattedTop5Sequences = top5Sequences.map(([sequenceKey, count]) => ({ + sequence: JSON.parse(sequenceKey), + count, + })); const filteredNormalizedThicknesses = normalizeThicknesses(filteredEdgeCounts, filteredMaxEdgeCount, 10); - // const filteredMostCommonSequenceKey = Object.keys(filteredStepSequences) - // .reduce((a, b) => filteredStepSequences[a].length > filteredStepSequences[b].length ? a : b); - // const filteredMostCommonSequence = filteredStepSequences[filteredMostCommonSequenceKey]; - // Generate the DOT string for the filtered graph - const filteredDotStr = generateDotString( + let filteredDotStr = generateDotString( filteredNormalizedThicknesses, - // filteredMostCommonSequence, filteredRatioEdges, filteredEdgeOutcomeCounts, filteredEdgeCounts, filteredTotalNodeEdges, 1, minVisits, - selectedSequence + formattedTop5Sequences[selectedSequenceIndex]?.sequence || [] ); setFilteredDotString(filteredDotStr); } else { - setFilteredDotString(null); // Clear filtered graph if no filter is set + setFilteredDotString(null); } - }, [csvData, filter, selfLoops, minVisits, selectedSequence]); - - const handleSequenceSelect = (sequence: string) => { - setSelectedSequence(sequence); - }; + // const handleSequenceChange = (event: React.ChangeEvent) => { + // const selectedIndex = parseInt(event.target.value); + // setSelectedSequence(top5Sequences[selectedSequence] || []); + // }; return (
- {/**/} -
- -
- {dotString && ( - - )} - {filteredDotString && ( - - )} -
-
-
-
- ); -} + +
+ {dotString && ( + + )} + {filteredDotString && ( + + )} +
+
+
+)} + ; +// return ( +//
+// +//
+// {dotString && ( +// +// )} +// {filteredDotString && ( +// +// )} +//
+//
+//
+// ); +// } + -export default GraphvizParent; + export default GraphvizParent; \ No newline at end of file diff --git a/src/components/GraphvizProcessing.ts b/src/components/GraphvizProcessing.ts index d1d5200..a7e9343 100644 --- a/src/components/GraphvizProcessing.ts +++ b/src/components/GraphvizProcessing.ts @@ -1,5 +1,5 @@ import Papa from 'papaparse'; -import {get} from "lodash"; +import {SequenceCount} from "@/components/GraphvizParent.tsx"; interface CSVRow { 'Session Id': string; @@ -91,9 +91,10 @@ export function getTopSequences(stepSequences: any, topN: number = 5) { const sequenceCounts: { [sequence: string]: number } = {}; // Iterate over the values (which are lists) of the stepSequences dictionary - Object.values(stepSequences).forEach((sequenceList: string[]) => { - const sequenceKey = JSON.stringify(sequenceList); // Convert the list to a string key + Object.values(stepSequences).forEach((sequence: string[]) => { + const sequenceKey = JSON.stringify(sequence); // Convert the list to a string key + // Count occurrences of each unique sequence if (sequenceCounts[sequenceKey]) { sequenceCounts[sequenceKey]++; } else { @@ -106,12 +107,17 @@ export function getTopSequences(stepSequences: any, topN: number = 5) { .sort(([, countA], [, countB]) => countB - countA) .slice(0, topN); - // Instead of returning the actual sequences, return "Count 1" through "Count 5" - // const topSequences = sortedSequences.map((sequence, index) => `Count ${index + 1}`); - const topSequences = sortedSequences.map(([sequenceKey, index]) => JSON.parse(sequenceKey, 'Count ${index + 1}')); - return topSequences; + // Convert to the desired format: { sequence: [step1, step2, step3], count } + const topSequences = sortedSequences.map(([sequenceKey, count]) => ({ + sequence: JSON.parse(sequenceKey), // Convert the string back to an array + count, + })); + + console.log(topSequences); // Log the top sequences for debugging + return topSequences; // Return the array of top sequences } + interface EdgeCounts { edgeCounts: { [key: string]: number }; totalNodeEdges: { [key: string]: number }; @@ -129,7 +135,7 @@ export const countEdges = ( maxEdgeCount: number; ratioEdges: { [p: string]: number }; edgeCounts: { [p: string]: number }; - top5Sequences: string[]; + top5Sequences: SequenceCount; } => { const edgeCounts: { [key: string]: number } = {}; const totalNodeEdges: { [key: string]: number } = {}; @@ -137,7 +143,7 @@ export const countEdges = ( const edgeOutcomeCounts: { [key: string]: { [outcome: string]: number } } = {}; let maxEdgeCount = 0; - const top5Sequences = getTopSequences(stepSequences, 5) + const top5Sequences = getTopSequences(stepSequences, 10) // Process edges for all sequences Object.keys(stepSequences).forEach((sessionId) => { @@ -174,22 +180,22 @@ export const countEdges = ( }; -// // Function to normalize edge thicknesses based on their ratio -// export function normalizeThicknesses( -// ratioEdges: { [key: string]: number }, -// maxThickness: number -// ): { [key: string]: number } { -// const normalized: { [key: string]: number } = {}; -// const maxRatio = Math.max(...Object.values(ratioEdges), 1); // Avoid division by zero -// -// // Scale edge thicknesses to a maximum value -// Object.keys(ratioEdges).forEach((edge) => { -// const ratio = ratioEdges[edge]; -// normalized[edge] = (ratio / maxRatio) * maxThickness; -// }); -// -// return normalized; -// } +// Function to normalize edge thicknesses based on their ratio +export function normalizeThicknessesRatios( + ratioEdges: { [key: string]: number }, + maxThickness: number +): { [key: string]: number } { + const normalized: { [key: string]: number } = {}; + const maxRatio = Math.max(...Object.values(ratioEdges), 1); // Avoid division by zero + + // Scale edge thicknesses to a maximum value + Object.keys(ratioEdges).forEach((edge) => { + const ratio = ratioEdges[edge]; + normalized[edge] = (ratio / maxRatio) * maxThickness; + }); + + return normalized; +} // // Function to normalize edge thicknesses based the full graph export function normalizeThicknesses( @@ -209,7 +215,7 @@ export function normalizeThicknesses( // Function to calculate the color of a node based on its rank in the most common sequence -function calculateColor(rank: number, totalSteps: number): string { +export function calculateColor(rank: number, totalSteps: number): string { const ratio = rank / totalSteps; const white = {r: 255, g: 255, b: 255}; @@ -266,13 +272,13 @@ export function generateDotString( totalNodeEdges: EdgeCounts['totalNodeEdges'], threshold: number, min_visits: number, - selectedSequence: string + selectedSequence: string[] ): string { const stepsInSelectedSequence = selectedSequence//.split('->'); // console.log(mostCommonSequence) - console.log("selectedSequence" + selectedSequence) + console.log("selectedSequence" + stepsInSelectedSequence) // console.log(selectedSequence[stepsInSelectedSequence]) - const totalSteps = selectedSequence.length; + const totalSteps = stepsInSelectedSequence.length; console.log("totalSteps" + totalSteps) // Create node definitions in the DOT string let dotString = 'digraph G {\n'; @@ -314,3 +320,329 @@ export function generateDotString( return dotString; } +// import Papa from 'papaparse'; +// +// interface CSVRow { +// 'Session Id': string; +// 'Time': string; +// 'Step Name': string; +// 'Outcome': string; +// 'CF (Workspace Progress Status)': string; +// } +// +// // Function to load and sort data +// export const loadAndSortData = (csvData: string): CSVRow[] => { +// // Step 1: Parse the CSV data using PapaParse +// const parsedData = Papa.parse(csvData, { +// header: true, +// skipEmptyLines: true +// }).data; +// +// // Step 2: Transform data to replace missing Step Names with a default value +// const transformedData = parsedData.map(row => { +// return { +// 'Session Id': row['Session Id'], +// 'Time': row['Time'], +// 'Step Name': row['Step Name'] || 'DoneButton', // Default value for missing Step Names +// 'Outcome': row['Outcome'], +// 'CF (Workspace Progress Status)': row['CF (Workspace Progress Status)'], +// }; +// }); +// +// // Step 3: Sort the transformed data by Session Id and Time +// return transformedData.sort((a, b) => { +// if (a['Session Id'] === b['Session Id']) { +// return new Date(a['Time']).getTime() - new Date(b['Time']).getTime(); +// } +// return a['Session Id'].localeCompare(b['Session Id']); +// }); +// }; +// +// // Function to create step sequences from sorted data +// export const createStepSequences = (sortedData: CSVRow[], selfLoops: boolean): { [key: string]: string[] } => { +// // Iterate over sorted data to build step sequences +// return sortedData.reduce((acc, row) => { +// const sessionId = row['Session Id']; +// if (!acc[sessionId]) { +// acc[sessionId] = []; +// } +// const stepName = row['Step Name']; +// +// // Add step to sequence based on whether self-loops are allowed +// if (selfLoops || acc[sessionId].length === 0 || acc[sessionId][acc[sessionId].length - 1] !== stepName) { +// acc[sessionId].push(stepName); +// } +// +// return acc; +// }, {} as { [key: string]: string[] }); +// }; +// +// // Function to create outcome sequences from sorted data +// export const createOutcomeSequences = (sortedData: CSVRow[]): { [key: string]: string[] } => { +// // Iterate over sorted data to build outcome sequences +// return sortedData.reduce((acc, row) => { +// const sessionId = row['Session Id']; +// if (!acc[sessionId]) { +// acc[sessionId] = []; +// } +// acc[sessionId].push(row['Outcome']); +// return acc; +// }, {} as { [key: string]: string[] }); +// }; +// +// // export function getTopSequences(stepSequences: any, topN: number = 5) { +// // // const topSequences = Object.values(stepSequences) +// // // .sort((a, b) => stepSequences[b].length - stepSequences[a].length) +// // // .slice(0, topN); +// // +// // const sequenceCounts = Object.entries(stepSequences).reduce((acc: any, [key, value]) => { +// // acc[key] = value.count; +// // return acc; +// // }, {}); +// // console.log("counts: " + sequenceCounts[stepSequences[0]]) +// // const sortedSequences = Object.entries(sequenceCounts) +// // .sort(([, a], [, b]) => b - a) +// // .slice(0, topN); +// // console.log("sorted sequences: " + sequenceCounts[sortedSequences[0]]) +// // // return sortedSequences.map(([sequence]) => sequence); +// // return topSequences +// // } +// export function getTopSequences(stepSequences: any, topN: number = 5) { +// // Create a frequency map to count how many times each unique sequence (list) occurs +// const sequenceCounts: { [sequence: string]: number } = {}; +// +// // Iterate over the values (which are lists) of the stepSequences dictionary +// Object.values(stepSequences).forEach((sequenceList: string[]) => { +// const sequenceKey = JSON.stringify(sequenceList); // Convert the list to a string key +// +// if (sequenceCounts[sequenceKey]) { +// sequenceCounts[sequenceKey]++; +// } else { +// sequenceCounts[sequenceKey] = 1; +// } +// }); +// +// // Sort the sequences based on their counts in descending order and take the top N +// const sortedSequences = Object.entries(sequenceCounts) +// .sort(([, countA], [, countB]) => countB - countA) +// .slice(0, topN); +// console.log("Sorted w counts: " + sortedSequences) +// console.log(sortedSequences.map(([sequenceKey, count]) => JSON.parse(sequenceKey, sequenceCounts[sequenceKey]))) +// const topSequences = sortedSequences.map(([sequenceKey, count]) => JSON.parse(sequenceKey, sequenceCounts[sequenceKey])); +// return topSequences; +// } +// +// interface EdgeCounts { +// edgeCounts: { [key: string]: number }; +// totalNodeEdges: { [key: string]: number }; +// ratioEdges: { [key: string]: number }; +// edgeOutcomeCounts: { [key: string]: { [outcome: string]: number } }; +// } +// +// // Function to count edges between steps +// export const countEdges = ( +// stepSequences: { [key: string]: string[] }, +// outcomeSequences: { [key: string]: string[] } +// ): { +// totalNodeEdges: { [p: string]: number }; +// edgeOutcomeCounts: { [p: string]: { [p: string]: number } }; +// maxEdgeCount: number; +// ratioEdges: { [p: string]: number }; +// edgeCounts: { [p: string]: number }; +// top5Sequences: string[]; +// } => { +// const edgeCounts: { [key: string]: number } = {}; +// const totalNodeEdges: { [key: string]: number } = {}; +// const ratioEdges: { [key: string]: number } = {}; +// const edgeOutcomeCounts: { [key: string]: { [outcome: string]: number } } = {}; +// let maxEdgeCount = 0; +// +// const top5Sequences = getTopSequences(stepSequences, 5) +// +// // Process edges for all sequences +// Object.keys(stepSequences).forEach((sessionId) => { +// const steps = stepSequences[sessionId]; +// // console.log(steps) +// const outcomes = outcomeSequences[sessionId]; +// +// if (steps.length < 2) return; +// +// for (let i = 0; i < steps.length - 1; i++) { +// const currentStep = steps[i]; +// const nextStep = steps[i + 1]; +// const outcome = outcomes[i + 1]; +// +// const edgeKey = `${currentStep}->${nextStep}`; +// edgeCounts[edgeKey] = (edgeCounts[edgeKey] || 0) + 1; +// edgeOutcomeCounts[edgeKey] = edgeOutcomeCounts[edgeKey] || {}; +// edgeOutcomeCounts[edgeKey][outcome] = (edgeOutcomeCounts[edgeKey][outcome] || 0) + 1; +// totalNodeEdges[currentStep] = (totalNodeEdges[currentStep] || 0) + 1; +// +// // Track the maximum edge count +// if (edgeCounts[edgeKey] > maxEdgeCount) { +// maxEdgeCount = edgeCounts[edgeKey]; +// } +// } +// }); +// +// Object.keys(edgeCounts).forEach((edge) => { +// const [start] = edge.split('->'); +// ratioEdges[edge] = edgeCounts[edge] / (totalNodeEdges[start] || 0); +// }); +// +// return {edgeCounts, totalNodeEdges, ratioEdges, edgeOutcomeCounts, maxEdgeCount, top5Sequences}; +// }; +// +// +// // // Function to normalize edge thicknesses based on their ratio +// // export function normalizeThicknesses( +// // ratioEdges: { [key: string]: number }, +// // maxThickness: number +// // ): { [key: string]: number } { +// // const normalized: { [key: string]: number } = {}; +// // const maxRatio = Math.max(...Object.values(ratioEdges), 1); // Avoid division by zero +// // +// // // Scale edge thicknesses to a maximum value +// // Object.keys(ratioEdges).forEach((edge) => { +// // const ratio = ratioEdges[edge]; +// // normalized[edge] = (ratio / maxRatio) * maxThickness; +// // }); +// // +// // return normalized; +// // } +// +// // // Function to normalize edge thicknesses based the full graph +// export function normalizeThicknesses( +// edgeCounts: { [key: string]: number }, +// maxEdgeCount: number, +// maxThickness: number +// ): { [key: string]: number } { +// const normalized: { [key: string]: number } = {}; +// +// Object.keys(edgeCounts).forEach((edge) => { +// const count = edgeCounts[edge]; +// normalized[edge] = (count / maxEdgeCount) * maxThickness; +// }); +// +// return normalized; +// } +// +// +// // Function to calculate the color of a node based on its rank in the most common sequence +// function calculateColor(sequence: string[], rank: number, totalSteps: number): string { +// const ratio = rank / totalSteps; +// +// const white = {r: 255, g: 255, b: 255}; +// const lightBlue = {r: 0, g: 166, b: 255}; +// +// const r = Math.round(white.r * (1 - ratio) + lightBlue.r * ratio); +// const g = Math.round(white.g * (1 - ratio) + lightBlue.g * ratio); +// const b = Math.round(white.b * (1 - ratio) + lightBlue.b * ratio); +// +// const toHex = (value: number) => value.toString(16).padStart(2, '0'); +// const color = `#${toHex(r)}${toHex(g)}${toHex(b)}`; +// +// return color; +// } +// +// // Function to calculate the color of an edge based on its outcome distribution +// function calculateEdgeColors(outcomes: { [outcome: string]: number }): string { +// const colorMap: { [key: string]: string } = { +// 'ERROR': '#ff0000', // Red +// 'OK': '#00ff00', // Green +// 'INITIAL_HINT': '#0000ff', // Blue +// 'HINT_LEVEL_CHANGE': '#0000ff', // Blue +// 'JIT': '#ffff00', // Yellow +// 'FREEBIE_JIT': '#ffff00' // Yellow +// }; +// +// if (Object.keys(outcomes).length === 0) { +// return '#00000000'; // Transparent black +// } +// +// const totalCount = Object.values(outcomes).reduce((sum, count) => sum + count, 0); +// let weightedR = 0, weightedG = 0, weightedB = 0; +// +// Object.entries(outcomes).forEach(([outcome, count]) => { +// const color = colorMap[outcome] || '#000000'; // Default to black if outcome is not found +// const [r, g, b] = [1, 3, 5].map(i => parseInt(color.slice(i, i + 2), 16)); // Extract RGB values +// const weight = count / totalCount; +// weightedR += r * weight; +// weightedG += g * weight; +// weightedB += b * weight; +// }); +// +// // Convert RGB values to hex and add alpha transparency +// return `#${Math.round(weightedR).toString(16).padStart(2, '0')}${Math.round(weightedG).toString(16).padStart(2, '0')}${Math.round(weightedB).toString(16).padStart(2, '0')}90`; +// } +// +// // Function to generate a Graphviz DOT string for visualization +// export function generateDotString( +// normalizedThicknesses: { [key: string]: number }, +// // mostCommonSequence: string[], +// ratioEdges: { [key: string]: number }, +// edgeOutcomeCounts: EdgeCounts['edgeOutcomeCounts'], +// edgeCounts: EdgeCounts['edgeCounts'], +// totalNodeEdges: EdgeCounts['totalNodeEdges'], +// threshold: number, +// min_visits: number, +// selectedSequence: string +// ): string { +// // const stepsInSelectedSequence = selectedSequence//.split('->'); +// // console.log(mostCommonSequence) +// // console.log(selectedSequence[stepsInSelectedSequence]) +// const totalSteps = selectedSequence.length; +// +// console.log("selected sequence" + selectedSequence) +// console.log("totalSteps" + totalSteps) +// // Create node definitions in the DOT string +// let dotString = 'digraph G {\n'; +// Object.keys(edgeCounts).forEach((sourceNode) => { +// // Determine the rank of the node in the selected sequence +// const rank = selectedSequence.indexOf(sourceNode) + 1; +// const color = rank > 0 ? calculateColor(selectedSequence, rank, totalSteps) : '#FFFFFF'; // Default to white if not in sequence +// +// // dotString += `"${sourceNode}" [style=filled, fillcolor="${color}"];\n`; +// +// // for (let rank = 0; rank < totalSteps; rank++) { +// // const step = stepsInSelectedSequence[rank]; +// // const color = calculateColor(selectedSequence, rank, totalSteps); +// // console.log(color, step) +// const node_tooltip = `Rank:\n\t\t ${rank + 1}\nColor:\n\t\t ${color}`; +// +// dotString += ` "${sourceNode}" [rank=${rank + 1}, style=filled, fillcolor="${color}", tooltip="${node_tooltip}"];\n`; +// } +// +// // Create edge definitions in the DOT string based on normalized thickness and thresholds +// for (const edge of Object.keys(normalizedThicknesses)) +// { +// if (normalizedThicknesses[edge] >= threshold) { +// const [currentStep, nextStep] = edge.split('->'); +// const thickness = normalizedThicknesses[edge]; +// const outcomes = edgeOutcomeCounts[edge] || {}; +// const edgeCount = edgeCounts[edge] || 0; +// const totalCount = totalNodeEdges[currentStep] || 0; +// const color = calculateEdgeColors(outcomes); +// const outcomesStr = Object.entries(outcomes) +// .map(([outcome, count]) => `${outcome}: ${count}`) +// .join('\n\t\t '); +// +// if (edgeCount > min_visits) { +// const tooltip = `${currentStep} to ${nextStep}\n` +// + `- Edge Count: \n\t\t ${edgeCount}\n` +// + `- Total Count for ${currentStep}: \n\t\t${totalCount}\n` +// + `- Ratio: \n\t\t${((ratioEdges[edge] || 0) * 100).toFixed(2)}% of students at ${currentStep} go to ${nextStep}\n` +// + `- Outcomes: \n\t\t ${outcomesStr}\n` +// + `- Color Codes: \n\t\t Hex: ${color}\n\t\t RGB: ${[parseInt(color.substring(1, 3), 16), parseInt(color.substring(3, 5), 16), parseInt(color.substring(5, 7), 16)]}`; +// +// dotString += ` "${currentStep}" -> "${nextStep}" [penwidth=${thickness}, color="${color}", tooltip="${tooltip}"];\n`; +// } +// } +// } +// +// dotString += '}'; +// return dotString; +// } +// + + diff --git a/src/components/SequenceSelector.tsx b/src/components/SequenceSelector.tsx index df0e9fd..a4a471c 100644 --- a/src/components/SequenceSelector.tsx +++ b/src/components/SequenceSelector.tsx @@ -18,8 +18,8 @@ const SequenceSelector: React.FC = ({ return (
setSelectedSequenceIndex(Number(e.target.value))} - value={selectedSequenceIndex}> - {top5Sequences.map((seq, index) => ( - - ))} - -
- {dotString && ( - - )} - {filteredDotString && ( - - )} -
- -
-)} - ; +
+ {dotString && ( + + )} + {filteredDotString && ( + + )} +
+ +
+ ); +} // return ( //
// @@ -645,4 +635,4 @@ const GraphvizParent: React.FC = ({ // } - export default GraphvizParent; \ No newline at end of file +export default GraphvizParent; \ No newline at end of file diff --git a/src/components/SequenceSelector.tsx b/src/components/SequenceSelector.tsx index a4a471c..68ed8ba 100644 --- a/src/components/SequenceSelector.tsx +++ b/src/components/SequenceSelector.tsx @@ -1,7 +1,8 @@ import React from 'react'; +import {SequenceCount} from "@/components/GraphvizParent.tsx"; interface SequenceSelectorProps { - sequences: string[]; + sequences: SequenceCount|string; selectedSequence: string; onSequenceSelect: (sequence: string) => void; } @@ -11,7 +12,7 @@ const SequenceSelector: React.FC = ({ selectedSequence, onSequenceSelect, }) => { - if (sequences.length === 0) { + if (sequences == '') { return
No sequences available
; // Display a message when no sequences are present } @@ -20,7 +21,7 @@ const SequenceSelector: React.FC = ({ From 1aab228641f9172d16ee4bc794befae25abb9abe Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:17:36 -0500 Subject: [PATCH 23/54] Color working but graph not reloading upon new upload --- src/App.tsx | 30 +- src/Context.tsx | 17 +- src/components/GraphvizParent.tsx | 541 +-------------------------- src/components/GraphvizProcessing.ts | 31 +- src/components/SequenceSelector.tsx | 33 +- 5 files changed, 77 insertions(+), 575 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 0073db7..d0b040d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,21 +1,22 @@ import './App.css'; -import React, { useCallback, useState } from 'react'; +import React, {useContext, useState} from 'react'; import Upload from "@/components/Upload.tsx"; -import GraphvizParent, {SequenceCount} from "@/components/GraphvizParent.tsx"; +import GraphvizParent from "@/components/GraphvizParent.tsx"; import FilterComponent from './components/FilterComponent.tsx'; import SelfLoopSwitch from './components/selfLoopSwitch.tsx'; import Slider from './components/slider.tsx'; import SequenceSelector from "@/components/SequenceSelector.tsx"; +import {Context, SequenceCount} from "@/Context.tsx"; const App: React.FC = () => { const [csvData, setCsvData] = useState(''); const [filter, setFilter] = useState(''); const [selfLoops, setSelfLoops] = useState(true); const [minVisits, setMinVisits] = useState(10); - const [topSequences, setTopSequences] = useState([]); - const [selectedSequence, setSelectedSequence] = useState(['']); + const {top5Sequences} = useContext(Context); + const [selectedSequence, setSelectedSequence] = useState(null); - const handleSelectSequence = (selectedSequence: string[]) => { + const handleSelectSequence = (selectedSequence: SequenceCount["sequence"]) => { setSelectedSequence(selectedSequence); // Fix: Use the correct parameter to update the state console.log(`Selected sequence: ${selectedSequence}`); }; @@ -24,19 +25,20 @@ const App: React.FC = () => { const handleSlider = (value: number) => setMinVisits(value); const handleDataProcessed = (uploadedCsvData: string) => setCsvData(uploadedCsvData); - // Fix: Remove `topSequences` dependency from `useCallback` to avoid unnecessary re-creations - // const handleTopSequencesUpdate = useCallback((sequences: string[][]) => { - // setTopSequences(sequences); - // }, [selectedSequence]); return (

Path Analysis Tool

- - - - - + + +

{selectedSequence}

+ + + + void; setData: (data: GlobalDataType[] | null) => void; setGraphData: (graphData: GraphData | null) => void; resetData: () => void; + setTop5Sequences: (top5Sequences: SequenceCount[]) => void; } +export interface SequenceCount { + sequence: string[] | null; // or whatever type your steps are (e.g., number[]) + count: number; +} export const Context = createContext({} as ContextInterface); const initialState = { data: null, graphData: null, - loading: false + loading: false, + top5Sequences: null } interface ProviderProps { children: React.ReactNode; } + + export const Provider = ({ children }: ProviderProps) => { const [data, setData] = useState(initialState.data) const [graphData, setGraphData] = useState(initialState.graphData) const [loading, setLoading] = useState(initialState.loading) - + const [top5Sequences, setTop5Sequences] = useState(initialState.top5Sequences) const resetData = () => { setData(null) setGraphData(null) @@ -38,10 +47,12 @@ export const Provider = ({ children }: ProviderProps) => { data, graphData, loading, + top5Sequences, setLoading, setData, setGraphData, - resetData + resetData, + setTop5Sequences }} > {children} diff --git a/src/components/GraphvizParent.tsx b/src/components/GraphvizParent.tsx index d86f153..93e1c4b 100644 --- a/src/components/GraphvizParent.tsx +++ b/src/components/GraphvizParent.tsx @@ -1,483 +1,4 @@ -// import {useEffect, useState} from "react"; -// import ErrorBoundary from "@/components/errorBoundary.tsx"; -// import Graphviz from 'graphviz-react'; -// import { -// loadAndSortData, -// createStepSequences, -// createOutcomeSequences, -// countEdges, -// normalizeThicknesses, -// generateDotString, calculateColor -// } from './GraphvizProcessing'; -// import '../GraphvizContainer.css'; -// -// // import SequenceSelector from './SequenceSelector'; -// -// interface GraphvizParentProps { -// csvData: string; -// filter: string; -// selfLoops: boolean; -// minVisits: number; -// onTopSequencesUpdate: (sequences: string[]) => void; -// // selectedSequence: string; -// } -// -// const GraphvizParent: React.FC = ({ -// csvData, -// filter, -// selfLoops, -// minVisits, -// onTopSequencesUpdate, -// // selectedSequence, -// }) => { -// const [dotString, setDotString] = useState(''); -// const [filteredDotString, setFilteredDotString] = useState(''); -// const [topSequences, setTopSequences] = useState([]); -// const [selectedSequence, setSelectedSequence] = useState([]); -// -// // Function to update the node colors based on the selected sequence -// -// const applySequenceColors = (dotStr: string, sequence: string[]) => { -// return dotStr.replace(/(\w+ \[)(.*?\])/g, (match, p1, p2) => { -// const nodeName = match.match(/(\w+) \[/)?.[1]; -// -// // Find the rank of the node in the selected sequence -// const rank = sequence.indexOf(nodeName!) + 1; // +1 because index is 0-based -// const totalSteps = sequence.length; -// -// // Only calculate color if the node exists in the sequence -// if (rank > 0) { -// const color = calculateColor(rank, totalSteps); -// return `${p1}style=filled, fillcolor="${color}", ${p2}`; -// } else { -// return match; // return the original string if node is not in sequence -// } -// }); -// }; -// -// -// useEffect(() => { -// if (!csvData) return; // Skip processing if no CSV data is available -// -// // Step 1: Load and sort the data -// const sortedData = loadAndSortData(csvData); -// -// // Step 2: Generate the sequences for steps and outcomes -// const stepSequences = createStepSequences(sortedData, selfLoops); -// const outcomeSequences = createOutcomeSequences(sortedData); -// -// // Step 3: Count edges and normalize thicknesses -// const { -// edgeCounts, -// totalNodeEdges, -// ratioEdges, -// edgeOutcomeCounts, -// maxEdgeCount, -// top5Sequences -// } = countEdges(stepSequences, outcomeSequences); -// setTopSequences(top5Sequences) -// console.log('TEST: ' + top5Sequences) -// setSelectedSequence(top5Sequences[0]) -// const normalizedThicknesses = normalizeThicknesses(edgeCounts, maxEdgeCount, 10); -// -// // Step 4: Find the most common sequences -// // const mostCommonSequenceKey = Object.keys(stepSequences) -// // .reduce((a, b) => stepSequences[a].length > stepSequences[b].length ? a : b); -// // const mostCommonSequence = stepSequences[mostCommonSequenceKey]; -// // const topSequences = Object.keys(stepSequences) -// // .sort((a, b) => stepSequences[b].length - stepSequences[a].length) -// // .slice(0, 5); -// // console.log(stepSequences) -// // const top5Sequences = getTopSequences(stepSequences,5) -// onTopSequencesUpdate(top5Sequences); -// -// setTopSequences(top5Sequences) -// console.log("GVP67:" + topSequences) -// // Call the update function to pass top sequences to App component -// // Step 5: Generate the DOT string for the unfiltered graph -// let dotStr = generateDotString( -// normalizedThicknesses, -// // mostCommonSequence, -// ratioEdges, -// edgeOutcomeCounts, -// edgeCounts, -// totalNodeEdges, -// 1, -// minVisits, -// selectedSequence -// ); -// // Step 5: Apply initial color based on the selected sequence -// -// if (selectedSequence) { -// -// dotStr = applySequenceColors(dotStr, selectedSequence); -// -// } -// -// setDotString(dotStr); -// -// // Step 6: Generate the filtered graph if a filter is provided -// if (filter) { -// const filteredData = sortedData.filter(row => row['CF (Workspace Progress Status)'] === filter); -// const filteredStepSequences = createStepSequences(filteredData, selfLoops); -// const filteredOutcomeSequences = createOutcomeSequences(filteredData); -// -// const { -// edgeCounts: filteredEdgeCounts, -// totalNodeEdges: filteredTotalNodeEdges, -// ratioEdges: filteredRatioEdges, -// edgeOutcomeCounts: filteredEdgeOutcomeCounts, -// maxEdgeCount: filteredMaxEdgeCount, -// top5Sequences, -// } = countEdges(filteredStepSequences, filteredOutcomeSequences); -// -// const filteredNormalizedThicknesses = normalizeThicknesses(filteredEdgeCounts, filteredMaxEdgeCount, 10); -// // const filteredMostCommonSequenceKey = Object.keys(filteredStepSequences) -// // .reduce((a, b) => filteredStepSequences[a].length > filteredStepSequences[b].length ? a : b); -// // const filteredMostCommonSequence = filteredStepSequences[filteredMostCommonSequenceKey]; -// -// // Generate the DOT string for the filtered graph -// let filteredDotStr = generateDotString( -// filteredNormalizedThicknesses, -// // filteredMostCommonSequence, -// filteredRatioEdges, -// filteredEdgeOutcomeCounts, -// filteredEdgeCounts, -// filteredTotalNodeEdges, -// 1, -// minVisits, -// selectedSequence -// ); -// if (selectedSequence != top5Sequences[0]) { -// filteredDotStr = applySequenceColors(dotStr, selectedSequence); -// } -// setFilteredDotString(filteredDotStr); -// } else { -// setFilteredDotString(null); // Clear filtered graph if no filter is set -// } -// -// }, [csvData, filter, selfLoops, minVisits, selectedSequence]); -// -// const handleSequenceSelect = (sequence: string) => { -// setSelectedSequence(sequence); -// }; -// -// return ( -//
-// {/**/} -// -//
-// -//
-// {dotString && ( -// -// )} -// {filteredDotString && ( -// -// )} -//
-//
-//
-//
-// ); -// } -// -// export default GraphvizParent; - - -// import {useEffect, useState} from "react"; -// import ErrorBoundary from "@/components/errorBoundary.tsx"; -// import Graphviz from 'graphviz-react'; -// import { -// loadAndSortData, -// createStepSequences, -// createOutcomeSequences, -// countEdges, -// normalizeThicknesses, -// generateDotString, calculateColor -// } from './GraphvizProcessing'; -// import '../GraphvizContainer.css'; -// -// interface GraphvizParentProps { -// csvData: string; -// filter: string; -// selfLoops: boolean; -// minVisits: number; -// onTopSequencesUpdate: (sequences: string[]) => void; -// } -// -// const GraphvizParent: React.FC = ({ -// csvData, -// filter, -// selfLoops, -// minVisits, -// onTopSequencesUpdate, -// }) => { -// const [dotString, setDotString] = useState(''); -// const [filteredDotString, setFilteredDotString] = useState(null); -// const [topSequences, setTopSequences] = useState([]); -// const [selectedSequence, setSelectedSequence] = useState([]); -// -// const applySequenceColors = (dotStr: string, sequence: string[]) => { -// return dotStr.replace(/(\w+ \[)(.*?\])/g, (match, p1, p2) => { -// const nodeName = match.match(/(\w+) \[/)?.[1]; -// const rank = sequence.indexOf(nodeName!) + 1; -// const totalSteps = sequence.length; -// -// if (rank > 0) { -// const color = calculateColor(rank, totalSteps); -// return `${p1}style=filled, fillcolor="${color}", ${p2}`; -// } else { -// return match; -// } -// }); -// }; -// -// useEffect(() => { -// if (!csvData) return; -// -// const sortedData = loadAndSortData(csvData); -// const stepSequences = createStepSequences(sortedData, selfLoops); -// const outcomeSequences = createOutcomeSequences(sortedData); -// -// const { -// edgeCounts, -// totalNodeEdges, -// ratioEdges, -// edgeOutcomeCounts, -// maxEdgeCount, -// top5Sequences, -// } = countEdges(stepSequences, outcomeSequences); -// -// setTopSequences(top5Sequences); -// setSelectedSequence(top5Sequences[0][0]); -// -// const normalizedThicknesses = normalizeThicknesses(edgeCounts, maxEdgeCount, 10); -// -// let dotStr = generateDotString( -// normalizedThicknesses, -// ratioEdges, -// edgeOutcomeCounts, -// edgeCounts, -// totalNodeEdges, -// 1, -// minVisits, -// top5Sequences[0] // Use the first sequence directly here -// ); -// -// dotStr = applySequenceColors(dotStr, top5Sequences[0]); -// setDotString(dotStr); -// -// if (filter) { -// const filteredData = sortedData.filter(row => row['CF (Workspace Progress Status)'] === filter); -// const filteredStepSequences = createStepSequences(filteredData, selfLoops); -// const filteredOutcomeSequences = createOutcomeSequences(filteredData); -// -// const { -// edgeCounts: filteredEdgeCounts, -// totalNodeEdges: filteredTotalNodeEdges, -// ratioEdges: filteredRatioEdges, -// edgeOutcomeCounts: filteredEdgeOutcomeCounts, -// maxEdgeCount: filteredMaxEdgeCount, -// top5Sequences: filteredTop5Sequences, -// } = countEdges(filteredStepSequences, filteredOutcomeSequences); -// -// const filteredNormalizedThicknesses = normalizeThicknesses(filteredEdgeCounts, filteredMaxEdgeCount, 10); -// -// let filteredDotStr = generateDotString( -// filteredNormalizedThicknesses, -// filteredRatioEdges, -// filteredEdgeOutcomeCounts, -// filteredEdgeCounts, -// filteredTotalNodeEdges, -// 1, -// minVisits, -// selectedSequence // Apply selected sequence here -// ); -// -// filteredDotStr = applySequenceColors(filteredDotStr, selectedSequence); -// setFilteredDotString(filteredDotStr); -// } else { -// setFilteredDotString(null); -// } -// -// }, [csvData, filter, selfLoops, minVisits]); -// -// return ( -//
-// -//
-// {dotString && ( -// -// )} -// {filteredDotString && ( -// -// )} -//
-//
-//
-// ); -// } -// -// export default GraphvizParent; - -// import React, {useEffect, useState} from 'react'; -// import { -// loadAndSortData, -// createStepSequences, -// createOutcomeSequences, -// countEdges, -// normalizeThicknesses, -// generateDotString, -// calculateColor -// } from './GraphvizProcessing'; -// import ErrorBoundary from "@/components/errorBoundary.tsx"; -// import Graphviz from "graphviz-react"; -// -// -// const GraphvizParent: React.FC = () => { -// const [selectedSequence, setSelectedSequence] = useState([]); -// const [dotString, setDotString] = useState(''); -// const [filteredDotString, setFilteredDotString] = useState(null); // Use null for no filter -// const [edgeCounts, setEdgeCounts] = useState({}); -// const [maxEdgeCount, setMaxEdgeCount] = useState(0); -// const [ratioEdges, setRatioEdges] = useState({}); -// const [edgeOutcomeCounts, setEdgeOutcomeCounts] = useState({}); -// const [totalNodeEdges, setTotalNodeEdges] = useState< {[p: string]: number; }>({}); -// const [filterCriteria, setFilterCriteria] = useState(null); // Track filter criteria -// const [csvData, setCsvData] = useState(''); // State for CSV data -// const [selfLoops, setSelfLoops] = useState(false); // State for self-loops -// const [minVisits, setMinVisits] = useState(0); // State for minimum visits -// -// const applySequenceColors = (dotStr: string, sequence: string[]) => { -// return dotStr.replace(/(\w+ \[)(.*?\])/g, (match, p1, p2) => { -// const nodeName = match.match(/(\w+) \[/)?.[1]; -// const rank = sequence.indexOf(nodeName!) + 1; -// const totalSteps = sequence.length; -// -// if (rank > 0) { -// const color = calculateColor(rank, totalSteps); -// return `${p1}style=filled, fillcolor="${color}", ${p2}`; -// } else { -// return match; -// } -// }); -// }; -// useEffect(() => { -// const fetchData = async () => { -// if (!csvData) return; -// else setCsvData(csvData); -// const sortedData = loadAndSortData(csvData); -// const stepSequences = createStepSequences(sortedData, selfLoops); -// const outcomeSequences = createOutcomeSequences(sortedData); -// -// const { edgeCounts, totalNodeEdges, ratioEdges, edgeOutcomeCounts, maxEdgeCount, top5Sequences } = countEdges(stepSequences, outcomeSequences); -// -// setEdgeCounts(edgeCounts); -// setTotalNodeEdges(totalNodeEdges); -// setRatioEdges(ratioEdges); -// setEdgeOutcomeCounts(edgeOutcomeCounts); -// setMaxEdgeCount(maxEdgeCount); -// -// // Set the initial selected sequence -// const initialSequence = top5Sequences[0][0]; // Adjust based on your top sequences structure -// setSelectedSequence(initialSequence); -// -// // Generate the initial DOT string -// const normalizedThicknesses = normalizeThicknesses(edgeCounts, maxEdgeCount, 10); -// let newDotString = generateDotString(normalizedThicknesses, ratioEdges, edgeOutcomeCounts, edgeCounts, totalNodeEdges, 0, minVisits, initialSequence); -// newDotString = applySequenceColors(newDotString, initialSequence); -// setDotString(newDotString); -// }; -// -// fetchData(); -// }, []); -// -// // Effect to regenerate the non-filtered DOT string when dependencies change -// useEffect(() => { -// const normalizedThicknesses = normalizeThicknesses(edgeCounts, maxEdgeCount, 10); -// let newDotString = generateDotString(normalizedThicknesses, ratioEdges, edgeOutcomeCounts, edgeCounts, totalNodeEdges, 0, minVisits, selectedSequence); -// newDotString = applySequenceColors(newDotString, selectedSequence); -// setDotString(newDotString); -// }, [selectedSequence, edgeCounts, maxEdgeCount, ratioEdges, edgeOutcomeCounts, totalNodeEdges, minVisits, selfLoops]); -// -// // Effect to regenerate filtered DOT string when filterCriteria or selfLoops change -// useEffect(() => { -// if (filterCriteria) { -// const filteredData = csvData.filter(row => row['CF (Workspace Progress Status)'] === filterCriteria); -// const filteredStepSequences = createStepSequences(filteredData, selfLoops); -// const filteredOutcomeSequences = createOutcomeSequences(filteredData); -// -// const { -// edgeCounts: filteredEdgeCounts, -// totalNodeEdges: filteredTotalNodeEdges, -// ratioEdges: filteredRatioEdges, -// edgeOutcomeCounts: filteredEdgeOutcomeCounts, -// maxEdgeCount: filteredMaxEdgeCount, -// } = countEdges(filteredStepSequences, filteredOutcomeSequences); -// -// const filteredNormalizedThicknesses = normalizeThicknesses(filteredEdgeCounts, filteredMaxEdgeCount, 10); -// -// let filteredDotStr = generateDotString( -// filteredNormalizedThicknesses, -// filteredRatioEdges, -// filteredEdgeOutcomeCounts, -// filteredEdgeCounts, -// filteredTotalNodeEdges, -// 1, -// minVisits, -// selectedSequence // Apply selected sequence here -// ); -// -// filteredDotStr = applySequenceColors(filteredDotStr, selectedSequence); -// setFilteredDotString(filteredDotStr); -// } else { -// setFilteredDotString(null); // Set to null if no filter is applied -// } -// }, [csvData, filterCriteria, selfLoops, minVisits, selectedSequence]); -// -// -// return ( -//
-// -//
-// {dotString && ( -// -// )} -// {filteredDotString && ( -// -// )} -//
-//
-//
-// ); -// } -// -// export default GraphvizParent; - -import React, {useEffect, useState} from 'react'; +import React, {useContext, useEffect, useState} from 'react'; import { generateDotString, normalizeThicknesses, @@ -489,21 +10,16 @@ import { import Graphviz from "graphviz-react"; import ErrorBoundary from "@/components/errorBoundary.tsx"; import '../GraphvizContainer.css'; -import sequenceSelector from "@/components/SequenceSelector.tsx"; -import SequenceSelector from "@/components/SequenceSelector.tsx"; +import {Context, SequenceCount} from "@/Context.tsx"; interface GraphvizParentProps { csvData: string; filter: string | null; selfLoops: boolean; minVisits: number; - selectedSequence: string[]; + selectedSequence: SequenceCount["sequence"] | null; } -export interface SequenceCount { - sequence: string[]; // or whatever type your steps are (e.g., number[]) - count: number; -} const GraphvizParent: React.FC = ({ csvData, @@ -514,8 +30,9 @@ const GraphvizParent: React.FC = ({ }) => { const [dotString, setDotString] = useState(null); const [filteredDotString, setFilteredDotString] = useState(null); - const [top5Sequences, setTop5Sequences] = useState('') - const [selectedSequence, setSelectedSequence] = useState([]) + const [selectedSequence, setSelectedSequence] = useState([]) + const {top5Sequences, setTop5Sequences} = useContext(Context); + // const [selectedSequenceIndex, setSelectedSequenceIndex] = useState(0); useEffect(() => { @@ -529,11 +46,14 @@ const GraphvizParent: React.FC = ({ ratioEdges, edgeOutcomeCounts, maxEdgeCount, - top5Sequences + topSequences } = countEdges(stepSequences, outcomeSequences); - - setTop5Sequences(top5Sequences) - + console.log("Before: " + topSequences) + setTop5Sequences(topSequences) + if (top5Sequences != null) { + console.log("THIS: " + top5Sequences) + setSelectedSequence(top5Sequences[0]["sequence"]) + } const normalizedThicknesses = normalizeThicknesses(edgeCounts, maxEdgeCount, 10); let generatedDotStr = generateDotString( @@ -548,8 +68,11 @@ const GraphvizParent: React.FC = ({ ); setDotString(generatedDotStr); + console.log(selectedSequence) + console.log(dotString) }, [csvData, selfLoops, minVisits, selectedSequence]); + useEffect(() => { if (filter) { const sortedData = loadAndSortData(csvData); @@ -579,20 +102,16 @@ const GraphvizParent: React.FC = ({ ); setFilteredDotString(filteredDotStr); + console.log(selectedSequence) + console.log(filteredDotStr) } else { setFilteredDotString(null); } }, [csvData, filter, selfLoops, minVisits, selectedSequence]); - const handleSequenceChange = (event: React.ChangeEvent) => { - const selectedIndex: number = parseInt(event.target.value); - setSelectedSequence(top5Sequences[selectedIndex].sequence || []); - }; - // const handleTop5Sequences = useState(top5Sequences); + return (
- -
{dotString && ( @@ -612,27 +131,5 @@ const GraphvizParent: React.FC = ({
); } -// return ( -//
-// -//
-// {dotString && ( -// -// )} -// {filteredDotString && ( -// -// )} -//
-//
-//
-// ); -// } - export default GraphvizParent; \ No newline at end of file diff --git a/src/components/GraphvizProcessing.ts b/src/components/GraphvizProcessing.ts index a7e9343..8bf5440 100644 --- a/src/components/GraphvizProcessing.ts +++ b/src/components/GraphvizProcessing.ts @@ -1,5 +1,5 @@ import Papa from 'papaparse'; -import {SequenceCount} from "@/components/GraphvizParent.tsx"; +import {SequenceCount} from "@/Context"; interface CSVRow { 'Session Id': string; @@ -69,29 +69,12 @@ export const createOutcomeSequences = (sortedData: CSVRow[]): { [key: string]: s }, {} as { [key: string]: string[] }); }; -// export function getTopSequences(stepSequences: any, topN: number = 5) { -// // const topSequences = Object.values(stepSequences) -// // .sort((a, b) => stepSequences[b].length - stepSequences[a].length) -// // .slice(0, topN); -// -// const sequenceCounts = Object.entries(stepSequences).reduce((acc: any, [key, value]) => { -// acc[key] = value.count; -// return acc; -// }, {}); -// console.log("counts: " + sequenceCounts[stepSequences[0]]) -// const sortedSequences = Object.entries(sequenceCounts) -// .sort(([, a], [, b]) => b - a) -// .slice(0, topN); -// console.log("sorted sequences: " + sequenceCounts[sortedSequences[0]]) -// // return sortedSequences.map(([sequence]) => sequence); -// return topSequences -// } export function getTopSequences(stepSequences: any, topN: number = 5) { // Create a frequency map to count how many times each unique sequence (list) occurs const sequenceCounts: { [sequence: string]: number } = {}; // Iterate over the values (which are lists) of the stepSequences dictionary - Object.values(stepSequences).forEach((sequence: string[]) => { + Object.values(stepSequences).forEach((sequence) => { const sequenceKey = JSON.stringify(sequence); // Convert the list to a string key // Count occurrences of each unique sequence @@ -135,7 +118,7 @@ export const countEdges = ( maxEdgeCount: number; ratioEdges: { [p: string]: number }; edgeCounts: { [p: string]: number }; - top5Sequences: SequenceCount; + topSequences: SequenceCount[]; } => { const edgeCounts: { [key: string]: number } = {}; const totalNodeEdges: { [key: string]: number } = {}; @@ -176,7 +159,7 @@ export const countEdges = ( ratioEdges[edge] = edgeCounts[edge] / (totalNodeEdges[start] || 0); }); - return {edgeCounts, totalNodeEdges, ratioEdges, edgeOutcomeCounts, maxEdgeCount, top5Sequences}; + return {edgeCounts, totalNodeEdges, ratioEdges, edgeOutcomeCounts, maxEdgeCount, topSequences: top5Sequences}; }; @@ -272,13 +255,13 @@ export function generateDotString( totalNodeEdges: EdgeCounts['totalNodeEdges'], threshold: number, min_visits: number, - selectedSequence: string[] + selectedSequence: SequenceCount["sequence"] ): string { const stepsInSelectedSequence = selectedSequence//.split('->'); // console.log(mostCommonSequence) - console.log("selectedSequence" + stepsInSelectedSequence) + console.log("selectedSequenceR" + stepsInSelectedSequence) // console.log(selectedSequence[stepsInSelectedSequence]) - const totalSteps = stepsInSelectedSequence.length; + const totalSteps = selectedSequence.length//stepsInSelectedSequence.length; console.log("totalSteps" + totalSteps) // Create node definitions in the DOT string let dotString = 'digraph G {\n'; diff --git a/src/components/SequenceSelector.tsx b/src/components/SequenceSelector.tsx index 68ed8ba..92d9e5c 100644 --- a/src/components/SequenceSelector.tsx +++ b/src/components/SequenceSelector.tsx @@ -1,30 +1,39 @@ import React from 'react'; -import {SequenceCount} from "@/components/GraphvizParent.tsx"; +import {SequenceCount} from "@/Context"; interface SequenceSelectorProps { - sequences: SequenceCount|string; - selectedSequence: string; - onSequenceSelect: (sequence: string) => void; + sequences: SequenceCount[] | null; + selectedSequence: string[] | null; + onSequenceSelect: (sequence: string[]) => void; } const SequenceSelector: React.FC = ({ - sequences, - selectedSequence, - onSequenceSelect, -}) => { - if (sequences == '') { + sequences, + selectedSequence, + onSequenceSelect, + }) => { + + if (sequences == null) { return
No sequences available
; // Display a message when no sequences are present } + // sequences.map((seq: SequenceCount) => { + // const count: number = seq.count + // const localSequence: string[] = seq.sequence + // localSequence.map((s: string) => { + // console.log(s) + // }) + // }) return (
+
); }; From 6cc64f439879a9bc188a108912661357c35f41d6 Mon Sep 17 00:00:00 2001 From: talizacks <55198700+talizacks@users.noreply.github.com> Date: Fri, 30 Aug 2024 14:25:36 -0500 Subject: [PATCH 24/54] Everything working again except node color still won't update with sequence change --- src/App.tsx | 9 +- src/Context.tsx | 15 +- src/GraphvizContainer.css | 2 +- src/components/GraphvizParent.tsx | 115 +++++---- src/components/GraphvizProcessing.ts | 345 +-------------------------- src/components/SequenceSelector.tsx | 4 +- src/components/slider.tsx | 2 +- src/main.tsx | 2 +- 8 files changed, 101 insertions(+), 393 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index d0b040d..3b330a9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,8 +17,11 @@ const App: React.FC = () => { const [selectedSequence, setSelectedSequence] = useState(null); const handleSelectSequence = (selectedSequence: SequenceCount["sequence"]) => { - setSelectedSequence(selectedSequence); // Fix: Use the correct parameter to update the state - console.log(`Selected sequence: ${selectedSequence}`); + if (top5Sequences) { + setSelectedSequence(selectedSequence); // Fix: Use the correct parameter to update the state + console.log(`Selected sequence: ${selectedSequence}`); + //Selected sequence gets this far but doesn't update in GraphvizProcessing + } }; const handleToggle = () => setSelfLoops(!selfLoops); @@ -32,7 +35,7 @@ const App: React.FC = () => {

{selectedSequence}

- void; } + export interface SequenceCount { sequence: string[] | null; // or whatever type your steps are (e.g., number[]) - count: number; + count: number | null; } + export const Context = createContext({} as ContextInterface); const initialState = { data: null, @@ -29,16 +32,16 @@ interface ProviderProps { } -export const Provider = ({ children }: ProviderProps) => { +export const Provider = ({children}: ProviderProps) => { const [data, setData] = useState(initialState.data) const [graphData, setGraphData] = useState(initialState.graphData) const [loading, setLoading] = useState(initialState.loading) - const [top5Sequences, setTop5Sequences] = useState(initialState.top5Sequences) + const [top5Sequences, setTop5Sequences] = useState(initialState.top5Sequences) const resetData = () => { setData(null) setGraphData(null) console.log("Data reset"); - + } return ( diff --git a/src/GraphvizContainer.css b/src/GraphvizContainer.css index a5aea5e..78c631d 100644 --- a/src/GraphvizContainer.css +++ b/src/GraphvizContainer.css @@ -19,5 +19,5 @@ .graphs > div { flex: 1; /* Allow the graphs to scale according to available space */ - max-width: 800px; /* Set a max-width for each graph */ + max-width: 600px; /* Set a max-width for each graph */ } diff --git a/src/components/GraphvizParent.tsx b/src/components/GraphvizParent.tsx index 93e1c4b..ff914ed 100644 --- a/src/components/GraphvizParent.tsx +++ b/src/components/GraphvizParent.tsx @@ -17,61 +17,59 @@ interface GraphvizParentProps { filter: string | null; selfLoops: boolean; minVisits: number; - selectedSequence: SequenceCount["sequence"] | null; -} + selectedSequence: string[]|null; +} const GraphvizParent: React.FC = ({ csvData, filter, selfLoops, minVisits, - }) => { const [dotString, setDotString] = useState(null); const [filteredDotString, setFilteredDotString] = useState(null); - const [selectedSequence, setSelectedSequence] = useState([]) + const [selectedSequence, setSelectedSequence] = useState(null); const {top5Sequences, setTop5Sequences} = useContext(Context); - // const [selectedSequenceIndex, setSelectedSequenceIndex] = useState(0); - useEffect(() => { - const sortedData = loadAndSortData(csvData); - const stepSequences = createStepSequences(sortedData, selfLoops); - const outcomeSequences = createOutcomeSequences(sortedData); - - const { - edgeCounts, - totalNodeEdges, - ratioEdges, - edgeOutcomeCounts, - maxEdgeCount, - topSequences - } = countEdges(stepSequences, outcomeSequences); - console.log("Before: " + topSequences) - setTop5Sequences(topSequences) - if (top5Sequences != null) { - console.log("THIS: " + top5Sequences) - setSelectedSequence(top5Sequences[0]["sequence"]) - } - const normalizedThicknesses = normalizeThicknesses(edgeCounts, maxEdgeCount, 10); - - let generatedDotStr = generateDotString( - normalizedThicknesses, - ratioEdges, - edgeOutcomeCounts, - edgeCounts, - totalNodeEdges, - 1, - minVisits, - selectedSequence - ); + if (csvData) { + const sortedData = loadAndSortData(csvData); + const stepSequences = createStepSequences(sortedData, selfLoops); + const outcomeSequences = createOutcomeSequences(sortedData); - setDotString(generatedDotStr); - console.log(selectedSequence) - console.log(dotString) - }, [csvData, selfLoops, minVisits, selectedSequence]); + const { + edgeCounts, + totalNodeEdges, + ratioEdges, + edgeOutcomeCounts, + maxEdgeCount, + topSequences + } = countEdges(stepSequences, outcomeSequences); + + if (JSON.stringify(top5Sequences) !== JSON.stringify(topSequences)) { + setTop5Sequences(topSequences); + } + + if (!selectedSequence && topSequences.length > 0) { + setSelectedSequence(topSequences[0].sequence); + } + + const normalizedThicknesses = normalizeThicknesses(edgeCounts, maxEdgeCount, 10); + const generatedDotStr = generateDotString( + normalizedThicknesses, + ratioEdges, + edgeOutcomeCounts, + edgeCounts, + totalNodeEdges, + 1, + minVisits, + selectedSequence + ); + setDotString(generatedDotStr); + } + }, [csvData, selfLoops, minVisits, top5Sequences, selectedSequence]); useEffect(() => { if (filter) { @@ -89,8 +87,7 @@ const GraphvizParent: React.FC = ({ } = countEdges(filteredStepSequences, filteredOutcomeSequences); const filteredNormalizedThicknesses = normalizeThicknesses(filteredEdgeCounts, filteredMaxEdgeCount, 10); - - let filteredDotStr = generateDotString( + const filteredDotStr = generateDotString( filteredNormalizedThicknesses, filteredRatioEdges, filteredEdgeOutcomeCounts, @@ -102,14 +99,40 @@ const GraphvizParent: React.FC = ({ ); setFilteredDotString(filteredDotStr); - console.log(selectedSequence) - console.log(filteredDotStr) } else { setFilteredDotString(null); } }, [csvData, filter, selfLoops, minVisits, selectedSequence]); +useEffect(() => { + if (dotString && selectedSequence) { + const sortedData = loadAndSortData(csvData); + const stepSequences = createStepSequences(sortedData, selfLoops); + const outcomeSequences = createOutcomeSequences(sortedData); + const { + edgeCounts, + totalNodeEdges, + ratioEdges, + edgeOutcomeCounts, + maxEdgeCount, + } = countEdges(stepSequences, outcomeSequences); + + const normalizedThicknesses = normalizeThicknesses(edgeCounts, maxEdgeCount, 10); + const updatedDotStr = generateDotString( + normalizedThicknesses, + ratioEdges, + edgeOutcomeCounts, + edgeCounts, + totalNodeEdges, + 1, + minVisits, + selectedSequence + ); + + setDotString(updatedDotStr); + } + }, [selectedSequence]); return (
@@ -130,6 +153,6 @@ const GraphvizParent: React.FC = ({
); -} +}; -export default GraphvizParent; \ No newline at end of file +export default GraphvizParent; diff --git a/src/components/GraphvizProcessing.ts b/src/components/GraphvizProcessing.ts index 8bf5440..db82aa7 100644 --- a/src/components/GraphvizProcessing.ts +++ b/src/components/GraphvizProcessing.ts @@ -126,7 +126,7 @@ export const countEdges = ( const edgeOutcomeCounts: { [key: string]: { [outcome: string]: number } } = {}; let maxEdgeCount = 0; - const top5Sequences = getTopSequences(stepSequences, 10) + const top5Sequences = getTopSequences(stepSequences, 5) // Process edges for all sequences Object.keys(stepSequences).forEach((sessionId) => { @@ -257,23 +257,29 @@ export function generateDotString( min_visits: number, selectedSequence: SequenceCount["sequence"] ): string { + if (!selectedSequence || selectedSequence.length === 0) { + return 'digraph G {\n"Error" [label="No valid sequences found to display."];\n}'; + } + console.log("TOTAL STEP NUMBER: " + selectedSequence.length) const stepsInSelectedSequence = selectedSequence//.split('->'); // console.log(mostCommonSequence) console.log("selectedSequenceR" + stepsInSelectedSequence) // console.log(selectedSequence[stepsInSelectedSequence]) - const totalSteps = selectedSequence.length//stepsInSelectedSequence.length; - console.log("totalSteps" + totalSteps) // Create node definitions in the DOT string let dotString = 'digraph G {\n'; + + let totalSteps = selectedSequence.length//stepsInSelectedSequence.length; + console.log("totalSteps" + totalSteps) for (let rank = 0; rank < totalSteps; rank++) { - const step = stepsInSelectedSequence[rank]; + const step = stepsInSelectedSequence![rank]; const color = calculateColor(rank, totalSteps); const node_tooltip = `Rank:\n\t\t ${rank + 1}\nColor:\n\t\t ${color}`; dotString += ` "${step}" [rank=${rank + 1}, style=filled, fillcolor="${color}", tooltip="${node_tooltip}"];\n`; + } - // Create edge definitions in the DOT string based on normalized thickness and thresholds +// Create edge definitions in the DOT string based on normalized thickness and thresholds for (const edge of Object.keys(normalizedThicknesses)) { if (normalizedThicknesses[edge] >= threshold) { const [currentStep, nextStep] = edge.split('->'); @@ -301,331 +307,4 @@ export function generateDotString( dotString += '}'; return dotString; -} - -// import Papa from 'papaparse'; -// -// interface CSVRow { -// 'Session Id': string; -// 'Time': string; -// 'Step Name': string; -// 'Outcome': string; -// 'CF (Workspace Progress Status)': string; -// } -// -// // Function to load and sort data -// export const loadAndSortData = (csvData: string): CSVRow[] => { -// // Step 1: Parse the CSV data using PapaParse -// const parsedData = Papa.parse(csvData, { -// header: true, -// skipEmptyLines: true -// }).data; -// -// // Step 2: Transform data to replace missing Step Names with a default value -// const transformedData = parsedData.map(row => { -// return { -// 'Session Id': row['Session Id'], -// 'Time': row['Time'], -// 'Step Name': row['Step Name'] || 'DoneButton', // Default value for missing Step Names -// 'Outcome': row['Outcome'], -// 'CF (Workspace Progress Status)': row['CF (Workspace Progress Status)'], -// }; -// }); -// -// // Step 3: Sort the transformed data by Session Id and Time -// return transformedData.sort((a, b) => { -// if (a['Session Id'] === b['Session Id']) { -// return new Date(a['Time']).getTime() - new Date(b['Time']).getTime(); -// } -// return a['Session Id'].localeCompare(b['Session Id']); -// }); -// }; -// -// // Function to create step sequences from sorted data -// export const createStepSequences = (sortedData: CSVRow[], selfLoops: boolean): { [key: string]: string[] } => { -// // Iterate over sorted data to build step sequences -// return sortedData.reduce((acc, row) => { -// const sessionId = row['Session Id']; -// if (!acc[sessionId]) { -// acc[sessionId] = []; -// } -// const stepName = row['Step Name']; -// -// // Add step to sequence based on whether self-loops are allowed -// if (selfLoops || acc[sessionId].length === 0 || acc[sessionId][acc[sessionId].length - 1] !== stepName) { -// acc[sessionId].push(stepName); -// } -// -// return acc; -// }, {} as { [key: string]: string[] }); -// }; -// -// // Function to create outcome sequences from sorted data -// export const createOutcomeSequences = (sortedData: CSVRow[]): { [key: string]: string[] } => { -// // Iterate over sorted data to build outcome sequences -// return sortedData.reduce((acc, row) => { -// const sessionId = row['Session Id']; -// if (!acc[sessionId]) { -// acc[sessionId] = []; -// } -// acc[sessionId].push(row['Outcome']); -// return acc; -// }, {} as { [key: string]: string[] }); -// }; -// -// // export function getTopSequences(stepSequences: any, topN: number = 5) { -// // // const topSequences = Object.values(stepSequences) -// // // .sort((a, b) => stepSequences[b].length - stepSequences[a].length) -// // // .slice(0, topN); -// // -// // const sequenceCounts = Object.entries(stepSequences).reduce((acc: any, [key, value]) => { -// // acc[key] = value.count; -// // return acc; -// // }, {}); -// // console.log("counts: " + sequenceCounts[stepSequences[0]]) -// // const sortedSequences = Object.entries(sequenceCounts) -// // .sort(([, a], [, b]) => b - a) -// // .slice(0, topN); -// // console.log("sorted sequences: " + sequenceCounts[sortedSequences[0]]) -// // // return sortedSequences.map(([sequence]) => sequence); -// // return topSequences -// // } -// export function getTopSequences(stepSequences: any, topN: number = 5) { -// // Create a frequency map to count how many times each unique sequence (list) occurs -// const sequenceCounts: { [sequence: string]: number } = {}; -// -// // Iterate over the values (which are lists) of the stepSequences dictionary -// Object.values(stepSequences).forEach((sequenceList: string[]) => { -// const sequenceKey = JSON.stringify(sequenceList); // Convert the list to a string key -// -// if (sequenceCounts[sequenceKey]) { -// sequenceCounts[sequenceKey]++; -// } else { -// sequenceCounts[sequenceKey] = 1; -// } -// }); -// -// // Sort the sequences based on their counts in descending order and take the top N -// const sortedSequences = Object.entries(sequenceCounts) -// .sort(([, countA], [, countB]) => countB - countA) -// .slice(0, topN); -// console.log("Sorted w counts: " + sortedSequences) -// console.log(sortedSequences.map(([sequenceKey, count]) => JSON.parse(sequenceKey, sequenceCounts[sequenceKey]))) -// const topSequences = sortedSequences.map(([sequenceKey, count]) => JSON.parse(sequenceKey, sequenceCounts[sequenceKey])); -// return topSequences; -// } -// -// interface EdgeCounts { -// edgeCounts: { [key: string]: number }; -// totalNodeEdges: { [key: string]: number }; -// ratioEdges: { [key: string]: number }; -// edgeOutcomeCounts: { [key: string]: { [outcome: string]: number } }; -// } -// -// // Function to count edges between steps -// export const countEdges = ( -// stepSequences: { [key: string]: string[] }, -// outcomeSequences: { [key: string]: string[] } -// ): { -// totalNodeEdges: { [p: string]: number }; -// edgeOutcomeCounts: { [p: string]: { [p: string]: number } }; -// maxEdgeCount: number; -// ratioEdges: { [p: string]: number }; -// edgeCounts: { [p: string]: number }; -// top5Sequences: string[]; -// } => { -// const edgeCounts: { [key: string]: number } = {}; -// const totalNodeEdges: { [key: string]: number } = {}; -// const ratioEdges: { [key: string]: number } = {}; -// const edgeOutcomeCounts: { [key: string]: { [outcome: string]: number } } = {}; -// let maxEdgeCount = 0; -// -// const top5Sequences = getTopSequences(stepSequences, 5) -// -// // Process edges for all sequences -// Object.keys(stepSequences).forEach((sessionId) => { -// const steps = stepSequences[sessionId]; -// // console.log(steps) -// const outcomes = outcomeSequences[sessionId]; -// -// if (steps.length < 2) return; -// -// for (let i = 0; i < steps.length - 1; i++) { -// const currentStep = steps[i]; -// const nextStep = steps[i + 1]; -// const outcome = outcomes[i + 1]; -// -// const edgeKey = `${currentStep}->${nextStep}`; -// edgeCounts[edgeKey] = (edgeCounts[edgeKey] || 0) + 1; -// edgeOutcomeCounts[edgeKey] = edgeOutcomeCounts[edgeKey] || {}; -// edgeOutcomeCounts[edgeKey][outcome] = (edgeOutcomeCounts[edgeKey][outcome] || 0) + 1; -// totalNodeEdges[currentStep] = (totalNodeEdges[currentStep] || 0) + 1; -// -// // Track the maximum edge count -// if (edgeCounts[edgeKey] > maxEdgeCount) { -// maxEdgeCount = edgeCounts[edgeKey]; -// } -// } -// }); -// -// Object.keys(edgeCounts).forEach((edge) => { -// const [start] = edge.split('->'); -// ratioEdges[edge] = edgeCounts[edge] / (totalNodeEdges[start] || 0); -// }); -// -// return {edgeCounts, totalNodeEdges, ratioEdges, edgeOutcomeCounts, maxEdgeCount, top5Sequences}; -// }; -// -// -// // // Function to normalize edge thicknesses based on their ratio -// // export function normalizeThicknesses( -// // ratioEdges: { [key: string]: number }, -// // maxThickness: number -// // ): { [key: string]: number } { -// // const normalized: { [key: string]: number } = {}; -// // const maxRatio = Math.max(...Object.values(ratioEdges), 1); // Avoid division by zero -// // -// // // Scale edge thicknesses to a maximum value -// // Object.keys(ratioEdges).forEach((edge) => { -// // const ratio = ratioEdges[edge]; -// // normalized[edge] = (ratio / maxRatio) * maxThickness; -// // }); -// // -// // return normalized; -// // } -// -// // // Function to normalize edge thicknesses based the full graph -// export function normalizeThicknesses( -// edgeCounts: { [key: string]: number }, -// maxEdgeCount: number, -// maxThickness: number -// ): { [key: string]: number } { -// const normalized: { [key: string]: number } = {}; -// -// Object.keys(edgeCounts).forEach((edge) => { -// const count = edgeCounts[edge]; -// normalized[edge] = (count / maxEdgeCount) * maxThickness; -// }); -// -// return normalized; -// } -// -// -// // Function to calculate the color of a node based on its rank in the most common sequence -// function calculateColor(sequence: string[], rank: number, totalSteps: number): string { -// const ratio = rank / totalSteps; -// -// const white = {r: 255, g: 255, b: 255}; -// const lightBlue = {r: 0, g: 166, b: 255}; -// -// const r = Math.round(white.r * (1 - ratio) + lightBlue.r * ratio); -// const g = Math.round(white.g * (1 - ratio) + lightBlue.g * ratio); -// const b = Math.round(white.b * (1 - ratio) + lightBlue.b * ratio); -// -// const toHex = (value: number) => value.toString(16).padStart(2, '0'); -// const color = `#${toHex(r)}${toHex(g)}${toHex(b)}`; -// -// return color; -// } -// -// // Function to calculate the color of an edge based on its outcome distribution -// function calculateEdgeColors(outcomes: { [outcome: string]: number }): string { -// const colorMap: { [key: string]: string } = { -// 'ERROR': '#ff0000', // Red -// 'OK': '#00ff00', // Green -// 'INITIAL_HINT': '#0000ff', // Blue -// 'HINT_LEVEL_CHANGE': '#0000ff', // Blue -// 'JIT': '#ffff00', // Yellow -// 'FREEBIE_JIT': '#ffff00' // Yellow -// }; -// -// if (Object.keys(outcomes).length === 0) { -// return '#00000000'; // Transparent black -// } -// -// const totalCount = Object.values(outcomes).reduce((sum, count) => sum + count, 0); -// let weightedR = 0, weightedG = 0, weightedB = 0; -// -// Object.entries(outcomes).forEach(([outcome, count]) => { -// const color = colorMap[outcome] || '#000000'; // Default to black if outcome is not found -// const [r, g, b] = [1, 3, 5].map(i => parseInt(color.slice(i, i + 2), 16)); // Extract RGB values -// const weight = count / totalCount; -// weightedR += r * weight; -// weightedG += g * weight; -// weightedB += b * weight; -// }); -// -// // Convert RGB values to hex and add alpha transparency -// return `#${Math.round(weightedR).toString(16).padStart(2, '0')}${Math.round(weightedG).toString(16).padStart(2, '0')}${Math.round(weightedB).toString(16).padStart(2, '0')}90`; -// } -// -// // Function to generate a Graphviz DOT string for visualization -// export function generateDotString( -// normalizedThicknesses: { [key: string]: number }, -// // mostCommonSequence: string[], -// ratioEdges: { [key: string]: number }, -// edgeOutcomeCounts: EdgeCounts['edgeOutcomeCounts'], -// edgeCounts: EdgeCounts['edgeCounts'], -// totalNodeEdges: EdgeCounts['totalNodeEdges'], -// threshold: number, -// min_visits: number, -// selectedSequence: string -// ): string { -// // const stepsInSelectedSequence = selectedSequence//.split('->'); -// // console.log(mostCommonSequence) -// // console.log(selectedSequence[stepsInSelectedSequence]) -// const totalSteps = selectedSequence.length; -// -// console.log("selected sequence" + selectedSequence) -// console.log("totalSteps" + totalSteps) -// // Create node definitions in the DOT string -// let dotString = 'digraph G {\n'; -// Object.keys(edgeCounts).forEach((sourceNode) => { -// // Determine the rank of the node in the selected sequence -// const rank = selectedSequence.indexOf(sourceNode) + 1; -// const color = rank > 0 ? calculateColor(selectedSequence, rank, totalSteps) : '#FFFFFF'; // Default to white if not in sequence -// -// // dotString += `"${sourceNode}" [style=filled, fillcolor="${color}"];\n`; -// -// // for (let rank = 0; rank < totalSteps; rank++) { -// // const step = stepsInSelectedSequence[rank]; -// // const color = calculateColor(selectedSequence, rank, totalSteps); -// // console.log(color, step) -// const node_tooltip = `Rank:\n\t\t ${rank + 1}\nColor:\n\t\t ${color}`; -// -// dotString += ` "${sourceNode}" [rank=${rank + 1}, style=filled, fillcolor="${color}", tooltip="${node_tooltip}"];\n`; -// } -// -// // Create edge definitions in the DOT string based on normalized thickness and thresholds -// for (const edge of Object.keys(normalizedThicknesses)) -// { -// if (normalizedThicknesses[edge] >= threshold) { -// const [currentStep, nextStep] = edge.split('->'); -// const thickness = normalizedThicknesses[edge]; -// const outcomes = edgeOutcomeCounts[edge] || {}; -// const edgeCount = edgeCounts[edge] || 0; -// const totalCount = totalNodeEdges[currentStep] || 0; -// const color = calculateEdgeColors(outcomes); -// const outcomesStr = Object.entries(outcomes) -// .map(([outcome, count]) => `${outcome}: ${count}`) -// .join('\n\t\t '); -// -// if (edgeCount > min_visits) { -// const tooltip = `${currentStep} to ${nextStep}\n` -// + `- Edge Count: \n\t\t ${edgeCount}\n` -// + `- Total Count for ${currentStep}: \n\t\t${totalCount}\n` -// + `- Ratio: \n\t\t${((ratioEdges[edge] || 0) * 100).toFixed(2)}% of students at ${currentStep} go to ${nextStep}\n` -// + `- Outcomes: \n\t\t ${outcomesStr}\n` -// + `- Color Codes: \n\t\t Hex: ${color}\n\t\t RGB: ${[parseInt(color.substring(1, 3), 16), parseInt(color.substring(3, 5), 16), parseInt(color.substring(5, 7), 16)]}`; -// -// dotString += ` "${currentStep}" -> "${nextStep}" [penwidth=${thickness}, color="${color}", tooltip="${tooltip}"];\n`; -// } -// } -// } -// -// dotString += '}'; -// return dotString; -// } -// - - +} \ No newline at end of file diff --git a/src/components/SequenceSelector.tsx b/src/components/SequenceSelector.tsx index 92d9e5c..b5a74d5 100644 --- a/src/components/SequenceSelector.tsx +++ b/src/components/SequenceSelector.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {SequenceCount} from "@/Context"; interface SequenceSelectorProps { - sequences: SequenceCount[] | null; + sequences: SequenceCount[]|null; selectedSequence: string[] | null; onSequenceSelect: (sequence: string[]) => void; } @@ -26,7 +26,7 @@ const SequenceSelector: React.FC = ({ // }) return (
- onSequenceSelect([e.target.value])}> {sequences.map((seq: SequenceCount) => (