Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: CF-423: additional styling on course node hover #1034

Merged
merged 43 commits into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
366f395
feat: dark mode functionality added for editMarkModal's input and but…
Dasyure Mar 13, 2023
76578e4
feat: dark mode improvement for editMarkModal's cancel button
Dasyure Mar 13, 2023
69ec162
feat: dark mode improvements for OptionHeader icons underneath the 'T…
Dasyure Mar 14, 2023
b4e18ec
feat: dark mode added for the select menu in the settingsMenu tooltip…
Dasyure Mar 19, 2023
fdded9f
feat: dark mode added to SettingMenu's DatePicker element
Dasyure Mar 19, 2023
20bed7a
feat: dark mode for export button done + editModalMark bug fixed
Dasyure Mar 19, 2023
d9413ec
fix: making sure the css for the select element in settingsMenu does …
Dasyure Mar 19, 2023
6a2d53d
feat: dark mode improvement for popconfirm for unplan wanring, import…
Dasyure Mar 21, 2023
6fc5273
feat: dark mode scrollbar added
Dasyure Mar 21, 2023
87139ec
feat: dark mode scrollbar on courseSelector menu had ugly white paddi…
Dasyure Mar 21, 2023
e3c93c7
fix: href in courseSelector not very readable in dark mode, made the …
Dasyure Mar 21, 2023
2240f9c
feat: dark mode added for search bar
Dasyure Mar 21, 2023
c6c4c9d
feat: dark mode added for remove planner button
Dasyure Mar 22, 2023
49bb64d
fix: forgot to add the new styles.ts file
Dasyure Mar 22, 2023
c01c969
feat: progressBar's text color and trailing color fixed
Dasyure Mar 22, 2023
b7db298
feat: dividing line in courseDescription changed from white to a dark…
Dasyure Mar 22, 2023
c2cc02d
feat: bug icon turned into dark mode
Dasyure Mar 22, 2023
02c619f
feat: dark mode added for quick add and remove buttons in course menu
Dasyure Mar 22, 2023
6c7fb22
feat: courseProgression progress bar trailing color changed to dark grey
Dasyure Mar 22, 2023
de30074
feat: progress on dark mode for graph, need to save this commit befor…
Dasyure Mar 25, 2023
7dadf3d
fix: fixing merge conflict, as I needed Oli's changes in order to mak…
Dasyure Mar 25, 2023
d16b122
feat: dark mode for graph complete (nodes, arrows, hover states) + la…
Dasyure Mar 25, 2023
9c6a707
feat: buttons on graphical selector are dark mode
Dasyure Mar 26, 2023
4efb5fd
feat: saving progress on converting courseDescription panel to dark mode
Dasyure Mar 26, 2023
4abb9fa
feat: dark mode added to the sidebar
Dasyure Mar 26, 2023
bb0e323
feat: sidebardrawer color changed, box shadow added to tabs so it loo…
Dasyure Mar 26, 2023
9c96ac7
feat: new images added in help menu in course selector, dark mode ver…
Dasyure Mar 26, 2023
ccd6087
feat: TermPlanner's help menu tooltips now have dark mode pics and gifs
Dasyure Mar 26, 2023
a70de35
feat: highlight adjacent nodes and edges on hover
Dasyure Mar 28, 2023
3f429b1
feat: highlight adjacent nodes opacity updated
Dasyure Mar 28, 2023
78bf7b2
refactor: graph.ts, changing function names and object names to be mo…
Dasyure Mar 28, 2023
8878308
feat: implemented a function that checks if a course is a prereq base…
Dasyure Mar 28, 2023
71946ed
fix: two graphs get rendered if you switch tabs fast enough
Dasyure Mar 28, 2023
e16ad2a
feat: created a function to store a hashmap of prereqs for later use …
Dasyure Mar 28, 2023
3d58f93
fix: updated the function that checks for coursePrerequisite
Dasyure Mar 28, 2023
5509034
refactor: graph.ts function and object names made more readable
Dasyure Mar 28, 2023
69f6c82
refactor: rewriting the returns and using spread operator to reduce r…
Dasyure Mar 28, 2023
3089e4b
feat: highlight prerequisite nodes on hover
Dasyure Mar 29, 2023
30819cd
refactor: splitting functions up as they were getting too long
Dasyure Mar 29, 2023
695467a
fix: if the dark mode button is toggled on and off, it repaints the c…
Dasyure Mar 29, 2023
3af8752
feat: highlighted incoming edge if it's a prerequisite as well
Dasyure Mar 29, 2023
fe6cbc4
feat: forgot to add pics into the HelpMenu for the new graphical sele…
Dasyure Mar 29, 2023
7729fbb
Merge branch 'dev' into CF-423/neighbouring-nodes-styling
leonardo-fan Apr 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified frontend/src/assets/GraphicalSelectorHelp/step3-dark.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified frontend/src/assets/GraphicalSelectorHelp/step3-light.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
150 changes: 123 additions & 27 deletions frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,18 @@ import { useAppWindowSize } from 'hooks';
import { ZOOM_IN_RATIO, ZOOM_OUT_RATIO } from '../constants';
import {
defaultEdge,
defaultNode,
edgeInHoverStyle,
edgeOpacity,
edgeOutHoverStyle,
edgeUnhoverStyle,
mapNodeOpacity,
mapNodePrereq,
mapNodeRestore,
mapNodeStyle,
nodeLabelHoverStyle,
nodeLabelUnhoverStyle,
nodeStateStyles
nodeStateStyles,
plannedNode
} from './graph';
import S from './styles';

