Skip to content
Open
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
e92d3ab
StreamViz arrow drawing
of89p Apr 6, 2026
e254f4b
Update js-slang dependency version in package.json
DaRealTristan Apr 6, 2026
02c8ea1
package.json fix and formatting
Akshay-2007-1 Apr 6, 2026
0013166
deleted useless ts file
Akshay-2007-1 Apr 6, 2026
71f111c
Merge branch 'master' into streamVis
DaRealTristan Apr 8, 2026
484a680
removed debug logs as per gemini suggestion
TristanTayYuHng Apr 8, 2026
2d48357
removed debug logs as per gemini suggestion
TristanTayYuHng Apr 8, 2026
6344e48
Fixed merger with layout feature
TristanTayYuHng Apr 8, 2026
8fe8c43
Merge branch 'master' into streamVis
DaRealTristan Apr 8, 2026
7b21298
missing curly brace
of89p Apr 8, 2026
d8758f4
Simplifying features to integrate with arrows team
DaRealTristan Apr 8, 2026
5319513
Merge branch 'master' into streamVis
martin-henz Apr 8, 2026
5114633
removed streamsPointStep
of89p Apr 9, 2026
f4d64e3
Merge branch 'streamVis' of https://github.com/source-academy/fronten…
DaRealTristan Apr 9, 2026
fe322c0
Merge branch 'streamVis' of https://github.com/source-academy/fronten…
TristanTayYuHng Apr 9, 2026
9b9a019
Removed local resolution for 'js-slang' package.
DaRealTristan Apr 9, 2026
ecc11c5
simplified for mvp
TristanTayYuHng Apr 9, 2026
e48d282
simplified for mvp
TristanTayYuHng Apr 9, 2026
27ee97c
removed redundant draw
TristanTayYuHng Apr 9, 2026
599b74f
Merge branch 'master' into streamVis
martin-henz Apr 10, 2026
db43ec6
Drifting fixed!!
Akshay-2007-1 Apr 10, 2026
755bcef
print mode dotted line colour change
TristanTayYuHng Apr 10, 2026
a7c1787
Update js-slang dependency resolution in yarn.lock
DaRealTristan Apr 10, 2026
05e6d15
Update js-slang package version
DaRealTristan Apr 10, 2026
155e6d7
yarn format
TristanTayYuHng Apr 10, 2026
53a5458
bumping js-slang to 1.0.90
martin-henz Apr 17, 2026
05407ec
Merge branch 'master' into streamVis
martin-henz Apr 17, 2026
d4a1a6c
Merge branch 'master' into streamVis
martin-henz Apr 17, 2026
334e2a3
trying to fix testcase
martin-henz Apr 17, 2026
a98d4d2
Merge remote-tracking branch 'origin/streamVis' into streamVis
martin-henz Apr 17, 2026
4e795ea
streamvis now survives run
DaRealTristan Apr 19, 2026
e7e374f
Merge branch 'master' into streamVis
martin-henz Apr 20, 2026
54af008
bumping js-slang
martin-henz Apr 20, 2026
8fd160c
Fixed multiple arrows to pair issue
DaRealTristan Apr 21, 2026
df7b92c
Merge branch 'master' into streamVis
martin-henz Apr 22, 2026
917f345
Merge branch 'master' of https://github.com/source-academy/frontend i…
RichDom2185 May 6, 2026
967f06c
Merge branch 'master' into streamVis
DaRealTristan May 26, 2026
050f95f
resolved merge conflicts and ran yarn format
DaRealTristan May 26, 2026
c52c48b
yarn format
DaRealTristan May 26, 2026
a2475fe
fix: Line 81 bug. Removed redundant code due to mismatched ordering
Akshay-2007-1 May 27, 2026
dd5a1f2
Merge branch 'master' into streamVis
RichDom2185 May 31, 2026
fad0127
Merge branch 'master' into streamVis
martin-henz Jun 1, 2026
37d480e
Merge branch 'master' into streamVis
martin-henz Jun 1, 2026
34373de
removed comments and console logs
DaRealTristan Jun 1, 2026
c0cefd0
Merge branch 'master' into streamVis
martin-henz Jun 2, 2026
a117f8a
Merge branch 'master' into streamVis
martin-henz Jun 3, 2026
53377b8
Merge branch 'master' into streamVis
RichDom2185 Jun 3, 2026
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
19 changes: 16 additions & 3 deletions src/commons/sideContent/content/SideContentCseMachine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,12 @@
label="From stash"
onChange={() => this.toggleArrowFilter('stash')}
/>
<Checkbox
checked={CseMachine.getPairCreationMode()}
disabled={!this.state.visualization}
label="Pairs returned by nullary functions"
onChange={() => this.togglePairCreationModeArrows()}
/>
</div>
}
>
Expand Down Expand Up @@ -613,7 +619,8 @@
};

