diff --git a/src/commons/sagas/WorkspaceSaga/index.ts b/src/commons/sagas/WorkspaceSaga/index.ts index 2649642402..ab2068ca44 100644 --- a/src/commons/sagas/WorkspaceSaga/index.ts +++ b/src/commons/sagas/WorkspaceSaga/index.ts @@ -407,7 +407,7 @@ const WorkspaceSaga = combineSagaHandlers({ * the function. */ [WorkspaceActions.beginClearContext.type]: function* (action): any { - yield call([DataVisualizer, DataVisualizer.clear]); + yield call([DataVisualizer, DataVisualizer.clearWithData]); yield call([CseMachine, CseMachine.clear]); const globals: Array<[string, any]> = action.payload.library.globals as Array<[string, any]>; for (const [key, value] of globals) { diff --git a/src/commons/sideContent/content/SideContentDataVisualizer.tsx b/src/commons/sideContent/content/SideContentDataVisualizer.tsx index 88e1a4158b..e9f34c6f6a 100644 --- a/src/commons/sideContent/content/SideContentDataVisualizer.tsx +++ b/src/commons/sideContent/content/SideContentDataVisualizer.tsx @@ -1,4 +1,6 @@ -import { Button, Card, Classes } from '@blueprintjs/core'; +import { AnchorButton, Button, Card, Checkbox, Classes } from '@blueprintjs/core'; +import { Tooltip } from '@blueprintjs/core'; +import { Icon } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import { HotkeyItem } from '@mantine/hooks'; import { bindActionCreators } from '@reduxjs/toolkit'; @@ -19,6 +21,7 @@ import { ItalicLink } from './SideContentCseMachine'; type State = { steps: Step[]; currentStep: number; + treeMode: boolean; }; type OwnProps = { @@ -37,7 +40,7 @@ type DispatchProps = { class SideContentDataVisualizerBase extends React.Component { constructor(props: any) { super(props); - this.state = { steps: [], currentStep: 0 }; + this.state = { steps: [], currentStep: 0, treeMode: false }; DataVisualizer.init(steps => { if (this.state.steps.length > 0) { // Blink icon @@ -134,6 +137,98 @@ class SideContentDataVisualizerBase extends React.Component )} + {this.state.steps.length > 0 && ( + <> + + { + if (DataVisualizer.getBinTreeMode()) { + DataVisualizer.toggleBinTreeMode(); + } + if (DataVisualizer.getTreeMode()) { + DataVisualizer.toggleTreeMode(); + } + if (!DataVisualizer.getNormalMode()) { + DataVisualizer.toggleNormalMode(); + } + DataVisualizer.redraw(); + }} + > +
+ + +
+
+
+ + { + if (DataVisualizer.getTreeMode()) { + DataVisualizer.toggleTreeMode(); + } + if (DataVisualizer.getNormalMode()) { + DataVisualizer.toggleNormalMode(); + } + if (DataVisualizer.getBinTreeMode()) { + DataVisualizer.toggleNormalMode(); + } + DataVisualizer.toggleBinTreeMode(); + DataVisualizer.redraw(); + }} + > +
+ + +
+
+
+ + { + if (DataVisualizer.getBinTreeMode()) { + DataVisualizer.toggleBinTreeMode(); + } + if (DataVisualizer.getNormalMode()) { + DataVisualizer.toggleNormalMode(); + } + if (DataVisualizer.getTreeMode()) { + DataVisualizer.toggleNormalMode(); + } + DataVisualizer.toggleTreeMode(); + DataVisualizer.redraw(); + }} + > +
+ + +
+
+
+ + )} ); diff --git a/src/features/dataVisualizer/Config.ts b/src/features/dataVisualizer/Config.ts index 36d708408a..0be3df47a9 100644 --- a/src/features/dataVisualizer/Config.ts +++ b/src/features/dataVisualizer/Config.ts @@ -6,6 +6,7 @@ export const Config = { Stroke: 'white', Fill: 'white', + NWidth: 90, BoxWidth: 45, BoxMinWidth: 15, // Set to half of BoxHeight for empty arrays following CseMachineConfig BoxHeight: 30, diff --git a/src/features/dataVisualizer/Documentation.md b/src/features/dataVisualizer/Documentation.md new file mode 100644 index 0000000000..47d4f410af --- /dev/null +++ b/src/features/dataVisualizer/Documentation.md @@ -0,0 +1,118 @@ +# Data Visualizer Tool +This tool is used by draw_data to render box and pointer diagrams. There are 3 view modes available, the Original mode, the Binary Tree mode and the General Tree mode. + +## Original mode +This is the default view mode which shows only the box and pointer diagrams without any additional spacing, formatting or colour. + +Original Mode + +> *draw_data(list(1, list(2, null, null), list(3, null, null)));* + +## Binary Tree mode +This is the binary tree view mode which shows the binary tree representation of a valid binary tree input, as per the following definition of a binary tree, and using the structure of a 3-tuple input as written in Source Academy's binary_tree module. +- A binary tree of a certain data type is either null, or it is a **list** with **3** elements: the first being an element of that data type, and the remaining being trees of that data type.
+- However, for the purpose of allowing a general Data Visualizer tool, restricting to a single data type has not been strictly enforced. +- Structure of a 3-tuple input: (**value:** any, **left:** BinaryTree, **right:** BinaryTree) + +Each node in the tree comprises of group of 3 boxes: +- A box containing the node's value +- A box from which the left subtree originates +- A box from which the right subtree originates + +These 3 boxes are closely arranged in a triangular node group. The box containing the value is at the top of the node group, with the boxes pointing to the left and right subtrees at its bottom left and right, respectively. + +Binary Tree Mode + +> *draw_data(list(1, list(2, null, null), list(3, null, null)));* + +For example, consider the above data visualisation.
+The tree has a **root node** with a value of 1, and it also has a left subtree and a right subtree. The **left subtree** has a parent node with value 2, while the **right subtree** has a parent node with value 3. + +## General Tree mode +This is the general tree view mode which shows the tree representation (left aligned) of a valid tree input, as per the following definition of a tree: +- A tree of a certain data type is either null, or it is a **list** whose elements are of that data type, or trees of that data type. +- However, for the purpose of allowing a general Data Visualizer tool, restricting to a single data type has not been strictly enforced. + +General Tree Mode + +> *draw_data(list(1, list(2, null, null), list(3, null, null)));* + +## Spacing +### `nodePos`, `depth` (in `dataVisualizer.tsx`) +The input data is initially iterated through once to get the nodePos and the maximum depth of the tree. `nodePos` represents the position of the box within the node, and will be stored as a field in `BaseTreeNode.ts`. + +The depth is calculated through traversing the input array. Whenever the first element of the array is another nested array, the recursion increases the depth by 1. When `get_depth` reaches the end of the recursion, the final depth of that branch is compared to the maximum depth of the tree and the maximum depth is updated accordingly. + +nodeCount is an array that is used to keep track the largest node (ie. the node with the most number of boxes) for each level. Currently, it is used for spacing purposes to ensure that there the spacing between nodes account for the worst case scenario whereby all nodes have the size of the largest node. This field may be changed in the future as we explore more space efficient ways to space out the nodes. + +### `scalerV` (in `Tree.tsx`) +For the Binary Tree mode, in order to make the tree appear compact, the horizontal spacing between distinct node groups should be inversely proportional to level of these node groups, i.e. the larger / deeper the level in the tree, the closer the node groups. + +This is done through a `scalerV`, applied to the boxes when they are being rendered (example below). +> if (index == 0 && y == parentY + Config.DistanceY) {
+myY = y + Config.DistanceY * 2;
+myX = x - Config.NWidth * **scalerV**;
+TreeDrawer.colorCounter++;
+colorIndex=TreeDrawer.colorCounter;
+} + +Since scalerV should be inversely proportional to the level of the node groups, the calculation for scalerV is equivalent to: +- 2depth of tree divided by 2current level + +This way, as the current level increases (going down the tree), the resultant scalerV decreases. The current level can be determined by dividing the y value of the box to be rendered by 6 * Config.BoxHeight, which is the amount of height used by each node group + vertical spacing between levels.
+Powers of 2 are used to appropriately space the binary tree, given that each node group can have 2 subtrees. + +Equation for scalerV: +> scalerV = Math.round(Math.pow(2, DataVisualizer.binaryTreeDepth) / Math.pow(2, (Math.round(y / (6 * Config.BoxHeight))))); + +### `leftCOUNTER`, `rightCOUNTER`, `downCOUNTER` (in `Tree.tsx`) +For the Binary Tree mode, it is necessary to identify how far the tree stretches left / right away from the centre (the root node), in order to generate sufficient space to show the tree in the visualizer itself. + +As the tree is being rendered box by box, the field `leftCounter` is incremented whenever a new node group is created towards the left of the root node, and is further left than any previous node. Similarly, the field `rightCounter` is incremented whenever a new node group is created towards the right of the root node, and is further right than any previous node. Lastly, the field `downCounter` is incremented whenever a new node group is created below the root node, and is further down than any previous node. + +These 3 fields are used in the subsequent calculations of the variables EY1, EY2, EY3 and EY4, used in the generation of space in the visualizer for the Binary Tree mode and the General Tree mode. + +### `EY1`, `EY2`, `EY3`, `EY4` (in `dataVisualizer.tsx`, `Tree.tsx`) +There are 2 steps to generating space in the visualizer. +1. Creating the entire visual canvas (the dark blue backdrop) +2. Setting the offset from the top left of the visual canvas, from which the data will begin drawing from + +The visual canvas is created through `createDrawing()` in `dataVisualizer.tsx`, while the offset is set through `draw()` in `Tree.tsx`. + +Purpose of the EY Variables: +- `EY1`: Get the maximum of the fields `leftCounter` and `rightCounter`. +- `EY2`: Used to set the horizontal width for Binary Tree mode.
+Due to `scalerV`, as one goes lower down the tree, the horizontal spacing between the distinct node groups decreases, allowing the tree to appear compact. This decreasing space is equivalent to decreasing powers of 2 * `Config.NWidth` as explained in the section for `scalerV`.
+Thus, to calculate how much offset is required before generating the tree, it is equivalent to: 21 + 22 + 23 + ... + 2EY1-1. This is a sum of a finite geometric progression with first term 2, common ratio 2, and (EY1-1) terms. Hence, using the formula for the sum of a finite geometric progression, we get the following equation for EY2: +> *EY2 = 2 * (Math.pow(2, EY1 - 1) - 1) + 1;* +- `EY3`: Get the field `downCounter`. Used by `createDrawing()` and `draw()` to set the vertical height. +- `EY4`: Used to set the horizontal width for General Tree mode.
+For General Tree mode, as the tree is left-aligned, the only consideration required is the possible maximum width of the entire tree. Using the largest `nodeCount` ("L"), we can proactively generate a visual canvas that is sufficiently wide to accomodate any possible arrangement of branches throughout the tree.
+This largest width is equivalent to the nth term of a finite geometric progression with first term (L+1), common ratio L, and n terms (where n = the depth of the tree). Hence, using the formula for the nth term of a finate geometric progression, we get the following equation for EY4: +> *L = DataVisualizer.nodeCount[0];*
+> *EY4 = (Config.NWidth + Config.BoxWidth) * (L + 1) * Math.pow(L, DataVisualizer.binaryTreeDepth) - Config.BoxWidth;* + +## Coloring +All boxes belonging to the same node would be the same color. The coloring mechanism uses two key variables: `TreeDrawer.colorCounter` and `colorIndex`. + +- `TreeDrawer.colorCounter` is a static counter that starts at 0 for each new tree drawing (reset in `Tree.draw()`). It increments each time a new node is encountered during the recursive drawing process, ensuring each unique node in the tree gets a distinct color index. + +- `colorIndex` is a parameter passed to the `createDrawable` method of each `ArrayTreeNode`. It determines the actual color by indexing into a predefined array of colors: `this.Colors[colorIndex % this.Colors.length]`, where `this.Colors` is an array of 9 colors defined in `ArrayTreeNode.tsx`. + +In binary tree mode: +- When drawing a new branch (left or right child), `colorCounter` increments, assigning a new `colorIndex` to the child node. +- Boxes within the same node (e.g., the three boxes representing a binary tree node) share the same `colorIndex`, thus the same color, hence `colorIndex` is set to `parentIndex`. + +In general tree mode, similar logic applies, with `colorCounter` incrementing for each new child subtree. + +In original mode, `colorIndex` is set to 0, resulting in all boxes being black. + +## Tree checking +### Binary Tree mode +The input data would be checked to ensure that it is a binary tree using `isBinaryTree()`. This is done by recursively checking if every node is made up of 3 boxes. If the given input is not a binary tree and the binary tree mode is selected, an error would be shown. + +### General Tree mode +The input array would be iterated through to ensure that the length of nested arrays, checking if their size exceed 2. This is because trees are list, and lists are stored as pairs, hence the size of the input array and nested arrays should be less than 2. + +## `dataRecords` +Keeps a copy of all inputs to ensure that when another mode is chosen, all the instances of draw_data is redrawn. diff --git a/src/features/dataVisualizer/dataVisualizer.tsx b/src/features/dataVisualizer/dataVisualizer.tsx index f32c22a445..4b3ced0559 100644 --- a/src/features/dataVisualizer/dataVisualizer.tsx +++ b/src/features/dataVisualizer/dataVisualizer.tsx @@ -15,9 +15,20 @@ import { DataTreeNode } from './tree/TreeNode'; * clear is used by WorkspaceSaga to reset the visualizer after every "Run" button press */ export default class DataVisualizer { + private static counter = 1; private static empty(step: Step[]) {} private static setSteps: (step: Step[]) => void = DataVisualizer.empty; + public static dataRecords: Data[] = []; + public static isRedraw = false; private static _instance = new DataVisualizer(); + public static treeMode = false; + public static BinTreeMode = false; + public static normalMode = true; + public static TreeDepth = 0; + public static isBinTree = false; + public static isGenTree = false; + public static nodeCount: number[] = []; + public static longestNodePos: number = 0; private steps: Step[] = []; private nodeLabel = 0; @@ -25,20 +36,111 @@ export default class DataVisualizer { private constructor() {} + public static get_depth(structures: Data[], depth: number, nodePos: number): number { + if (!(structures instanceof Array)) { + return 0; + } + //nodeCount keeps track of the current index of nodes at each depth + if (this.getTreeMode()) { + if (this.nodeCount[depth] === undefined) { + this.nodeCount[depth] = 0; + } + structures.push(this.nodeCount[depth]); + if (this.nodeCount[depth] > this.longestNodePos) { + this.longestNodePos = this.nodeCount[depth]; + } + this.nodeCount[depth]++; + } + + this.TreeDepth = Math.max(this.TreeDepth, depth); + this.get_depth(structures[0], depth + 1, 0); + this.get_depth(structures[1], depth, nodePos + 1); + return depth; + } + + public static isBinaryTree(structures: Data[]): boolean { + if (structures[0] === null) { + return true; + } + let next = structures[0]; + let ans = false; + let count = 0; + while (next instanceof Array) { + count++; + next = next[1]; + } + if (count == 3) { + ans = true; + } + return ans && this.isBinaryTree(structures[0][1]); + } + + public static isGeneralTree(structures: Data[]): boolean { + if (structures == null) { + return true; + } + if (structures.length > 2 || (!(structures[1] instanceof Array) && structures[1] != null)) { + return false; + } + return this.isGeneralTree(structures[1]) && this.isGeneralTree(structures[0]); + } + public static init(setSteps: (step: Step[]) => void): void { DataVisualizer.setSteps = setSteps; } + // RenderBinaryTree + public static toggleBinTreeMode(): void { + DataVisualizer.BinTreeMode = !DataVisualizer.BinTreeMode; + } + + // RenderGeneralTree + public static toggleTreeMode(): void { + DataVisualizer.treeMode = !DataVisualizer.treeMode; + } + + // OriginalView + public static toggleNormalMode(): void { + DataVisualizer.normalMode = !DataVisualizer.normalMode; + } + + public static getBinTreeMode(): boolean { + return DataVisualizer.BinTreeMode; + } + + public static getTreeMode(): boolean { + return DataVisualizer.treeMode; + } + + public static getNormalMode(): boolean { + return DataVisualizer.normalMode; + } public static drawData(structures: Data[]): void { if (!DataVisualizer.setSteps) { throw new Error('Data visualizer not initialized'); } + if (!DataVisualizer.isRedraw) { + this.dataRecords.push(structures); + } + DataVisualizer.isBinTree = this.isBinaryTree(structures); + DataVisualizer.isGenTree = this.isGeneralTree(structures); + this.get_depth(structures[0], 0, 0); + DataVisualizer._instance.addStep(structures); DataVisualizer.setSteps(DataVisualizer._instance.steps); } + public static clearWithData(): void { + DataVisualizer.longestNodePos = 0; + DataVisualizer.dataRecords = []; + DataVisualizer.isRedraw = false; + DataVisualizer.clear(); + } + public static clear(): void { DataVisualizer._instance = new DataVisualizer(); + this.nodeCount = []; + this.TreeDepth = 0; DataVisualizer.setSteps(DataVisualizer._instance.steps); } @@ -50,14 +152,14 @@ export default class DataVisualizer { if (this.nodeToLabelMap.has(dataNode)) { return this.nodeToLabelMap.get(dataNode) ?? 0; } else { - console.log('*' + this.nodeLabel + ': ' + dataNode.data); + // console.log('*' + this.nodeLabel + ': ' + dataNode.data); this.nodeToLabelMap.set(dataNode, this.nodeLabel); return this.nodeLabel++; } } private addStep(structures: Data[]) { - const step = structures.map(this.createDrawing); + const step = structures.map((xs, index) => this.createDrawing(xs, index)); this.steps.push(step); } @@ -65,21 +167,95 @@ export default class DataVisualizer { * For student use. Draws a structure by converting it into a tree object, attempts to draw on the canvas, * Then shift it to the left end. */ - private createDrawing(xs: Data): JSX.Element { + private createDrawing(xs: Data, key: number): JSX.Element { const treeDrawer = Tree.fromSourceStructure(xs).draw(); // To account for overflow to the left side due to a backward arrow - // const leftMargin = Config.ArrowMarginHorizontal + Config.StrokeWidth; const leftMargin = Config.StrokeWidth / 2; // To account for overflow to the top due to a backward arrow const topMargin = Config.StrokeWidth / 2; const layer = treeDrawer.draw(leftMargin, topMargin); - return ( - - {layer} - - ); + + //for normal mode + if (DataVisualizer.normalMode) { + return ( + + {layer} + + ); + } + // NON-BINARY TREE WARNING + if (!DataVisualizer.isBinTree && DataVisualizer.BinTreeMode) { + return ( + + {layer} + + ); + } + if (!DataVisualizer.isGenTree && DataVisualizer.treeMode) { + return ( + + {layer} + + ); + } + if (DataVisualizer.getBinTreeMode()) { + // RenderBinaryTree + const EY1 = Math.max(treeDrawer.leftCOUNTER, treeDrawer.rightCOUNTER); + const EY2 = 2 * (Math.pow(2, EY1 - 1) - 1) + 1; // how many nodegroups stretch left or right (not including root) + const EY3 = treeDrawer.downCOUNTER - 1; // how many node groups stretch down + return ( + + {layer} + + ); + } else if (DataVisualizer.getTreeMode()) { + // RenderGeneralTree + // const L = DataVisualizer.nodeCount[0]; + const EY4 = + (Config.NWidth + Config.BoxWidth) * (DataVisualizer.longestNodePos + 1) - Config.BoxWidth; + const EY3 = treeDrawer.downCOUNTER; + return ( + + {layer} + + ); + } else { + // OriginalView + return ( + + {layer} + + ); + } + } + static redraw() { + this.isRedraw = true; + this.clear(); + DataVisualizer.counter = -DataVisualizer.counter; + return DataVisualizer.dataRecords.map(structures => this.drawData(structures)); } } diff --git a/src/features/dataVisualizer/drawable/ArrayDrawable.tsx b/src/features/dataVisualizer/drawable/ArrayDrawable.tsx index 8f37d7d716..6847b9c638 100644 --- a/src/features/dataVisualizer/drawable/ArrayDrawable.tsx +++ b/src/features/dataVisualizer/drawable/ArrayDrawable.tsx @@ -11,6 +11,7 @@ type ArrayProps = { nodes: TreeNode[]; x: number; y: number; + color: string; }; /** @@ -55,7 +56,9 @@ class ArrayDrawable extends React.PureComponent { height={Config.BoxHeight} strokeWidth={Config.StrokeWidth} stroke={Config.Stroke} - fill="#17181A" + fill={this.props.color} + //"#d81d1d" + preventDefault={false} /> {/* Vertical lines in the box */} diff --git a/src/features/dataVisualizer/drawable/ArrowDrawable.tsx b/src/features/dataVisualizer/drawable/ArrowDrawable.tsx index 1e3698c544..94a28c76bd 100644 --- a/src/features/dataVisualizer/drawable/ArrowDrawable.tsx +++ b/src/features/dataVisualizer/drawable/ArrowDrawable.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Arrow } from 'react-konva'; import { Config } from '../Config'; +import DataVisualizer from '../dataVisualizer'; type Props = { from: { x: number; y: number }; @@ -14,23 +15,54 @@ type Props = { * Used with ArrayDrawable and FunctionDrawable. */ const ArrowDrawable: React.FC = props => { - return ( - - ); + if (DataVisualizer.getBinTreeMode()) { + // RenderBinaryTree + return ( + + ); + } else if (DataVisualizer.getTreeMode()) { + // RenderGeneralTree + return ( + + ); + } else { + // OriginalView + return ( + + ); + } }; export default React.memo(ArrowDrawable); diff --git a/src/features/dataVisualizer/images/BINARY_TREE_IMAGE.png b/src/features/dataVisualizer/images/BINARY_TREE_IMAGE.png new file mode 100644 index 0000000000..7c185d8095 Binary files /dev/null and b/src/features/dataVisualizer/images/BINARY_TREE_IMAGE.png differ diff --git a/src/features/dataVisualizer/images/GENERAL_TREE_IMAGE.png b/src/features/dataVisualizer/images/GENERAL_TREE_IMAGE.png new file mode 100644 index 0000000000..dd2e6bdf27 Binary files /dev/null and b/src/features/dataVisualizer/images/GENERAL_TREE_IMAGE.png differ diff --git a/src/features/dataVisualizer/images/ORIGINAL_VIEW_IMAGE.png b/src/features/dataVisualizer/images/ORIGINAL_VIEW_IMAGE.png new file mode 100644 index 0000000000..ebf96a7f11 Binary files /dev/null and b/src/features/dataVisualizer/images/ORIGINAL_VIEW_IMAGE.png differ diff --git a/src/features/dataVisualizer/tree/ArrayTreeNode.tsx b/src/features/dataVisualizer/tree/ArrayTreeNode.tsx index 44761375c1..8341cb53df 100644 --- a/src/features/dataVisualizer/tree/ArrayTreeNode.tsx +++ b/src/features/dataVisualizer/tree/ArrayTreeNode.tsx @@ -9,8 +9,33 @@ import { DrawableTreeNode } from './DrawableTreeNode'; * Represents a node corresponding to a Source pair or array. */ export class ArrayTreeNode extends DrawableTreeNode { - createDrawable(x: number, y: number, parentX: number, parentY: number): JSX.Element { - const arrayProps = { nodes: this.children ?? [], x, y }; + Colors: string[] = [ + 'black', + '#d81d1d', + '#e46510', + '#259530', + '#27d6e6', + '#0d54ed', + '#5a10d1', + '#e6148f', + '#870854' + ]; + createDrawable( + x: number, + y: number, + parentX: number, + parentY: number, + colorIndex: number + ): JSX.Element { + let color = ''; + // if (!partOfNode){ + // ArrayTreeNode.i++; + // } + // if (ArrayTreeNode.i>5){ + // ArrayTreeNode.i=0; + // } + color = this.Colors[colorIndex % this.Colors.length]; + const arrayProps = { nodes: this.children ?? [], x, y, color }; const arrayDrawable = ; this._drawable = ( diff --git a/src/features/dataVisualizer/tree/BaseTreeNode.ts b/src/features/dataVisualizer/tree/BaseTreeNode.ts index 939610c3a8..9c9c60f22a 100644 --- a/src/features/dataVisualizer/tree/BaseTreeNode.ts +++ b/src/features/dataVisualizer/tree/BaseTreeNode.ts @@ -1,7 +1,10 @@ export class TreeNode { public children: TreeNode[] | null; + public nodePos: number = 0; + public data: any; constructor() { this.children = null; + this.nodePos = 0; } } diff --git a/src/features/dataVisualizer/tree/DrawableTreeNode.tsx b/src/features/dataVisualizer/tree/DrawableTreeNode.tsx index 61ed91fe8f..45fe765f8f 100644 --- a/src/features/dataVisualizer/tree/DrawableTreeNode.tsx +++ b/src/features/dataVisualizer/tree/DrawableTreeNode.tsx @@ -25,5 +25,11 @@ export abstract class DrawableTreeNode extends TreeNode { * @param parentX The x position of the parent. * @param parentY The y position of the parent. */ - abstract createDrawable(x: number, y: number, parentX: number, parentY: number): JSX.Element; + abstract createDrawable( + x: number, + y: number, + parentX: number, + parentY: number, + colorIndex: number + ): JSX.Element; } diff --git a/src/features/dataVisualizer/tree/FunctionTreeNode.tsx b/src/features/dataVisualizer/tree/FunctionTreeNode.tsx index 6b2a1a1ad4..1d5664eba9 100644 --- a/src/features/dataVisualizer/tree/FunctionTreeNode.tsx +++ b/src/features/dataVisualizer/tree/FunctionTreeNode.tsx @@ -9,7 +9,13 @@ import { DrawableTreeNode } from './DrawableTreeNode'; * Represents a node corresponding to a Source (and Javascript) function. */ export class FunctionTreeNode extends DrawableTreeNode { - createDrawable(x: number, y: number, parentX: number, parentY: number): JSX.Element { + createDrawable( + x: number, + y: number, + parentX: number, + parentY: number, + colorIndex: number + ): JSX.Element { this._drawable = ( diff --git a/src/features/dataVisualizer/tree/Tree.tsx b/src/features/dataVisualizer/tree/Tree.tsx index 0457d327f1..6e99262375 100644 --- a/src/features/dataVisualizer/tree/Tree.tsx +++ b/src/features/dataVisualizer/tree/Tree.tsx @@ -3,6 +3,7 @@ import type { JSX } from 'react'; import { Layer, Text } from 'react-konva'; import { Config } from '../Config'; +import DataVisualizer from '../dataVisualizer'; import { Data, Pair } from '../dataVisualizerTypes'; import { isArray, isFunction, toText } from '../dataVisualizerUtils'; import { ArrowDrawable, BackwardArrowDrawable } from '../drawable/Drawable'; @@ -50,7 +51,7 @@ export class Tree { static fromSourceStructure(tree: Data): Tree { let nodeCount = 0; - + const genTreeChecker = DataVisualizer.getTreeMode(); function constructNode(structure: Data): TreeNode { const alreadyDrawnNode = visitedStructures.get(structure); if (alreadyDrawnNode !== undefined) { @@ -71,12 +72,15 @@ export class Tree { */ function constructTree(tree: Array) { const node = new ArrayTreeNode(); - visitedStructures.set(tree, node); treeNodes[nodeCount] = node; nodeCount++; - // Done like that instead of in constructor to prevent infinite recursion + if (genTreeChecker) { + node.nodePos = tree[tree.length - 1]; + tree.pop(); + } + node.children = tree.map(constructNode); return node; @@ -116,6 +120,7 @@ export class Tree { } draw(): TreeDrawer { + TreeDrawer.colorCounter = 0; return new TreeDrawer(this); } } @@ -125,6 +130,12 @@ export class Tree { */ class TreeDrawer { private tree: Tree; + public leftCOUNTER: integer = 0; + public rightCOUNTER: integer = 0; + public downCOUNTER: integer = 0; + private runningX: integer = 0; + private runningY: integer = 0; + private runningX2: integer = 0; // for rightCOUNTER private drawables: JSX.Element[]; private nodeWidths: Map; @@ -134,6 +145,9 @@ class TreeDrawer { // Used to account for backward arrow private minX = 0; private minY = 0; + public static colorCounter = 0; + + private leftMargin: integer = Config.StrokeWidth / 2; constructor(tree: Tree) { this.tree = tree; @@ -162,15 +176,77 @@ class TreeDrawer { ); - } else { - this.drawNode(this.tree.rootNode, x, y, x, y); - this.width = this.getNodeWidth(this.tree.rootNode) - this.minX; - this.height = this.getNodeHeight(this.tree.rootNode) - this.minY + Config.StrokeWidth; + } + // NON-BINARY TREE WARNING + if (!DataVisualizer.isBinTree && DataVisualizer.getBinTreeMode()) { + return ( + + + + ); + } + // NON-GENERAL TREE WARNING + else if (!DataVisualizer.isGenTree && DataVisualizer.getTreeMode()) { + console.log('Not general tree'); return ( - - {this.drawables} + + ); + } else { + if (DataVisualizer.getBinTreeMode()) { + // RenderBinaryTree + this.drawNode(this.tree.rootNode, x, y, x, y, 0, 0, 0, 0); + this.width = this.getNodeWidth(this.tree.rootNode) - this.minX; + this.height = this.getNodeHeight(this.tree.rootNode) - this.minY + Config.StrokeWidth; + + const EY1 = Math.max(this.leftCOUNTER, this.rightCOUNTER); + let EY2; + if (EY1 === 0) { + EY2 = EY1; + } else { + EY2 = 2 * (Math.pow(2, EY1 - 1) - 1) + 1; // how many nodegroups stretch left or right (not including root) + } + return ( + + {this.drawables} + + ); + } else if (DataVisualizer.getTreeMode()) { + // RenderGeneralTree + this.drawNode(this.tree.rootNode, x, y, x, y, 0, 0, 0, 0); + this.width = this.getNodeWidth(this.tree.rootNode) - this.minX; + this.height = this.getNodeHeight(this.tree.rootNode) - this.minY + Config.StrokeWidth; + + return ( + + {this.drawables} + + ); + } else { + // OriginalView + this.drawNode(this.tree.rootNode, x, y, x, y, 0, 0, 0, 0); + this.width = this.getNodeWidth(this.tree.rootNode) - this.minX; + this.height = this.getNodeHeight(this.tree.rootNode) - this.minY + Config.StrokeWidth; + + return ( + + {this.drawables} + + ); + } } } @@ -186,7 +262,17 @@ class TreeDrawer { * @param parentX The x position of the parent. If there is no parent, it is the same as x. * @param parentY The y position of the parent. If there is no parent, it is the same as y. */ - drawNode(node: TreeNode, x: number, y: number, parentX: number, parentY: number) { + drawNode( + node: TreeNode, + x: number, + y: number, + parentX: number, + parentY: number, + colorIndex: number, + parentIndex: number, + originIndex: number, + originX: number + ) { if (node instanceof AlreadyParsedTreeNode) { // if its child is part of a cycle and it's been drawn, link back to that node instead const drawnNode = node.actualNode; @@ -228,21 +314,135 @@ class TreeDrawer { // draws the content if (node instanceof FunctionTreeNode) { - const drawable = node.createDrawable(x, y, parentX, parentY); + const drawable = node.createDrawable(x, y, parentX, parentY, 0); this.drawables.push(drawable); } else if (node instanceof ArrayTreeNode) { - const drawable = node.createDrawable(x, y, parentX, parentY); - this.drawables.push(drawable); - - // if it has children, draw them - // const width = this.getNodeWidth(node); - let leftX = x; - node.children?.forEach((childNode, index) => { - const childY = childNode instanceof AlreadyParsedTreeNode ? y : y + Config.DistanceY; - this.drawNode(childNode, leftX, childY, x + Config.BoxWidth * index, y); - const childNodeWidth = this.getNodeWidth(childNode); - leftX += childNodeWidth ? childNodeWidth + Config.DistanceX : 0; - }); + if (DataVisualizer.getBinTreeMode()) { + // RenderBinaryTree + const drawable = node.createDrawable(x, y, parentX, parentY, colorIndex); + this.drawables.push(drawable); + + node.children?.forEach((childNode, index) => { + let myY; + let myX; + let scalerV = Math.round( + Math.pow(2, DataVisualizer.TreeDepth) / + Math.pow(2, Math.round(y / (6 * Config.BoxHeight))) + ); + scalerV--; + + if (index === 0 && y === parentY + Config.DistanceY) { + // NEW left branch + myY = y + Config.DistanceY * 2; + myX = x - Config.NWidth * scalerV; + TreeDrawer.colorCounter++; + colorIndex = TreeDrawer.colorCounter; + } else if (index === 0) { + // NEW right branch + myY = y + Config.DistanceY * 2; + myX = x + Config.NWidth * scalerV; + colorIndex = TreeDrawer.colorCounter; + } else if (y === parentY + Config.DistanceY) { + // third box + myY = y; + myX = x + Config.NWidth * 2; + colorIndex = parentIndex; + } else { + // second box + myY = y + Config.DistanceY; + myX = x - Config.NWidth; + colorIndex = parentIndex; + } + + if (x < this.runningX && index === 0 && y === parentY + Config.DistanceY) { + // NEW left branches that stretch towards the left + this.leftCOUNTER++; + this.runningX = myX; + } else if (x > this.runningX2 && index === 0 && y === parentY + Config.DistanceY) { + // NEW right branches that stretch towards the right + this.rightCOUNTER++; + this.runningX2 = myX; + } + + if (y > this.runningY && index === 0) { + // NEW branches (doesn't matter left or right) that stretches down + this.downCOUNTER++; + this.runningY = myY; + } + + this.drawNode( + childNode, + myX, + myY, + x + Config.BoxWidth * index, + y, + colorIndex, + colorIndex, + 0, + 0 + ); + }); + } else if (DataVisualizer.getTreeMode()) { + // RenderGeneralTree + const drawable = node.createDrawable(x, y, parentX, parentY, colorIndex); + this.drawables.push(drawable); + + const longest = DataVisualizer.nodeCount[0]; // e.g. 3 + this.runningX2 = (Config.NWidth + Config.BoxWidth) * (longest + 1); + this.downCOUNTER = DataVisualizer.TreeDepth; + + node.children?.forEach((childNode, index) => { + let myY; + let myX; + + if (index == 0) { + myY = y + Config.DistanceY * 2; + myX = originX; + TreeDrawer.colorCounter++; + colorIndex = TreeDrawer.colorCounter; + } else { + myY = y; + myX = x + Config.NWidth + Config.BoxWidth; + colorIndex = parentIndex; + } + + if (x > this.runningX2 && index == 0 && y == parentY + Config.DistanceY * 2) { + // NEW right branches that stretch towards the right + this.rightCOUNTER++; + this.runningX2 = myX; + } + + if (node.children![1] instanceof ArrayTreeNode) { + if (node.children![1].children![0] instanceof ArrayTreeNode) { + originIndex = node.children![1].children![0].nodePos; + originX = 0 + this.leftMargin + (Config.NWidth + Config.BoxWidth) * originIndex; + } + } + + this.drawNode( + childNode, + myX, + myY, + x + Config.BoxWidth * index, + y, + colorIndex, + colorIndex, + originIndex, + originX + ); + }); + } else { + // OriginalView + const drawable = node.createDrawable(x, y, parentX, parentY, 0); + this.drawables.push(drawable); + let leftX = x; + node.children?.forEach((childNode, index) => { + const childY = childNode instanceof AlreadyParsedTreeNode ? y : y + Config.DistanceY; + this.drawNode(childNode, leftX, childY, x + Config.BoxWidth * index, y, 0, 0, 0, 0); + const childNodeWidth = this.getNodeWidth(childNode); + leftX += childNodeWidth ? childNodeWidth + Config.DistanceX : 0; + }); + } } }