Expand All @@ -33,6 +40,10 @@ type Props = {
focused?: string;
};

interface CoursePrerequisite {
[key: string]: string[];
}

const CourseGraph = ({ onNodeClick, handleToggleFullscreen, fullscreen, focused }: Props) => {
const { theme } = useSelector((state: RootState) => state.settings);
const previousTheme = useRef<typeof theme>(theme);
Expand All @@ -42,26 +53,94 @@ const CourseGraph = ({ onNodeClick, handleToggleFullscreen, fullscreen, focused
const windowSize = useAppWindowSize();

const graphRef = useRef<Graph | null>(null);
const initialising = useRef(false); // prevents multiple graphs being loaded
const [loading, setLoading] = useState(true);
const [unlockedCourses, setUnlockedCourses] = useState(false);
const [prerequisites, setPrerequisites] = useState<CoursePrerequisite>({});

const containerRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
const isCoursePrerequisite = (target: string, neighbour: string) => {
const prereqs = prerequisites[target] || [];
return prereqs.includes(neighbour);
};

const addAdjacentStyles = async (nodeItem: Item) => {
const node = nodeItem as INode;
const neighbours = node.getNeighbors();
const opacity = theme === 'light' ? 0.3 : 0.4;
const { Arrow } = await import('@antv/g6');

// Every other node and edge becomes less visible
graphRef.current?.getNodes().forEach((n) => {
graphRef.current?.updateItem(n as Item, mapNodeOpacity(n.getID(), opacity));
n.getEdges().forEach((e) => {
graphRef.current?.updateItem(e, edgeOpacity(e.getID(), opacity));
});
n.toBack();
});
// Highlight node's edges
node.getOutEdges().forEach((e) => {
graphRef.current?.updateItem(e, edgeOutHoverStyle(Arrow, theme, e.getID()));
graphRef.current?.updateItem(e, edgeOpacity(e.getID(), 1));
e.toFront();
});
node.getInEdges().forEach((e) => {
graphRef.current?.updateItem(e, edgeInHoverStyle(Arrow, theme, e.getID()));
graphRef.current?.updateItem(e, edgeOpacity(e.getID(), 1));
e.toFront();
});
// Target node and neighbouring nodes remain visible
node.toFront();
graphRef.current?.updateItem(node as Item, mapNodeOpacity(node.getID(), 1));
neighbours.forEach((n) => {
graphRef.current?.updateItem(n as Item, mapNodeOpacity(n.getID(), 1));
n.toFront();
const courseId = n.getID();
if (isCoursePrerequisite(node.getID(), courseId)) {
graphRef.current?.updateItem(n as Item, mapNodePrereq(courseId, theme));
}
});
};

const removeAdjacentStyles = async (nodeItem: Item) => {
const node = nodeItem as INode;
const edges = node.getEdges();
const { Arrow } = await import('@antv/g6');

edges.forEach((e) => {
graphRef.current?.updateItem(e, edgeUnhoverStyle(Arrow, theme, e.getID()));
});
graphRef.current?.getNodes().forEach((n) => {
const courseId = n.getID();
graphRef.current?.updateItem(n as Item, mapNodeRestore(courseId, plannedCourses, theme));
graphRef.current?.updateItem(n as Item, mapNodeOpacity(courseId, 1));
n.toFront();
});
graphRef.current?.getEdges().forEach((e) => {
graphRef.current?.updateItem(e, edgeOpacity(e.getID(), 1));
});
};

// On hover: add styles
const addHoverStyles = (ev: IG6GraphEvent) => {
const node = ev.item as Item;
graphRef.current?.setItemState(node, 'hover', true);
graphRef.current?.updateItem(node, nodeLabelHoverStyle(node.getID()));
addAdjacentStyles(node);
graphRef.current?.paint();
};

// On hover: remove styles
const addUnhoverStyles = (ev: IG6GraphEvent) => {
const node = ev.item as Item;
graphRef.current?.clearItemStates(node, 'hover');
graphRef.current?.updateItem(
node,
nodeLabelUnhoverStyle(node.getID(), plannedCourses, theme)
);
removeAdjacentStyles(node);
graphRef.current?.paint();
};

Expand Down Expand Up @@ -94,7 +173,8 @@ const CourseGraph = ({ onNodeClick, handleToggleFullscreen, fullscreen, focused
duration: 500, // Number, the duration of one animation
easing: 'easeQuadInOut' // String, the easing function
},
defaultNode,
groupByTypes: false,
defaultNode: plannedNode,
defaultEdge: defaultEdge(Arrow, theme),
nodeStateStyles
};
Expand Down Expand Up @@ -124,47 +204,63 @@ const CourseGraph = ({ onNodeClick, handleToggleFullscreen, fullscreen, focused
});
};

// Store a hashmap for performance reasons when highlighting nodes
const makePrerequisitesMap = (edges: CourseEdge[]) => {
const prereqs: CoursePrerequisite = prerequisites;
edges.forEach((e) => {
if (!prereqs[e.target]) {
prereqs[e.target] = [e.source];
} else {
prereqs[e.target].push(e.source);
}
});
setPrerequisites(prereqs);
};

// Update styling for: each node, hovering state and edges
const repaintCanvas = async () => {
const nodes = graphRef.current?.getNodes();
nodes?.map((n) =>
graphRef.current?.updateItem(n, mapNodeStyle(n.getID(), plannedCourses, theme))
);

graphRef.current?.off('node:mouseenter');
graphRef.current?.off('node:mouseleave');
Dasyure marked this conversation as resolved.
Show resolved Hide resolved
graphRef.current?.on('node:mouseenter', async (ev) => {
addHoverStyles(ev);
});
graphRef.current?.on('node:mouseleave', async (ev) => {
addUnhoverStyles(ev);
});

const { Arrow } = await import('@antv/g6');
const edges = graphRef.current?.getEdges();
edges?.map((e) => graphRef.current?.updateItem(e, defaultEdge(Arrow, theme)));
graphRef.current?.paint();
};

const setupGraph = async () => {
try {
initialising.current = true;
const res = await axios.get<GraphPayload>(
`/programs/graph/${programCode}/${specs.join('+')}`
);
const { edges, courses } = res.data;
makePrerequisitesMap(edges);
if (courses.length !== 0 && edges.length !== 0) initialiseGraph(courses, edges);
} catch (e) {
// eslint-disable-next-line no-console
console.error('Error at setupGraph', e);
}
};

// Update styling for: each node, hovering state and edges
const repaintCanvas = async () => {
if (graphRef.current) {
const nodes = graphRef.current.getNodes();
nodes.map((n) =>
graphRef.current?.updateItem(n, mapNodeStyle(n.getID(), plannedCourses, theme))
);

graphRef.current.on('node:mouseenter', async (ev) => {
addHoverStyles(ev);
});
graphRef.current.on('node:mouseleave', async (ev) => {
addUnhoverStyles(ev);
});

const { Arrow } = await import('@antv/g6');
const edges = graphRef.current.getEdges();
edges.map((e) => graphRef.current?.updateItem(e, defaultEdge(Arrow, theme)));
graphRef.current.paint();
}
};

if (!graphRef.current) setupGraph();
if (!initialising.current) setupGraph();
// Repaint canvas when theme is changed without re-render
if (previousTheme.current !== theme) {
previousTheme.current = theme;
repaintCanvas();
}
}, [onNodeClick, plannedCourses, programCode, specs, theme]);
}, [onNodeClick, plannedCourses, programCode, specs, theme, prerequisites]);

const showAllCourses = () => {
if (!graphRef.current) return;
Expand Down
Loading