private stepNextChangepoint = () => {
for (const step of this.props.changepointSteps) {
const changeSteps = this.props.changepointSteps;
for (const step of changeSteps) {
if (step > this.state.value) {
this.sliderShift(step);
this.sliderRelease(step);
Expand All @@ -625,8 +632,9 @@
};

private stepPrevChangepoint = () => {
for (let i = this.props.changepointSteps.length - 1; i >= 0; i--) {
const step = this.props.changepointSteps[i];
const changeSteps = this.props.changepointSteps;
for (let i = changeSteps.length - 1; i >= 0; i--) {
const step = changeSteps[i];
if (step < this.state.value) {
this.sliderShift(step);
this.sliderRelease(step);
Expand All @@ -643,6 +651,11 @@
this.refreshArrowFilters();
};

private togglePairCreationModeArrows = () => {
CseMachine.togglePairCreationMode();
this.refreshArrowFilters();
};

private setAllArrowFilters = (visible: boolean) => {
CseMachine.setAllArrowOriginsVisible(visible);
this.refreshArrowFilters();
Expand Down Expand Up @@ -710,7 +723,7 @@
dispatch
);

export const SideContentCseMachine = connect(

Check warning on line 726 in src/commons/sideContent/content/SideContentCseMachine.tsx

View workflow job for this annotation

GitHub Actions / lint (eslint)

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
mapStateToProps,
mapDispatchToProps
)(SideContentCseMachineBase);
Expand Down
35 changes: 34 additions & 1 deletion src/features/cseMachine/CseMachine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export default class CseMachine {
private static printableMode: boolean = false;
private static controlStash: boolean = false; // TODO: discuss if the default should be true
private static stackTruncated: boolean = false;
private static pairCreationMode: boolean = false;
private static centerAlignment: boolean = false;
private static centerAlignmentToggled: boolean = false;
private static arrowOriginFilters: ArrowOriginFilters = {
Expand All @@ -55,15 +56,20 @@ export default class CseMachine {
private static currentEnvId: string;
private static control: Control | undefined;
private static stash: Stash | undefined;
private static streamLineage: Map<string, string[]> = new Map();
public static togglePrintableMode(): void {
CseMachine.printableMode = !CseMachine.printableMode;
}
public static toggleControlStash(): void {
//CseMachine.pairCreationMode = false;
Comment thread
DaRealTristan marked this conversation as resolved.
Outdated
CseMachine.controlStash = !CseMachine.controlStash;
}
public static toggleStackTruncated(): void {
CseMachine.stackTruncated = !CseMachine.stackTruncated;
}
public static togglePairCreationMode(): void {
CseMachine.pairCreationMode = !CseMachine.pairCreationMode;
}
public static setClearDeadFrames(enabled: boolean): void {
Layout.clearDeadFrames = enabled;
}
Expand Down Expand Up @@ -168,7 +174,22 @@ export default class CseMachine {
}
}
}
public static getStreamLineage(key: string): string[] | undefined {
return CseMachine.streamLineage.get(key);
}
public static findKeyByValueInMap(value: any) {
for (const [key, array] of CseMachine.streamLineage.entries()) {
// console.log(key + array);
Comment thread
DaRealTristan marked this conversation as resolved.
Outdated
if (array.includes(value)) {
return key;
}
}

return undefined;
}
public static getPairCreationMode(): boolean {
return CseMachine.pairCreationMode;
}
public static isControl(): boolean {
return this.control ? !this.control.isEmpty() : false;
}
Expand Down Expand Up @@ -203,6 +224,7 @@ export default class CseMachine {
throw new Error('CSE machine not initialized');
CseMachine.control = context.runtime.control;
CseMachine.stash = context.runtime.stash;
CseMachine.streamLineage = context.streamLineage;
Comment thread
DaRealTristan marked this conversation as resolved.
CseMachine.setClearDeadFrames(false);

Layout.setContext(
Expand Down Expand Up @@ -401,7 +423,18 @@ export default class CseMachine {
CseAnimation.updateAnimation();
}

if (
if (CseMachine.getPairCreationMode()) {
Layout.setContext(CseMachine.environmentTree, CseMachine.control, CseMachine.stash);
if (!CseMachine.getMasterLayout()) {
CseMachine.setMasterLayout(Layout.getLayoutPositions(this.controlStash));
}
Layout.applyFixedPositions();
CseAnimation.updateAnimation();
this.setVis(Layout.draw());
// this.setVis(Layout.draw());
// console.log(Layout.currentDarkPairs);
// this.setVis(Layout.currentDarkPairs);
Comment thread
DaRealTristan marked this conversation as resolved.
Outdated
} else if (
CseMachine.getPrintableMode() &&
CseMachine.getControlStash() &&
CseMachine.getStackTruncated() &&
Expand Down
20 changes: 20 additions & 0 deletions src/features/cseMachine/CseMachineLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,21 @@ export class Layout {
static underlayArrows: React.ReactNode[] = [];
static overlayNodes: React.ReactNode[] = [];

// For pair creation layouts:
static currentDarkPairs: React.ReactNode;
static currentLightPairs: React.ReactNode;
static currentStackDarkPairs: React.ReactNode;
static currentStackTruncDarkPairs: React.ReactNode;
static currentStackLightPairs: React.ReactNode;
static currentStackTruncLightPairs: React.ReactNode;

// buffer for faster rendering of diagram when scrolling
static invisiblePaddingVertical: number = 300;
static invisiblePaddingHorizontal: number = 300;
static scrollContainerRef: RefObject<HTMLDivElement | null> = React.createRef();

// STREAM VISUALISATION
static pendingFnLink: boolean = false;
static resetUnderlayArrows() {
Layout.underlayArrows = [];
}
Expand Down Expand Up @@ -198,6 +208,13 @@ export class Layout {
Layout.currentStackTruncDark = undefined;
Layout.currentStackLight = undefined;
Layout.currentStackTruncLight = undefined;

Layout.currentLightPairs = undefined;
Layout.currentDarkPairs = undefined;
Layout.currentStackDarkPairs = undefined;
Layout.currentStackTruncDarkPairs = undefined;
Layout.currentStackLightPairs = undefined;
Layout.currentStackTruncLightPairs = undefined;
// clear/initialize data and value arrays
Layout.values.clear();
arrowSelection.clearSelection();
Expand Down Expand Up @@ -701,6 +718,7 @@ export class Layout {
Layout.resetUnderlayArrows();
Layout.resetOverlayNodes();
const levelNodes = Layout.levels.map(level => level.draw());
const streamNodes = null;
const controlNode = CseMachine.getControlStash() ? Layout.controlComponent.draw() : null;
const stashNode = CseMachine.getControlStash() ? Layout.stashComponent.draw() : null;
const underlayArrows = [...Layout.underlayArrows];
Expand Down Expand Up @@ -762,6 +780,7 @@ export class Layout {
listening={false}
/>
<KonvaGroup ref={Layout.contentGroupRef}>
{streamNodes}
{levelNodes}
{controlNode}
{stashNode}
Expand All @@ -778,6 +797,7 @@ export class Layout {
</div>
</div>
);

Layout.prevLayout = layout;
if (CseMachine.getPrintableMode()) {
if (CseMachine.getControlStash()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Config } from '../../CseMachineConfig';
import { StepsArray } from '../../CseMachineTypes';
import { ArrayValue } from '../values/ArrayValue';
import { ContValue } from '../values/ContValue';
import { FnValue } from '../values/FnValue';
import { GlobalFnValue } from '../values/GlobalFnValue';
import { DottedArrow } from './DottedArrow';

/** this class encapsulates an GenericArrow to be drawn between 2 points */
export class ArrowFromStreamNullaryFn extends DottedArrow {
constructor(
from: FnValue | GlobalFnValue | ContValue,
public offsetIndex: number = 0
) {
super(from);
this.faded = !from.isReferenced();
}

protected calculateSteps() {
const from = this.source as FnValue | GlobalFnValue | ContValue;
const to = this.target;

if (!to || !(to instanceof ArrayValue)) return [];

const verticalShift = this.offsetIndex * 20; // 20px vertical separation for multiple arrows to the same target

// The arrow starts from the right of the function circle
const startPointX = from.centerX + 2 * from.radius;
const startPointY = from.y();
// The arrow ends at the top-center of the array
const endPointX = to.x() + Config.DataUnitWidth / 2;
const endPointY = to.y();

// An intermediate point is used to create the arch.
// It is placed horizontally between the start and end points,
// and vertically above them to form an upward arch.
const midPointX = (startPointX + endPointX) / 2;
const archHeight = 50;
const midPointY = Math.min(startPointY, endPointY) - archHeight - verticalShift;
const steps: StepsArray = [
// The GenericArrow class will draw a path through these points,
// creating smooth curves at the corners.
() => [startPointX, startPointY],
() => [midPointX, midPointY],
() => [endPointX, endPointY]
];
return steps;
}
}
33 changes: 33 additions & 0 deletions src/features/cseMachine/components/arrows/DottedArrow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Arrow as KonvaArrow, Group as KonvaGroup, Path as KonvaPath } from 'react-konva';

import CseMachine from '../../CseMachine';
import { Config, ShapeDefaultProps } from '../../CseMachineConfig';
import { Layout } from '../../CseMachineLayout';
import { IVisible } from '../../CseMachineTypes';
import { GenericArrow } from './GenericArrow';

export class DottedArrow extends GenericArrow<IVisible, IVisible> {
draw() {
const stroke = CseMachine.getPrintableMode() ? '#9B870C' : '#ded74e';
return (
<KonvaGroup key={Layout.key++} ref={this.ref} listening={false}>
<KonvaPath
{...ShapeDefaultProps}
stroke={stroke}
strokeWidth={Config.ArrowStrokeWidth}
data={this.path()}
key={Layout.key++}
dash={[10, 5]}
/>
<KonvaArrow
{...ShapeDefaultProps}
points={this.points.slice(this.points.length - 4)}
fill={stroke}
strokeEnabled={false}
pointerWidth={Config.ArrowHeadSize}
key={Layout.key++}
/>
</KonvaGroup>
);
}
}
13 changes: 13 additions & 0 deletions src/features/cseMachine/components/values/ArrayValue.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Closure from 'js-slang/dist/cse-machine/closure';
import { KonvaEventObject } from 'konva/lib/Node';
import React from 'react';
import { Group } from 'react-konva';
Expand Down Expand Up @@ -36,6 +37,18 @@ export class ArrayValue extends Value implements IHoverable {
super();
Layout.memoizeValue(data, this);
this.addReference(firstReference);
/** handling pairs for stream visualisation */
if (data[1] instanceof Closure) {
const originFnId = CseMachine.findKeyByValueInMap(data.id);
if (originFnId != undefined) {
const originFnValue = Layout.values.get(originFnId);
if (originFnValue instanceof FnValue) {
originFnValue.addArrow(this);
} else {
Layout.pendingFnLink = true;
}
}
}
}

handleNewReference(newReference: ReferenceType): void {
Expand Down
55 changes: 55 additions & 0 deletions src/features/cseMachine/components/values/FnValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import {
} from '../../CseMachineUtils';
import { ArrowFromFn } from '../arrows/ArrowFromFn';
import { ArrowFromFnTooltip } from '../arrows/ArrowFromFnTooltip';
import { ArrowFromStreamNullaryFn } from '../arrows/ArrowFromStreamNullaryFn';
import { Binding } from '../Binding';
import { Frame } from '../Frame';
import { ArrayValue } from './ArrayValue';
import { FunctionTooltipLabels, Value } from './Value';

/** this class encapsulates a JS Slang function (not from the global frame) that
Expand Down Expand Up @@ -57,6 +59,8 @@ export class FnValue extends Value implements IHoverable {
enclosingFrame?: Frame;
private isExpandedDescription: boolean = false;
private _arrow: ArrowFromFn | undefined;
private _streamArrows: ArrowFromStreamNullaryFn[] = [];

private tooltipArrow: ArrowFromFnTooltip | undefined;
private showTooltipArrow: boolean = false;

Expand Down Expand Up @@ -102,6 +106,32 @@ export class FnValue extends Value implements IHoverable {
this.totalWidth =
this._width + Config.TextMargin + this.exportTooltipWidth + Config.FnTooltipTextPadding * 2;

if (Layout.pendingFnLink) {
const thisId = (data as any).id;
const linkedPairs = CseMachine.getStreamLineage(thisId);

if (linkedPairs != undefined && CseMachine.getStreamLineage(thisId) != undefined) {
// console.log(CseMachine.getStreamLineage(thisId))
const targetCounts = new Map<ArrayValue, number>();

for (const pair of linkedPairs) {
const pairObject = Layout.values.get(pair) as ArrayValue;
// The pair might not be in Layout.values if it's not reachable in the current step, so we check.
if (pairObject instanceof ArrayValue) {
const currentCount = targetCounts.get(pairObject) || 0;
targetCounts.set(pairObject, currentCount + 1);

this._streamArrows.push(
new ArrowFromStreamNullaryFn(this).to(pairObject) as ArrowFromStreamNullaryFn
);
this._streamArrows[this._streamArrows.length - 1].draw();
}
}
}

Layout.pendingFnLink = false;
}

this.addReference(firstReference);
}

Expand Down Expand Up @@ -138,6 +168,16 @@ export class FnValue extends Value implements IHoverable {
return this._arrow;
}

addArrow(target: any): void {
// Check how many arrows already point to this specific target
const currentCount = this._streamArrows.filter(arrow => arrow.target === target).length;

// Pass the count as the offsetIndex
this._streamArrows?.push(
new ArrowFromStreamNullaryFn(this, currentCount).to(target) as ArrowFromStreamNullaryFn
);
}

onMouseEnter = ({ currentTarget }: KonvaEventObject<MouseEvent>) => {
if (CseMachine.getPrintableMode()) return;
setHoveredCursor(currentTarget);
Expand Down Expand Up @@ -225,6 +265,20 @@ export class FnValue extends Value implements IHoverable {
}

draw(): React.ReactNode {
const pairs = CseMachine.getStreamLineage((this.data as any).id);
this._streamArrows = [];
Comment thread
DaRealTristan marked this conversation as resolved.
if (CseMachine.getPairCreationMode()) {
// Clear arrows to prevent duplicates from multiple draw calls

if (pairs != undefined) {
for (const pair of pairs) {
const target = Layout.values.get(pair);
if (target) {
this.addArrow(target);
}
}
}
}

This comment was marked as outdated.

if (this.fnName === undefined) {
throw new Error('Closure has no main reference and is not initialised!');
}
Expand Down Expand Up @@ -301,6 +355,7 @@ export class FnValue extends Value implements IHoverable {
/>
</Group>
{this._arrow?.draw()}
{this._streamArrows.map((arrow, index) => arrow.draw())}
{this.tooltipArrow?.draw()}
</React.Fragment>
);
Expand Down
Loading