Skip to content

Commit

Permalink
Convert data visualizer to Typescript (#1685)
Browse files Browse the repository at this point in the history
* Begin moving ListVisualizer to jsx

* Convert list visualizer to tsx

* Added types

* clear list viz done

* Finish migration of list visualizer to TS

* Add documentation to list visualizer ts

* Fix tsx formatting

* Fix list visualizer issues

- Fix visualizer not caching long data asterisk ids
- Fix visualizer clear not working

* Fix draw-data issues

* Fix formatting

* Formatting fixes

* Changed setStep type

* Fixed setsteps function

* setSteps fixed

* Initialised setSteps

* Fix PR concerns

* format

Co-authored-by: VimuthM <vimuthmendis@gmail.com>
Co-authored-by: martin-henz <henz@comp.nus.edu.sg>
Co-authored-by: Vimuth <62249192+VimuthM@users.noreply.github.com>
  • Loading branch information
4 people committed Apr 12, 2021
1 parent 352a6a0 commit 5bfa5f2
Show file tree
Hide file tree
Showing 22 changed files with 1,527 additions and 84 deletions.
3 changes: 0 additions & 3 deletions public/externalLibs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@ function loadAllLibs() {
'/externalLibs/graphics/webGLgraphics.js',
'/externalLibs/graphics/webGLcurve.js',
'/externalLibs/graphics/webGLrune.js',
// list visualizer
'/externalLibs/visualizer/konva.js',
'/externalLibs/visualizer/visualizer.js',
// binary tree library
'/externalLibs/tree.js',
// support for Practical Assessments (presently none)
Expand Down
12 changes: 0 additions & 12 deletions public/externalLibs/visualizer/konva.js

This file was deleted.

3 changes: 2 additions & 1 deletion src/commons/sagas/WorkspaceSaga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { SagaIterator } from 'redux-saga';
import { call, delay, put, race, select, take } from 'redux-saga/effects';
import * as Sourceror from 'sourceror';

import ListVisualizer from '../../features/listVisualizer/ListVisualizer';
import { PlaygroundState } from '../../features/playground/PlaygroundTypes';
import { DeviceSession } from '../../features/remoteExecution/RemoteExecutionTypes';
import { OverallState, styliseSublanguage } from '../application/ApplicationTypes';
Expand Down Expand Up @@ -504,7 +505,7 @@ export default function* WorkspaceSaga(): SagaIterator {
break;
}
}
(window as any).ListVisualizer?.clear();
ListVisualizer.clear();
const globals: Array<[string, any]> = action.payload.library.globals as Array<[string, any]>;
for (const [key, value] of globals) {
window[key] = value;
Expand Down
123 changes: 61 additions & 62 deletions src/commons/sideContent/SideContentListVisualizer.tsx
Original file line number Diff line number Diff line change
@@ -1,131 +1,130 @@
import { Button, Classes, NonIdealState, Spinner } from '@blueprintjs/core';
import { Button, Classes } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames';
import * as React from 'react';
import { HotKeys } from 'react-hotkeys';

import ListVisualizer from '../../features/listVisualizer/ListVisualizer';
import { Step } from '../../features/listVisualizer/ListVisualizerTypes';
import { Links } from '../utils/Constants';

type State = {
loading: boolean;
steps: Step[];
currentStep: number;
};

const listVisualizerKeyMap = {
PREVIOUS_STEP: 'left',
NEXT_STEP: 'right'
};

/**
* This class is responsible for the visualization of data structures via the
* data_data function in Source. It adds a listener to the ListVisualizer singleton
* which updates the steps list via setState whenever new steps are added.
*/
class SideContentListVisualizer extends React.Component<{}, State> {
private $parent: HTMLElement | null = null;

constructor(props: any) {
super(props);
this.state = { loading: true };
}
this.state = { steps: [], currentStep: 0 };
ListVisualizer.init(steps => {
if (!steps) {
// Blink icon
const icon = document.getElementById('data_visualiser-icon');

public componentDidMount() {
this.tryToLoad();
if (icon) {
icon.classList.add('side-content-tab-alert');
}
}
this.setState({ steps, currentStep: 0 });
});
}

public render() {
const listVisualizerHandlers = {
PREVIOUS_STEP: this.onPrevButtonClick,
NEXT_STEP: this.onNextButtonClick
};
const step: Step | undefined = this.state.steps[this.state.currentStep];

const listVisualizer = (window as any).ListVisualizer;
// Default text will be hidden by visualizer.js when 'draw_data' is called
return (
<HotKeys keyMap={listVisualizerKeyMap} handlers={listVisualizerHandlers}>
<div
ref={r => (this.$parent = r)}
className={classNames('sa-list-visualizer', Classes.DARK)}
>
{(listVisualizer?.getStepCount() ?? 0) > 1 ? (
<div className={classNames('sa-list-visualizer', Classes.DARK)}>
{this.state.steps.length > 1 ? (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Button
large={true}
outlined={true}
icon={IconNames.ARROW_LEFT}
onClick={this.onPrevButtonClick}
disabled={(listVisualizer?.getCurrentStep() ?? 1) === 1}
disabled={this.state.currentStep === 0}
>
Prev
</Button>
<h3
className="bp3-text-large"
style={{ alignSelf: 'center', display: 'inline', margin: 0 }}
>
Call {listVisualizer?.getCurrentStep() ?? '0'}/{listVisualizer?.getStepCount()}
Call {this.state.currentStep + 1}/{this.state.steps.length}
</h3>
<Button
large={true}
outlined={true}
icon={IconNames.ARROW_RIGHT}
onClick={this.onNextButtonClick}
disabled={
listVisualizer
? listVisualizer.getCurrentStep() === listVisualizer.getStepCount()
this.state.steps.length > 0
? this.state.currentStep === this.state.steps.length - 1
: true
}
>
Next
</Button>
</div>
) : null}
<p
id="data-visualizer-default-text"
className={Classes.RUNNING_TEXT}
hidden={listVisualizer?.hasDrawing() ?? false}
>
The data visualizer visualizes data structures.
<br />
<br />
It is activated by calling the function <code>draw_data(the_data)</code>, where{' '}
<code>the_data</code> would be the data structure that you want to visualize.
<br />
<br />
The data visualizer uses box-and-pointer diagrams, as introduced in{' '}
<a href={Links.textbookChapter2_2} rel="noopener noreferrer" target="_blank">
<i>
Structure and Interpretation of Computer Programs, JavaScript Adaptation, Chapter 2,
Section 2
</i>
</a>
.
</p>
{this.state.loading && (
<NonIdealState description="Loading Data Visualizer..." icon={<Spinner />} />
{this.state.steps ? (
<div style={{ display: 'flex', flexDirection: 'row' }}>
{step?.map((elem, i) => (
<div key={i} style={{ flex: 1 }}>
<h3>Structure {i + 1}</h3>
{elem}
</div>
))}
</div>
) : (
<p id="data-visualizer-default-text" className={Classes.RUNNING_TEXT}>
The data visualizer visualises data structures.
<br />
<br />
It is activated by calling the function <code>draw_data(the_data)</code>, where{' '}
<code>the_data</code> would be the data structure that you want to visualise.
<br />
<br />
The data visualizer uses box-and-pointer diagrams, as introduced in{' '}
<a href={Links.textbookChapter2_2} rel="noopener noreferrer" target="_blank">
<i>
Structure and Interpretation of Computer Programs, JavaScript Adaptation, Chapter
2, Section 2
</i>
</a>
.
</p>
)}
</div>
</HotKeys>
);
}

private onPrevButtonClick = () => {
const element = (window as any).ListVisualizer;
element.previous();
this.setState({});
this.setState(state => {
return { currentStep: state.currentStep - 1 };
});
};

private onNextButtonClick = () => {
const element = (window as any).ListVisualizer;
element.next();
this.setState({});
};

private tryToLoad = () => {
const element = (window as any).ListVisualizer;
if (this.$parent && element) {
// List Visualizer has been loaded into the DOM
element.init(this.$parent);
this.setState((state, props) => {
return { loading: false };
});
} else {
// Try again in 1 second
window.setTimeout(this.tryToLoad, 1000);
}
this.setState(state => {
return { currentStep: state.currentStep + 1 };
});
};
}

Expand Down
17 changes: 11 additions & 6 deletions src/commons/utils/JsSlangHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { stringify } from 'js-slang/dist/utils/stringify';
import { difference, keys } from 'lodash';
import EnvVisualizer from 'src/features/envVisualizer/EnvVisualizer';

import ListVisualizer from '../../features/listVisualizer/ListVisualizer';
import { Data } from '../../features/listVisualizer/ListVisualizerTypes';
import { handleConsoleLog } from '../application/actions/InterpreterActions';

/**
Expand Down Expand Up @@ -77,12 +79,15 @@ function cadetAlert(value: any) {
*
* @param list the list to be visualized.
*/
function visualizeList(...xs: any[]) {
if ((window as any).ListVisualizer) {
// Pass in xs[0] since xs is in the form; [(Array of drawbables), "playground"]
(window as any).ListVisualizer.draw(xs[0]);
return xs[0];
} else {
function visualizeList(...args: Data[]) {
try {
// Pass in args[0] since args is in the form; [(Array of drawables), "playground"]
ListVisualizer.drawData(args[0]);

// If there is only one arg, just print out the first arg in REPL, instead of [first arg]
return args[0].length === 1 ? args[0][0] : args[0];
} catch (err) {
console.log(err);
throw new Error('List visualizer is not enabled');
}
}
Expand Down
26 changes: 26 additions & 0 deletions src/features/listVisualizer/Config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Represents the config used to draw the drawings.
*/
export const Config = {
StrokeWidth: 2,
Stroke: 'white',
Fill: 'white',
DistanceX: 50,
DistanceY: 60,

BoxWidth: 90,
BoxHeight: 30,
VertBarPos: 0.5,
BoxSpacingX: 50,
BoxSpacingY: 60,

CircleRadius: 12,

ArrowSpace: 5,
ArrowSpaceH: 13, // horizontal
ArrowLength: 8,
ArrowAngle: 0.5236, //25 - 0.4363,//20 - 0.3491,// 30 - 0.5236

Padding: 5,
CanvasWidth: 1000
};
106 changes: 106 additions & 0 deletions src/features/listVisualizer/ListVisualizer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Layer, Stage, Text } from 'react-konva';

import { Data, Step } from './ListVisualizerTypes';
import { findDataHeight, findDataWidth, isFunction, isPair, toText } from './ListVisualizerUtils';
import { Tree } from './tree/Tree';
import { DataTreeNode, FunctionTreeNode } from './tree/TreeNode';

/**
* The list visualizer class.
* Exposes three function: init, drawData, and clear.
*
* init is used by SideContentListVisualizer as a hook.
* drawData is the draw_data function in source.
* clear is used by WorkspaceSaga to reset the visualizer after every "Run" button press
*/
export default class ListVisualizer {
private static empty(step: Step[]) {}
private static setSteps: (step: Step[]) => void = ListVisualizer.empty;
private static _instance = new ListVisualizer();

private steps: Step[] = [];
private nodeLabel = 0;
private nodeToLabelMap: Map<DataTreeNode, number> = new Map();

private constructor() {}

public static init(setSteps: (step: Step[]) => void): void {
ListVisualizer.setSteps = setSteps;
}

public static drawData(structures: Data[]): void {
if (!ListVisualizer.setSteps) {
throw new Error('List visualizer not initialized');
}
ListVisualizer._instance.addStep(structures);
ListVisualizer.setSteps(ListVisualizer._instance.steps);
}

public static clear(): void {
ListVisualizer._instance = new ListVisualizer();
ListVisualizer.setSteps(ListVisualizer._instance.steps);
}

public static displaySpecialContent(dataNode: DataTreeNode): number {
return ListVisualizer._instance.displaySpecialContent(dataNode);
}

private displaySpecialContent(dataNode: DataTreeNode): number {
if (this.nodeToLabelMap.has(dataNode)) {
return this.nodeToLabelMap.get(dataNode) ?? 0;
} else {
// if (typeof display === 'function') {
// display('*' + nodeLabel + ': ' + value);
// } else {
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);
this.steps.push(step);
}

/**
* For student use. Draws a list 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 {
/**
* Create konva stage according to calculated width and height of drawing.
* Theoretically, as each box is 90px wide and successive boxes overlap by half,
* the width of the drawing should be roughly (width * 45), with a similar calculation
* for height.
* In practice, likely due to browser auto-scaling, for large drawings this results in
* some of the drawing being cut off. Hence the width and height formulas used are approximations.
*/
let layer: JSX.Element;

if (isPair(xs)) {
layer = Tree.fromSourceTree(xs).draw(500, 50);
} else if (isFunction(xs)) {
layer = <Layer>{new FunctionTreeNode(0).createDrawable(50, 50, 50, 50)}</Layer>;
} else {
layer = (
<Layer>
<Text
text={toText(xs, true)}
align={'center'}
fontStyle={'normal'}
fontSize={20}
fill={'white'}
/>
</Layer>
);
}
const stage = (
<Stage key={xs} width={findDataWidth(xs) * 60 + 60} height={findDataHeight(xs) * 60 + 100}>
{layer}
</Stage>
);
return stage;
}
}
9 changes: 9 additions & 0 deletions src/features/listVisualizer/ListVisualizerTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Source-related types
export type Data = any;
export type Pair = [Data, Data];
export type EmptyList = null;
export type List = [Data, List] | EmptyList;

// Drawing-related types
export type Drawing = JSX.Element;
export type Step = Drawing[];
Loading

0 comments on commit 5bfa5f2

Please sign in to comment.