Skip to content

Commit

Permalink
feat: CF-423: additional styling on course node hover (#1034)
Browse files Browse the repository at this point in the history
* feat: dark mode functionality added for editMarkModal's input and button elements

* feat: dark mode improvement for editMarkModal's cancel button

* feat: dark mode improvements for OptionHeader icons underneath the 'Term Planner' tab

* feat: dark mode added for the select menu in the settingsMenu tooltip under the TermPlanner tab

* feat: dark mode added to SettingMenu's DatePicker element

* feat: dark mode for export button done + editModalMark bug fixed

* fix: making sure the css for the select element in settingsMenu does not affect other select elements

* feat: dark mode improvement for popconfirm for unplan wanring, import tooltip, also moved the button for import and export tooltip to common styles

* feat: dark mode scrollbar added

* feat: dark mode scrollbar on courseSelector menu had ugly white padding, fixed it to be #333 color instead

* fix: href in courseSelector not very readable in dark mode, made the text a bit brighter and made a styled component for it

* feat: dark mode added for search bar

* feat: dark mode added for remove planner button

* fix: forgot to add the new styles.ts file

* feat: progressBar's text color and trailing color fixed

* feat: dividing line in courseDescription changed from white to a dark grey

* feat: bug icon turned into dark mode

* feat: dark mode added for quick add and remove buttons in course menu

* feat: courseProgression progress bar trailing color changed to dark grey

* feat: progress on dark mode for graph, need to save this commit before I merge in the latest changes since the graph was changed

* feat: dark mode for graph complete (nodes, arrows, hover states) + label now changes on hover (non-dark mode feature)

* feat: buttons on graphical selector are dark mode

* feat: saving progress on converting courseDescription panel to dark mode

* feat: dark mode added to the sidebar

* feat: sidebardrawer color changed, box shadow added to tabs so it looks more visible in dark mode

* feat: new images added in help menu in course selector, dark mode versions added too

* feat: TermPlanner's help menu tooltips now have dark mode pics and gifs

* feat: highlight adjacent nodes and edges on hover

* feat: highlight adjacent nodes opacity updated

* refactor: graph.ts, changing function names and object names to be more readable:

* feat: implemented a function that checks if a course is a prereq based on GraphData without calling the backend

* fix: two graphs get rendered if you switch tabs fast enough

* feat: created a function to store a hashmap of prereqs for later use for node styling

* fix: updated the function that checks for coursePrerequisite

Other options are:
1. Rely on GraphData, however it gets laggy when you hover over too many nodes
2. API call - would get slow with multiple requests
3. CourseEdge info gets stored into a hashmap at initialisation, so checking for prereqs is fast afterwards

* refactor: graph.ts function and object names made more readable

* refactor: rewriting the returns and using spread operator to reduce repetition in graph.ts

* feat: highlight prerequisite nodes on hover

* refactor: splitting functions up as they were getting too long

* fix: if the dark mode button is toggled on and off, it repaints the canvas more than one time

* feat: highlighted incoming edge if it's a prerequisite as well

* feat: forgot to add pics into the HelpMenu for the new graphical selector hover node feature

---------

Co-authored-by: Daysure <[email protected]>
Co-authored-by: Leonardo Fan <[email protected]>
  • Loading branch information
3 people authored Apr 12, 2023
1 parent 1b129d5 commit 857d612
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 67 deletions.
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');
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

0 comments on commit 857d612

Please sign in to comment.