linlu 16 hours ago
parent 99f4311eb7
commit 70fde7f10f

24
.gitignore vendored

@ -0,0 +1,24 @@
# WebStorm settings
/.idea
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

@ -0,0 +1,17 @@
tasks:
- init: >
git clone https://github.com/algorithm-visualizer/server.git &&
cd server &&
npm install &&
echo -e "GITHUB_CLIENT_ID=dummy\nGITHUB_CLIENT_SECRET=dummy\nAWS_ACCESS_KEY_ID=dummy\nAWS_SECRET_ACCESS_KEY=dummy" > .env.local &&
cd ..
command: cd server && npm run watch
- init: >
npm install &&
echo 'DANGEROUSLY_DISABLE_HOST_CHECK=true' > .env.local
command: npm start
ports:
- port: 3000
onOpen: notify
- port: 8080
onOpen: ignore

@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at parkjs814@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

@ -0,0 +1,60 @@
# Contributing
> #### Table of Contents
> - [Running Locally](#running-locally)
> - [Running in Gitpod](#running-in-gitpod)
> - [Directory Structure](#directory-structure)
Are you a first-timer in contributing to open source? [These guidelines](https://opensource.guide/how-to-contribute/#how-to-submit-a-contribution) from GitHub might help!
## Running Locally
1. Fork this repository.
2. Clone your forked repo to your machine.
```bash
git clone https://github.com/<your-username>/algorithm-visualizer.git
```
3. Choose whether to run [`server`](https://github.com/algorithm-visualizer/server) on your machine or to use the remote server.
- If you choose to run the server locally as well, follow the instructions [here](https://github.com/algorithm-visualizer/server/blob/master/CONTRIBUTING.md#running-locally).
- If you choose to use the remote server, **temporarily** (i.e., don't commit this change) modify `package.json` as follows:
```diff
- "proxy": "http://localhost:8080",
+ "proxy": "https://algorithm-visualizer.org",
```
4. Install dependencies, and run the web app.
```bash
cd algorithm-visualizer
npm install
npm start
```
5. Open [`http://localhost:3000/`](http://localhost:3000/) in a web browser.
## Running in Gitpod
You can also run `algorithm-visualizer` in Gitpod, a free online dev environment for GitHub.
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/algorithm-visualizer/algorithm-visualizer)
## Directory Structure
- [**branding/**](branding) contains representative image files.
- [**public/**](public) contains static files to be served.
- [**src/**](src) contains source code.
- [**apis/**](src/apis) defines outgoing API requests.
- [**common/**](src/common) contains commonly used files.
- [**components/**](src/components) contains UI components.
- [**core/**](src/core) processes visualization.
- [**layouts/**](src/core/layouts) layout tracers.
- [**renderers/**](src/core/renderers) renders visualization data.
- [**tracers/**](src/core/tracers) interprets visualizing commands into visualization data.
- [**files/**](src/files) contains markdown or skeleton files to be shown in the code editor.
- [**reducers/**](src/reducers) contains Redux reducers.

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Jinseo Jason Park
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -1,2 +1,54 @@
# algorithm-visualizer
# Algorithm Visualizer
## Introduction
Welcome to Algorithm Visualizer, an interactive online platform designed to bring algorithms to life through visualization. Whether you're a student, teacher, or professional, our platform provides an engaging way to explore and understand various algorithms.
[![GitHub contributors](https://img.shields.io/github/contributors/algorithm-visualizer/algorithm-visualizer.svg?style=flat-square)](https://github.com/algorithm-visualizer/algorithm-visualizer/graphs/contributors)
[![GitHub license](https://img.shields.io/github/license/algorithm-visualizer/algorithm-visualizer.svg?style=flat-square)](https://github.com/algorithm-visualizer/algorithm-visualizer/blob/master/LICENSE)
## Languages and Frameworks Used
[![Languages](https://skillicons.dev/icons?i=html,css,js,react,nodejs,redux)](https://skillicons.dev)
## Key Features
<ul>
<li>
### Visualize algorithms from code:
Algorithm Visualizer allows you to witness algorithms in action by visualizing code written in various programming languages. This visual approach facilitates a better understanding of algorithmic behavior..</li>
<li>
### Learn about Algorithms:
Explore our collection of tutorials, articles, and videos that serve as valuable resources for learning about algorithms.
</li>
</ul>
## algorithms
In this repository, you'll find visualizations of algorithms showcased in the website's side menu. Contributions here directly impact the educational content available on the platform. https://github.com/algorithm-visualizer/algorithms</li>
</ul>
## tracers
Explore the various visualization libraries in different programming languages. These libraries extract visualization commands from code.
https://github.com/search?q=topic%3Avisualization-library+org%3Aalgorithm-visualizer&type=Repositories</li>
</ul>
## Live Demo
Learning an algorithm gets much easier with visualizing it. Don't get what we mean? Check it out:
[**algorithm-visualizer.org**![Screenshot](https://raw.githubusercontent.com/algorithm-visualizer/algorithm-visualizer/master/branding/screenshot.png)](https://algorithm-visualizer.org/)
## Contributing
Our project consists of multiple repositories, each playing a crucial role in the Algorithm Visualizer ecosystem. If you're interested in contributing, check out the guidelines for the specific repository:
- [**`algorithm-visualizer`**](https://github.com/algorithm-visualizer/algorithm-visualizer) is a web app written in React. It contains UI components and interprets commands into visualizations. Check out [the contributing guidelines](CONTRIBUTING.md).
- [**`server`**](https://github.com/algorithm-visualizer/server) serves the web app and provides APIs that it needs on the fly. (e.g., GitHub sign in, compiling/running code, etc.)
- [**`algorithms`**](https://github.com/algorithm-visualizer/algorithms) contains visualizations of algorithms shown on the side menu of the website.
- [**`tracers.*`**](https://github.com/search?q=topic%3Avisualization-library+org%3Aalgorithm-visualizer&type=Repositories) are visualization libraries written in each supported language. They extract visualizing commands from code.
Ready to contribute? Explore the repositories and become part of the Algorithm Visualizer community!

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

@ -0,0 +1,6 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"baseUrl": "src"
}
}

15595
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,68 @@
{
"name": "@algorithm-visualizer/algorithm-visualizer",
"version": "2.0.0",
"title": "Algorithm Visualizer",
"description": "Algorithm Visualizer is an interactive online platform that visualizes algorithms from code.",
"scripts": {
"start": "NODE_OPTIONS=--openssl-legacy-provider react-scripts start",
"build": "NODE_OPTIONS=--openssl-legacy-provider react-scripts build",
"test": "NODE_OPTIONS=--openssl-legacy-provider react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "https://algorithm-visualizer.org",
"license": "MIT",
"engines": {
"node": ">=10.15.3"
},
"dependencies": {
"@fortawesome/fontawesome": "^1.1.8",
"@fortawesome/fontawesome-free-brands": "^5.0.13",
"@fortawesome/fontawesome-free-solid": "^5.0.13",
"@fortawesome/fontawesome-svg-core": "^1.2.19",
"@fortawesome/react-fontawesome": "0.1.4",
"axios": "^0.19.0",
"bluebird": "latest",
"brace": "latest",
"chart.js": "^2.8.0",
"js-cookie": "^2.2.0",
"sass": "^1.69.0",
"query-string": "^6.7.0",
"raw-loader": "^3.0.0",
"react": "^16.8.6",
"react-ace": "^7.0.2",
"react-chartjs-2": "^2.7.6",
"react-dom": "^16.8.6",
"react-helmet": "^5.2.1",
"react-input-autosize": "^2.2.1",
"react-input-range": "^1.3.0",
"react-markdown": "^4.0.8",
"react-redux": "^7.0.3",
"react-router": "^5.0.1",
"react-router-dom": "^5.0.1",
"react-router-redux": "^4.0.8",
"react-scripts": "^3.0.1",
"react-toastify": "^5.2.1",
"redbox-react": "^1.6.0",
"redux": "^4.0.1",
"redux-actions": "^2.6.5",
"remove-markdown": "^0.3.0",
"screenfull": "^4.2.0",
"sprintf-js": "^1.1.2",
"uuid": "^3.3.2"
}
}

@ -0,0 +1 @@
icons/icon-32x32.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-78128848-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'UA-78128848-1');
</script>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width">
<meta name="theme-color" content="#393939">
<meta name="description" content="Algorithm Visualizer is an interactive online platform that visualizes algorithms from code." data-react-helmet="true" />
<meta property="og:image"
content="https://raw.githubusercontent.com/algorithm-visualizer/algorithm-visualizer/master/branding/screenshot.png" />
<meta property="og:site_name" content="Algorithm Visualizer" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.png" type="image/png">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link href="https://fonts.googleapis.com/css?family=Roboto:400,700" rel="stylesheet">
<title>Algorithm Visualizer</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

@ -0,0 +1,27 @@
{
"name": "Algorithm Visualizer",
"short_name": "Algorithm Visualizer",
"start_url": "/?utm_source=homescreen",
"display": "fullscreen",
"orientation": "landscape",
"theme_color": "#393939",
"background_color": "#393939",
"description": "Algorithm Visualizer is an interactive online platform that visualizes algorithms from code.",
"icons": [
{
"src": "icons/icon-32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

@ -0,0 +1,2 @@
User-agent: *
Disallow: /scratch-paper/

@ -0,0 +1,112 @@
import Promise from 'bluebird';
import axios from 'axios';
axios.interceptors.response.use(response => response.data);
const request = (url, process) => {
const tokens = url.split('/');
const baseURL = /^https?:\/\//i.test(url) ? '' : '/api';
return (...args) => {
const mappedURL = baseURL + tokens.map((token, i) => token.startsWith(':') ? args.shift() : token).join('/');
return Promise.resolve(process(mappedURL, args));
};
};
const GET = URL => {
return request(URL, (mappedURL, args) => {
const [params, cancelToken] = args;
return axios.get(mappedURL, { params, cancelToken });
});
};
const DELETE = URL => {
return request(URL, (mappedURL, args) => {
const [params, cancelToken] = args;
return axios.delete(mappedURL, { params, cancelToken });
});
};
const POST = URL => {
return request(URL, (mappedURL, args) => {
const [body, params, cancelToken] = args;
return axios.post(mappedURL, body, { params, cancelToken });
});
};
const PUT = URL => {
return request(URL, (mappedURL, args) => {
const [body, params, cancelToken] = args;
return axios.put(mappedURL, body, { params, cancelToken });
});
};
const PATCH = URL => {
return request(URL, (mappedURL, args) => {
const [body, params, cancelToken] = args;
return axios.patch(mappedURL, body, { params, cancelToken });
});
};
const AlgorithmApi = {
getCategories: GET('/algorithms'),
getAlgorithm: GET('/algorithms/:categoryKey/:algorithmKey'),
};
const VisualizationApi = {
getVisualization: GET('/visualizations/:visualizationId'),
};
const GitHubApi = {
auth: token => Promise.resolve(axios.defaults.headers.common['Authorization'] = token && `token ${token}`),
getUser: GET('https://api.github.com/user'),
listGists: GET('https://api.github.com/gists'),
createGist: POST('https://api.github.com/gists'),
editGist: PATCH('https://api.github.com/gists/:id'),
getGist: GET('https://api.github.com/gists/:id'),
deleteGist: DELETE('https://api.github.com/gists/:id'),
forkGist: POST('https://api.github.com/gists/:id/forks'),
};
const TracerApi = {
md: ({ code }) => Promise.resolve([{
key: 'markdown',
method: 'MarkdownTracer',
args: ['Markdown'],
}, {
key: 'markdown',
method: 'set',
args: [code],
}, {
key: null,
method: 'setRoot',
args: ['markdown'],
}]),
json: ({ code }) => new Promise(resolve => resolve(JSON.parse(code))),
js: ({ code }, params, cancelToken) => new Promise((resolve, reject) => {
const worker = new Worker('/api/tracers/js/worker');
if (cancelToken) {
cancelToken.promise.then(cancel => {
worker.terminate();
reject(cancel);
});
}
worker.onmessage = e => {
worker.terminate();
resolve(e.data);
};
worker.onerror = error => {
worker.terminate();
reject(error);
};
worker.postMessage(code);
}),
cpp: POST('/tracers/cpp'),
java: POST('/tracers/java'),
};
export {
AlgorithmApi,
VisualizationApi,
GitHubApi,
TracerApi,
};

@ -0,0 +1,25 @@
import { CODE_CPP, CODE_JAVA, CODE_JS } from 'files';
const languages = [{
name: 'JavaScript',
ext: 'js',
mode: 'javascript',
skeleton: CODE_JS,
}, {
name: 'C++',
ext: 'cpp',
mode: 'c_cpp',
skeleton: CODE_CPP,
}, {
name: 'Java',
ext: 'java',
mode: 'java',
skeleton: CODE_JAVA,
}];
const exts = languages.map(language => language.ext);
export {
languages,
exts,
};

@ -0,0 +1,25 @@
$theme-dark: #242424;
$theme-normal: #393939;
$theme-light: #505050;
$color-font: #bbbbbb;
$color-shadow: rgba(#000000, .2);
$color-overlay: rgba(#ffffff, .1);
$color-alert: #f3bd58;
$color-selected: #2962ff;
$color-patched: #c51162;
$color-highlight: #29d;
$color-active: #00e676;
:export {
themeDark: $theme-dark;
themeNormal: $theme-normal;
themeLight: $theme-light;
colorFont: $color-font;
colorShadow: $color-shadow;
colorOverlay: $color-overlay;
colorAlert: $color-alert;
colorSelected: $color-selected;
colorPatched: $color-patched;
colorHighlight: $color-highlight;
colorActive: $color-active;
}

@ -0,0 +1,9 @@
$line-height: 32px;
$font-size-normal: 12px;
$font-size-large: 14px;
:export {
lineHeight: $line-height;
fontSizeNormal: $font-size-normal;
fontSizeLarge: $font-size-large;
}

@ -0,0 +1,2 @@
$font-family-normal: 'Roboto', sans-serif;
$font-family-monospace: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;

@ -0,0 +1,3 @@
@import "colors";
@import "dimensions";
@import "fonts";

@ -0,0 +1,50 @@
const classes = (...arr) => arr.filter(v => v).join(' ');
const distance = (a, b) => {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
};
const extension = fileName => /(?:\.([^.]+))?$/.exec(fileName)[1];
const refineGist = gist => {
const gistId = gist.id;
const title = gist.description;
delete gist.files['algorithm-visualizer'];
const { login, avatar_url } = gist.owner;
const files = Object.values(gist.files).map(file => ({
name: file.filename,
content: file.content,
contributors: [{ login, avatar_url }],
}));
return { login, gistId, title, files };
};
const createFile = (name, content, contributors) => ({ name, content, contributors });
const createProjectFile = (name, content) => createFile(name, content, [{
login: 'algorithm-visualizer',
avatar_url: 'https://github.com/algorithm-visualizer.png',
}]);
const createUserFile = (name, content) => createFile(name, content, undefined);
const isSaved = ({ titles, files, lastTitles, lastFiles }) => {
const serialize = (titles, files) => JSON.stringify({
titles,
files: files.map(({ name, content }) => ({ name, content })),
});
return serialize(titles, files) === serialize(lastTitles, lastFiles);
};
export {
classes,
distance,
extension,
refineGist,
createFile,
createProjectFile,
createUserFile,
isSaved,
};

@ -0,0 +1,30 @@
@import "~common/stylesheet/index";
.app {
display: flex;
flex-direction: column;
align-items: stretch;
height: 100%;
background-color: $theme-normal;
.header {
}
.workspace {
flex: 1;
.visualization_viewer {
background-color: $theme-dark;
}
.editor_tab_container {
}
}
.toast_container {
position: absolute;
bottom: 0;
right: 0;
z-index: 99;
}
}

@ -0,0 +1,254 @@
import React from 'react';
import Cookies from 'js-cookie';
import { connect } from 'react-redux';
import Promise from 'bluebird';
import { Helmet } from 'react-helmet';
import queryString from 'query-string';
import {
BaseComponent,
CodeEditor,
Header,
Navigator,
ResizableContainer,
TabContainer,
ToastContainer,
VisualizationViewer,
} from 'components';
import { AlgorithmApi, GitHubApi, VisualizationApi } from 'apis';
import { actions } from 'reducers';
import { createUserFile, extension, refineGist } from 'common/util';
import { exts, languages } from 'common/config';
import { SCRATCH_PAPER_README_MD } from 'files';
import styles from './App.module.scss';
class App extends BaseComponent {
constructor(props) {
super(props);
this.state = {
workspaceVisibles: [true, true, true],
workspaceWeights: [1, 2, 2],
};
this.codeEditorRef = React.createRef();
this.ignoreHistoryBlock = this.ignoreHistoryBlock.bind(this);
this.handleClickTitleBar = this.handleClickTitleBar.bind(this);
this.loadScratchPapers = this.loadScratchPapers.bind(this);
this.handleChangeWorkspaceWeights = this.handleChangeWorkspaceWeights.bind(this);
}
componentDidMount() {
window.signIn = this.signIn.bind(this);
window.signOut = this.signOut.bind(this);
const { params } = this.props.match;
const { search } = this.props.location;
this.loadAlgorithm(params, queryString.parse(search));
const accessToken = Cookies.get('access_token');
if (accessToken) this.signIn(accessToken);
AlgorithmApi.getCategories()
.then(({ categories }) => this.props.setCategories(categories))
.catch(this.handleError);
this.toggleHistoryBlock(true);
}
componentWillUnmount() {
delete window.signIn;
delete window.signOut;
this.toggleHistoryBlock(false);
}
componentWillReceiveProps(nextProps) {
const { params } = nextProps.match;
const { search } = nextProps.location;
if (params !== this.props.match.params || search !== this.props.location.search) {
const { categoryKey, algorithmKey, gistId } = params;
const { algorithm, scratchPaper } = nextProps.current;
if (algorithm && algorithm.categoryKey === categoryKey && algorithm.algorithmKey === algorithmKey) return;
if (scratchPaper && scratchPaper.gistId === gistId) return;
this.loadAlgorithm(params, queryString.parse(search));
}
}
toggleHistoryBlock(enable = !this.unblock) {
if (enable) {
const warningMessage = 'Are you sure you want to discard changes?';
window.onbeforeunload = () => {
const { saved } = this.props.current;
if (!saved) return warningMessage;
};
this.unblock = this.props.history.block((location) => {
if (location.pathname === this.props.location.pathname) return;
const { saved } = this.props.current;
if (!saved) return warningMessage;
});
} else {
window.onbeforeunload = undefined;
this.unblock();
this.unblock = undefined;
}
}
ignoreHistoryBlock(process) {
this.toggleHistoryBlock(false);
process();
this.toggleHistoryBlock(true);
}
signIn(accessToken) {
Cookies.set('access_token', accessToken);
GitHubApi.auth(accessToken)
.then(() => GitHubApi.getUser())
.then(user => {
const { login, avatar_url } = user;
this.props.setUser({ login, avatar_url });
})
.then(() => this.loadScratchPapers())
.catch(() => this.signOut());
}
signOut() {
Cookies.remove('access_token');
GitHubApi.auth(undefined)
.then(() => {
this.props.setUser(undefined);
})
.then(() => this.props.setScratchPapers([]));
}
loadScratchPapers() {
const per_page = 100;
const paginateGists = (page = 1, scratchPapers = []) => GitHubApi.listGists({
per_page,
page,
timestamp: Date.now(),
}).then(gists => {
scratchPapers.push(...gists.filter(gist => 'algorithm-visualizer' in gist.files).map(gist => ({
key: gist.id,
name: gist.description,
files: Object.keys(gist.files),
})));
if (gists.length < per_page) {
return scratchPapers;
} else {
return paginateGists(page + 1, scratchPapers);
}
});
return paginateGists()
.then(scratchPapers => this.props.setScratchPapers(scratchPapers))
.catch(this.handleError);
}
loadAlgorithm({ categoryKey, algorithmKey, gistId }, { visualizationId }) {
const { ext } = this.props.env;
const fetch = () => {
if (window.__PRELOADED_ALGORITHM__) {
this.props.setAlgorithm(window.__PRELOADED_ALGORITHM__);
delete window.__PRELOADED_ALGORITHM__;
} else if (window.__PRELOADED_ALGORITHM__ === null) {
delete window.__PRELOADED_ALGORITHM__;
return Promise.reject(new Error('Algorithm Not Found'));
} else if (categoryKey && algorithmKey) {
return AlgorithmApi.getAlgorithm(categoryKey, algorithmKey)
.then(({ algorithm }) => this.props.setAlgorithm(algorithm));
} else if (gistId === 'new' && visualizationId) {
return VisualizationApi.getVisualization(visualizationId)
.then(content => {
this.props.setScratchPaper({
login: undefined,
gistId,
title: 'Untitled',
files: [SCRATCH_PAPER_README_MD, createUserFile('visualization.json', JSON.stringify(content))],
});
});
} else if (gistId === 'new') {
const language = languages.find(language => language.ext === ext);
this.props.setScratchPaper({
login: undefined,
gistId,
title: 'Untitled',
files: [SCRATCH_PAPER_README_MD, language.skeleton],
});
} else if (gistId) {
return GitHubApi.getGist(gistId, { timestamp: Date.now() })
.then(refineGist)
.then(this.props.setScratchPaper);
} else {
this.props.setHome();
}
return Promise.resolve();
};
fetch()
.then(() => {
this.selectDefaultTab();
return null; // to suppress unnecessary bluebird warning
})
.catch(error => {
this.handleError(error);
this.props.history.push('/');
});
}
selectDefaultTab() {
const { ext } = this.props.env;
const { files } = this.props.current;
const editingFile = files.find(file => extension(file.name) === 'json') ||
files.find(file => extension(file.name) === ext) ||
files.find(file => exts.includes(extension(file.name))) ||
files[files.length - 1];
this.props.setEditingFile(editingFile);
}
handleChangeWorkspaceWeights(workspaceWeights) {
this.setState({ workspaceWeights });
this.codeEditorRef.current.handleResize();
}
toggleNavigatorOpened(navigatorOpened = !this.state.workspaceVisibles[0]) {
const workspaceVisibles = [...this.state.workspaceVisibles];
workspaceVisibles[0] = navigatorOpened;
this.setState({ workspaceVisibles });
}
handleClickTitleBar() {
this.toggleNavigatorOpened();
}
render() {
const { workspaceVisibles, workspaceWeights } = this.state;
const { titles, description, saved } = this.props.current;
const title = `${saved ? '' : '(Unsaved) '}${titles.join(' - ')}`;
const [navigatorOpened] = workspaceVisibles;
return (
<div className={styles.app}>
<Helmet>
<title>Educoder</title>
<meta name="description" content={description}/>
</Helmet>
<Header className={styles.header} onClickTitleBar={this.handleClickTitleBar}
navigatorOpened={navigatorOpened} loadScratchPapers={this.loadScratchPapers}
ignoreHistoryBlock={this.ignoreHistoryBlock}/>
<ResizableContainer className={styles.workspace} horizontal weights={workspaceWeights}
visibles={workspaceVisibles} onChangeWeights={this.handleChangeWorkspaceWeights}>
<Navigator/>
<VisualizationViewer className={styles.visualization_viewer}/>
<TabContainer className={styles.editor_tab_container}>
<CodeEditor ref={this.codeEditorRef}/>
</TabContainer>
</ResizableContainer>
<ToastContainer className={styles.toast_container}/>
</div>
);
}
}
export default connect(({ current, env }) => ({ current, env }), actions)(
App,
);

@ -0,0 +1,23 @@
import React from 'react';
class BaseComponent extends React.Component {
constructor(props) {
super(props);
this.handleError = this.handleError.bind(this);
}
handleError(error) {
if (error.response) {
const { data, statusText } = error.response;
const message = data ? typeof data === 'string' ? data : JSON.stringify(data) : statusText;
console.error(message);
this.props.showErrorToast(message);
} else {
console.error(error.message);
this.props.showErrorToast(error.message);
}
}
}
export default BaseComponent;

@ -0,0 +1,79 @@
@import "~common/stylesheet/index";
.button {
display: flex;
align-items: center;
cursor: pointer;
padding: 0 12px;
margin: 0;
.icon {
margin-right: 4px;
&.image {
width: 1.6em;
height: 1.6em;
background-position: center;
background-size: cover;
border-radius: 2px;
}
}
&.reverse {
flex-direction: row-reverse;
.icon {
margin-right: 0;
margin-left: 4px;
}
}
&.icon_only {
.icon {
margin-left: 0;
margin-right: 0;
}
}
&:hover {
background-color: $color-overlay;
}
&.primary {
&:hover {
background-color: $color-shadow;
&:active {
box-shadow: 0px 0px 10px 3px #1a1a1a inset;
}
}
&.active {
background-color: $color-shadow;
box-shadow: 0px 0px 10px 3px #1a1a1a inset;
font-weight: bold;
.icon {
color: $color-active;
}
}
}
&.selected {
background-color: $color-shadow;
&:hover {
color: rgba($color-font, .8);
}
}
&.disabled {
cursor: not-allowed;
background-color: $color-shadow;
opacity: 0.6;
}
&.confirming {
color: $color-alert;
}
}

@ -0,0 +1,89 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import faExclamationCircle from '@fortawesome/fontawesome-free-solid/faExclamationCircle';
import faSpinner from '@fortawesome/fontawesome-free-solid/faSpinner';
import { classes } from 'common/util';
import { Ellipsis } from 'components';
import styles from './Button.module.scss';
class Button extends React.Component {
constructor(props) {
super(props);
this.state = {
confirming: false,
};
this.timeout = null;
}
componentWillUnmount() {
if (this.timeout) {
window.clearTimeout(this.timeout);
this.timeout = undefined;
}
}
render() {
let { className, children, to, href, onClick, icon, reverse, selected, disabled, primary, active, confirmNeeded, inProgress, ...rest } = this.props;
const { confirming } = this.state;
if (confirmNeeded) {
if (confirming) {
className = classes(styles.confirming, className);
icon = faExclamationCircle;
children = <Ellipsis key="text">Click to Confirm</Ellipsis>;
const onClickOriginal = onClick;
onClick = () => {
if (onClickOriginal) onClickOriginal();
if (this.timeout) {
window.clearTimeout(this.timeout);
this.timeout = undefined;
this.setState({ confirming: false });
}
};
} else {
to = null;
href = null;
onClick = () => {
this.setState({ confirming: true });
this.timeout = window.setTimeout(() => {
this.timeout = undefined;
this.setState({ confirming: false });
}, 2000);
};
}
}
const iconOnly = !children;
const props = {
className: classes(styles.button, reverse && styles.reverse, selected && styles.selected, disabled && styles.disabled, primary && styles.primary, active && styles.active, iconOnly && styles.icon_only, className),
to: disabled ? null : to,
href: disabled ? null : href,
onClick: disabled ? null : onClick,
children: [
icon && (
typeof icon === 'string' ?
<div className={classes(styles.icon, styles.image)} key="icon"
style={{ backgroundImage: `url(${icon})` }} /> :
<FontAwesomeIcon className={styles.icon} fixedWidth icon={inProgress ? faSpinner : icon} spin={inProgress}
key="icon" />
),
children,
],
...rest,
};
return to ? (
<Link {...props} />
) : href ? (
<a rel="noopener" target="_blank" {...props} />
) : (
<div {...props} />
);
}
}
export default Button;

@ -0,0 +1,63 @@
@import "~common/stylesheet/index";
.code_editor {
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
.ace_editor {
flex: 1;
width: 100% !important;
height: 100% !important;
min-width: 0 !important;
min-height: 0 !important;
.current_line_marker {
background-color: rgba($color-highlight, 0.4);
border: 1px solid $color-highlight;
position: absolute;
width: 100% !important;
animation: line_highlight .1s;
}
@keyframes line_highlight {
from {
background-color: rgba($color-highlight, 0.1);
}
to {
background-color: rgba($color-highlight, 0.4);
}
}
}
.contributors_viewer {
display: flex;
flex-wrap: wrap;
align-items: center;
padding: 4px;
background-color: $theme-normal;
.contributor {
height: 28px;
padding: 0 6px;
font-weight: bold;
&.label {
display: flex;
align-items: center;
white-space: nowrap;
}
}
.empty {
display: flex;
flex: 1;
}
.delete {
height: $line-height;
}
}
}

@ -0,0 +1,83 @@
import React from 'react';
import faTrashAlt from '@fortawesome/fontawesome-free-solid/faTrashAlt';
import faUser from '@fortawesome/fontawesome-free-solid/faUser';
import { classes, extension } from 'common/util';
import { actions } from 'reducers';
import { connect } from 'react-redux';
import { languages } from 'common/config';
import { Button, Ellipsis, FoldableAceEditor } from 'components';
import styles from './CodeEditor.module.scss';
class CodeEditor extends React.Component {
constructor(props) {
super(props);
this.aceEditorRef = React.createRef();
}
handleResize() {
this.aceEditorRef.current.resize();
}
render() {
const { className } = this.props;
const { editingFile } = this.props.current;
const { user } = this.props.env;
const { lineIndicator } = this.props.player;
if (!editingFile) return null;
const fileExt = extension(editingFile.name);
const language = languages.find(language => language.ext === fileExt);
const mode = language ? language.mode :
fileExt === 'md' ? 'markdown' :
fileExt === 'json' ? 'json' :
'plain_text';
return (
<div className={classes(styles.code_editor, className)}>
<FoldableAceEditor
className={styles.ace_editor}
ref={this.aceEditorRef}
mode={mode}
theme="tomorrow_night_eighties"
name="code_editor"
editorProps={{ $blockScrolling: true }}
onChange={code => this.props.modifyFile(editingFile, code)}
markers={lineIndicator ? [{
startRow: lineIndicator.lineNumber,
startCol: 0,
endRow: lineIndicator.lineNumber,
endCol: Infinity,
className: styles.current_line_marker,
type: 'line',
inFront: true,
_key: lineIndicator.cursor,
}] : []}
value={editingFile.content}/>
<div className={classes(styles.contributors_viewer, className)}>
{/* <span className={classes(styles.contributor, styles.label)}>Contributed by</span>
{
(editingFile.contributors || [user || { login: 'guest', avatar_url: faUser }]).map(contributor => (
<Button className={styles.contributor} icon={contributor.avatar_url} key={contributor.login}
href={`https://github.com/${contributor.login}`}>
{contributor.login}
</Button>
))
} */}
<div className={styles.empty}>
<div className={styles.empty}/>
<Button className={styles.delete} icon={faTrashAlt} primary confirmNeeded
onClick={() => this.props.deleteFile(editingFile)}>
<Ellipsis>Delete File</Ellipsis>
</Button>
</div>
</div>
</div>
);
}
}
export default connect(({ current, env, player }) => ({ current, env, player }), actions, null, { forwardRef: true })(
CodeEditor,
);

@ -0,0 +1,38 @@
@import "~common/stylesheet/index";
.divider {
position: relative;
z-index: 97;
&:after {
position: absolute;
background-color: $theme-light;
content: '';
}
&.horizontal {
width: 7px;
margin: 0 -3px;
cursor: ew-resize;
&:after {
top: 0;
bottom: 0;
left: 3px;
width: 1px;
}
}
&.vertical {
height: 7px;
margin: -3px 0;
cursor: ns-resize;
&:after {
left: 0;
right: 0;
top: 3px;
height: 1px;
}
}
}

@ -0,0 +1,40 @@
import React from 'react';
import { classes } from 'common/util';
import styles from './Divider.module.scss';
class Divider extends React.Component {
constructor(props) {
super(props);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
}
handleMouseDown(e) {
this.target = e.target;
document.addEventListener('mousemove', this.handleMouseMove);
document.addEventListener('mouseup', this.handleMouseUp);
}
handleMouseMove(e) {
const { onResize } = this.props;
if (onResize) onResize(this.target.parentElement, e.clientX, e.clientY);
}
handleMouseUp(e) {
document.removeEventListener('mousemove', this.handleMouseMove);
document.removeEventListener('mouseup', this.handleMouseUp);
}
render() {
const { className, horizontal } = this.props;
return (
<div className={classes(styles.divider, horizontal ? styles.horizontal : styles.vertical, className)}
onMouseDown={this.handleMouseDown} />
);
}
}
export default Divider;

@ -0,0 +1,7 @@
@import "~common/stylesheet/index";
.ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

@ -0,0 +1,18 @@
import React from 'react';
import styles from './Ellipsis.module.scss';
import { classes } from 'common/util';
class Ellipsis extends React.Component {
render() {
const { className, children } = this.props;
return (
<span className={classes(styles.ellipsis, className)}>
{children}
</span>
);
}
}
export default Ellipsis;

@ -0,0 +1,14 @@
@import "~common/stylesheet/index";
.category {
justify-content: space-between;
.icon {
margin-left: 4px;
}
}
.expandable_list_item {
background-color: $color-shadow;
border-bottom: 1px solid $theme-dark;
}

@ -0,0 +1,29 @@
import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import faCaretDown from '@fortawesome/fontawesome-free-solid/faCaretDown';
import faCaretRight from '@fortawesome/fontawesome-free-solid/faCaretRight';
import styles from './ExpandableListItem.module.scss';
import { ListItem } from 'components';
import { classes } from 'common/util';
class ExpandableListItem extends React.Component {
render() {
const { className, children, opened, ...props } = this.props;
return opened ? (
<div className={classes(styles.expandable_list_item, className)}>
<ListItem className={styles.category} {...props}>
<FontAwesomeIcon className={styles.icon} fixedWidth icon={faCaretDown} />
</ListItem>
{children}
</div>
) : (
<ListItem className={classes(styles.category, className)} {...props}>
<FontAwesomeIcon className={styles.icon} fixedWidth icon={faCaretRight} />
</ListItem>
);
}
}
export default ExpandableListItem;

@ -0,0 +1,49 @@
import { connect } from 'react-redux';
import AceEditor from 'react-ace';
import 'brace/mode/plain_text';
import 'brace/mode/markdown';
import 'brace/mode/json';
import 'brace/mode/javascript';
import 'brace/mode/c_cpp';
import 'brace/mode/java';
import 'brace/theme/tomorrow_night_eighties';
import 'brace/ext/searchbox';
import { actions } from 'reducers';
class FoldableAceEditor extends AceEditor {
componentDidMount() {
super.componentDidMount();
const { shouldBuild } = this.props.current;
if (shouldBuild) this.foldTracers();
}
componentDidUpdate(prevProps, prevState, snapshot) {
super.componentDidUpdate(prevProps, prevState, snapshot);
const { editingFile, shouldBuild } = this.props.current;
if (editingFile !== prevProps.current.editingFile) {
if (shouldBuild) this.foldTracers();
}
}
foldTracers() {
const session = this.editor.getSession();
for (let row = 0; row < session.getLength(); row++) {
if (!/^\s*\/\/.+{\s*$/.test(session.getLine(row))) continue;
const range = session.getFoldWidgetRange(row);
if (range) {
session.addFold('...', range);
row = range.end.row;
}
}
}
resize() {
this.editor.resize();
}
}
export default connect(({ current }) => ({ current }), actions, null, { forwardRef: true })(
FoldableAceEditor,
);

@ -0,0 +1,68 @@
@import "~common/stylesheet/index";
.header {
display: flex;
flex-direction: column;
min-width: 0;
.row {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 0 16px;
border-bottom: 1px solid $theme-light;
.section {
height: $line-height;
display: flex;
align-items: stretch;
.title_bar {
font-size: $font-size-large;
font-weight: bold;
min-width: 0;
.nav_arrow {
margin: 0 4px;
}
.nav_caret {
margin-left: 4px;
}
.input_title {
padding: 4px 8px;
background-color: $theme-light;
}
}
.btn_dropdown {
position: relative;
font-weight: bold;
&:active {
box-shadow: none;
}
.dropdown {
z-index: 98;
position: absolute;
left: 0;
top: 0;
display: none;
flex-direction: column;
align-items: stretch;
box-shadow: 0 0 8px $color-shadow;
background-color: $theme-light;
margin-top: $line-height;
}
&:hover {
.dropdown {
display: flex;
}
}
}
}
}
}

@ -0,0 +1,188 @@
import React from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import AutosizeInput from 'react-input-autosize';
import screenfull from 'screenfull';
import Promise from 'bluebird';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import faAngleRight from '@fortawesome/fontawesome-free-solid/faAngleRight';
import faCaretDown from '@fortawesome/fontawesome-free-solid/faCaretDown';
import faCaretRight from '@fortawesome/fontawesome-free-solid/faCaretRight';
import faCodeBranch from '@fortawesome/fontawesome-free-solid/faCodeBranch';
import faExpandArrowsAlt from '@fortawesome/fontawesome-free-solid/faExpandArrowsAlt';
import faGithub from '@fortawesome/fontawesome-free-brands/faGithub';
import faTrashAlt from '@fortawesome/fontawesome-free-solid/faTrashAlt';
import faSave from '@fortawesome/fontawesome-free-solid/faSave';
import faFacebook from '@fortawesome/fontawesome-free-brands/faFacebook';
import faStar from '@fortawesome/fontawesome-free-solid/faStar';
import { GitHubApi } from 'apis';
import { classes, refineGist } from 'common/util';
import { actions } from 'reducers';
import { languages } from 'common/config';
import { BaseComponent, Button, Ellipsis, ListItem, Player } from 'components';
import styles from './Header.module.scss';
class Header extends BaseComponent {
handleClickFullScreen() {
if (screenfull.enabled) {
if (screenfull.isFullscreen) {
screenfull.exit();
} else {
screenfull.request();
}
}
}
handleChangeTitle(e) {
const { value } = e.target;
this.props.modifyTitle(value);
}
saveGist() {
const { user } = this.props.env;
const { scratchPaper, titles, files, lastFiles, editingFile } = this.props.current;
const gist = {
description: titles[titles.length - 1],
files: {},
};
files.forEach(file => {
gist.files[file.name] = {
content: file.content,
};
});
lastFiles.forEach(lastFile => {
if (!(lastFile.name in gist.files)) {
gist.files[lastFile.name] = null;
}
});
gist.files['algorithm-visualizer'] = {
content: 'https://algorithm-visualizer.org/',
};
const save = gist => {
if (!user) return Promise.reject(new Error('Sign In Required'));
if (scratchPaper && scratchPaper.login) {
if (scratchPaper.login === user.login) {
return GitHubApi.editGist(scratchPaper.gistId, gist);
} else {
return GitHubApi.forkGist(scratchPaper.gistId).then(forkedGist => GitHubApi.editGist(forkedGist.id, gist));
}
}
return GitHubApi.createGist(gist);
};
save(gist)
.then(refineGist)
.then(newScratchPaper => {
this.props.setScratchPaper(newScratchPaper);
this.props.setEditingFile(newScratchPaper.files.find(file => file.name === editingFile.name));
if (!(scratchPaper && scratchPaper.gistId === newScratchPaper.gistId)) {
this.props.history.push(`/scratch-paper/${newScratchPaper.gistId}`);
}
})
.then(this.props.loadScratchPapers)
.catch(this.handleError);
}
hasPermission() {
const { scratchPaper } = this.props.current;
const { user } = this.props.env;
if (!scratchPaper) return false;
if (scratchPaper.gistId !== 'new') {
if (!user) return false;
if (scratchPaper.login !== user.login) return false;
}
return true;
}
deleteGist() {
const { scratchPaper } = this.props.current;
const { gistId } = scratchPaper;
if (gistId === 'new') {
this.props.ignoreHistoryBlock(() => this.props.history.push('/'));
} else {
GitHubApi.deleteGist(gistId)
.then(() => {
this.props.ignoreHistoryBlock(() => this.props.history.push('/'));
})
.then(this.props.loadScratchPapers)
.catch(this.handleError);
}
}
render() {
const { className, onClickTitleBar, navigatorOpened } = this.props;
const { scratchPaper, titles, saved } = this.props.current;
const { ext, user } = this.props.env;
const permitted = this.hasPermission();
return (
<header className={classes(styles.header, className)}>
<div className={styles.row}>
<div className={styles.section}>
<Button className={styles.title_bar} onClick={onClickTitleBar}>
{/* {
titles.map((title, i) => [
scratchPaper && i === 1 ?
<AutosizeInput className={styles.input_title} key={`title-${i}`} value={title}
onClick={e => e.stopPropagation()} onChange={e => this.handleChangeTitle(e)}/> :
<Ellipsis key={`title-${i}`}>{title}</Ellipsis>,
i < titles.length - 1 &&
<FontAwesomeIcon className={styles.nav_arrow} fixedWidth icon={faAngleRight} key={`arrow-${i}`}/>,
])
} */}
<FontAwesomeIcon className={styles.nav_caret} fixedWidth
icon={navigatorOpened ? faCaretDown : faCaretRight}/>
</Button>
</div>
<div className={styles.section}>
{/* <Button icon={permitted ? faSave : faCodeBranch} primary disabled={permitted && saved}
onClick={() => this.saveGist()}>{permitted ? 'Save' : 'Fork'}</Button>
{
permitted &&
<Button icon={faTrashAlt} primary onClick={() => this.deleteGist()} confirmNeeded>Delete</Button>
} */}
{/* <Button icon={faFacebook} primary
href={`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(window.location.href)}`}>Share</Button> */}
<Button icon={faExpandArrowsAlt} primary
onClick={() => this.handleClickFullScreen()}>Fullscreen</Button>
</div>
</div>
<div className={styles.row}>
<div className={styles.section}>
{/* {
user ?
<Button className={styles.btn_dropdown} icon={user.avatar_url}>
{user.login}
<div className={styles.dropdown}>
<ListItem label="Sign Out" href="/api/auth/destroy" rel="opener"/>
</div>
</Button> :
<Button icon={faGithub} primary href="/api/auth/request" rel="opener">
<Ellipsis>Sign In</Ellipsis>
</Button>
} */}
<Button className={styles.btn_dropdown} icon={faStar}>
{languages.find(language => language.ext === ext).name}
<div className={styles.dropdown}>
{
languages.map(language => language.ext === ext ? null : (
<ListItem key={language.ext} onClick={() => this.props.setExt(language.ext)}
label={language.name}/>
))
}
</div>
</Button>
</div>
<Player className={styles.section}/>
</div>
</header>
);
}
}
export default withRouter(
connect(({ current, env }) => ({ current, env }), actions)(
Header,
),
);

@ -0,0 +1,13 @@
@import "~common/stylesheet/index";
.list_item {
height: $line-height;
.label {
flex: 1;
}
&.indent {
padding-left: 24px;
}
}

@ -0,0 +1,20 @@
import React from 'react';
import styles from './ListItem.module.scss';
import { classes } from 'common/util';
import { Button, Ellipsis } from 'components';
class ListItem extends React.Component {
render() {
const { className, children, indent, label, ...props } = this.props;
return (
<Button className={classes(styles.list_item, indent && styles.indent, className)} {...props}>
<Ellipsis className={styles.label}>{label}</Ellipsis>
{children}
</Button>
);
}
}
export default ListItem;

@ -0,0 +1,41 @@
@import "~common/stylesheet/index";
.navigator {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
.search_bar_container {
height: $line-height;
padding: 0 8px;
display: flex;
align-items: stretch;
border-bottom: 1px solid $theme-light;
&:focus-within {
background-color: $color-overlay;
}
.search_icon {
align-self: center;
margin-right: 8px;
}
.search_bar {
flex: 1;
box-sizing: border-box;
}
}
.algorithm_list {
flex: 1;
overflow-y: auto;
}
.footer {
max-height: 30vh;
border-top: 1px solid $theme-light;
overflow-y: auto;
}
}

@ -0,0 +1,136 @@
import React from 'react';
import { connect } from 'react-redux';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import faSearch from '@fortawesome/fontawesome-free-solid/faSearch';
import faCode from '@fortawesome/fontawesome-free-solid/faCode';
import faBook from '@fortawesome/fontawesome-free-solid/faBook';
import faGithub from '@fortawesome/fontawesome-free-brands/faGithub';
import { ExpandableListItem, ListItem } from 'components';
import { classes } from 'common/util';
import { actions } from 'reducers';
import styles from './Navigator.module.scss';
class Navigator extends React.Component {
constructor(props) {
super(props);
this.state = {
categoriesOpened: {},
scratchPaperOpened: true,
query: '',
};
}
componentDidMount() {
const { algorithm } = this.props.current;
if (algorithm) {
this.toggleCategory(algorithm.categoryKey, true);
}
}
componentWillReceiveProps(nextProps) {
const { algorithm } = nextProps.current;
if (algorithm) {
this.toggleCategory(algorithm.categoryKey, true);
}
}
toggleCategory(key, categoryOpened = !this.state.categoriesOpened[key]) {
const categoriesOpened = {
...this.state.categoriesOpened,
[key]: categoryOpened,
};
this.setState({ categoriesOpened });
}
toggleScratchPaper(scratchPaperOpened = !this.state.scratchPaperOpened) {
this.setState({ scratchPaperOpened });
}
handleChangeQuery(e) {
const { categories } = this.props.directory;
const categoriesOpened = {};
const query = e.target.value;
categories.forEach(category => {
if (this.testQuery(category.name) || category.algorithms.find(algorithm => this.testQuery(algorithm.name))) {
categoriesOpened[category.key] = true;
}
});
this.setState({ categoriesOpened, query });
}
testQuery(value) {
const { query } = this.state;
const refine = string => string.replace(/-/g, ' ').replace(/[^\w ]/g, '');
const refinedQuery = refine(query);
const refinedValue = refine(value);
return new RegExp(`(^| )${refinedQuery}`, 'i').test(refinedValue) ||
new RegExp(refinedQuery, 'i').test(refinedValue.split(' ').map(v => v && v[0]).join(''));
}
render() {
const { categoriesOpened, scratchPaperOpened, query } = this.state;
const { className } = this.props;
const { categories, scratchPapers } = this.props.directory;
const { algorithm, scratchPaper } = this.props.current;
const categoryKey = algorithm && algorithm.categoryKey;
const algorithmKey = algorithm && algorithm.algorithmKey;
const gistId = scratchPaper && scratchPaper.gistId;
return (
<nav className={classes(styles.navigator, className)}>
<div className={styles.search_bar_container}>
<FontAwesomeIcon fixedWidth icon={faSearch} className={styles.search_icon}/>
<input type="text" className={styles.search_bar} aria-label="Search" placeholder="Search ..." autoFocus
value={query} onChange={e => this.handleChangeQuery(e)}/>
</div>
<div className={styles.algorithm_list}>
{
categories.map(category => {
const categoryOpened = categoriesOpened[category.key];
let algorithms = category.algorithms;
if (!this.testQuery(category.name)) {
algorithms = algorithms.filter(algorithm => this.testQuery(algorithm.name));
if (!algorithms.length) return null;
}
return (
<ExpandableListItem key={category.key} onClick={() => this.toggleCategory(category.key)}
label={category.name}
opened={categoryOpened}>
{
algorithms.map(algorithm => (
<ListItem indent key={algorithm.key}
selected={category.key === categoryKey && algorithm.key === algorithmKey}
to={`/${category.key}/${algorithm.key}`} label={algorithm.name}/>
))
}
</ExpandableListItem>
);
})
}
</div>
<div className={styles.footer}>
<ExpandableListItem icon={faCode} label="Scratch Paper" onClick={() => this.toggleScratchPaper()}
opened={scratchPaperOpened}>
<ListItem indent label="New ..." to="/scratch-paper/new"/>
{
scratchPapers.map(scratchPaper => (
<ListItem indent key={scratchPaper.key} selected={scratchPaper.key === gistId}
to={`/scratch-paper/${scratchPaper.key}`} label={scratchPaper.name}/>
))
}
</ExpandableListItem>
{/* <ListItem icon={faBook} label="API Reference"
href="https://github.com/algorithm-visualizer/algorithm-visualizer/wiki"/>
<ListItem icon={faGithub} label="Fork me on GitHub"
href="https://github.com/algorithm-visualizer/algorithm-visualizer"/> */}
</div>
</nav>
);
}
}
export default connect(({ current, directory, env }) => ({ current, directory, env }), actions)(
Navigator,
);

@ -0,0 +1,52 @@
@import "~common/stylesheet/index";
.player {
.progress_bar {
width: 160px;
}
.speed {
display: flex;
align-items: center;
padding: 0 12px;
white-space: nowrap;
&:hover {
background-color: $color-shadow;
}
.range {
position: relative;
height: 16px;
width: 60px;
margin-left: 8px;
.range_label_container {
display: none;
}
.range_track {
top: 50%;
height: 6px;
margin-top: -3px;
background-color: $theme-light;
cursor: pointer;
display: block;
position: relative;
}
.range_slider {
top: 0;
width: 6px;
height: 12px;
margin-left: -3px;
margin-top: -3px;
appearance: none;
background-color: $color-font;
cursor: pointer;
display: block;
position: absolute;
}
}
}
}

@ -0,0 +1,188 @@
import React from 'react';
import { connect } from 'react-redux';
import InputRange from 'react-input-range';
import axios from 'axios';
import faPlay from '@fortawesome/fontawesome-free-solid/faPlay';
import faChevronLeft from '@fortawesome/fontawesome-free-solid/faChevronLeft';
import faChevronRight from '@fortawesome/fontawesome-free-solid/faChevronRight';
import faPause from '@fortawesome/fontawesome-free-solid/faPause';
import faWrench from '@fortawesome/fontawesome-free-solid/faWrench';
import { classes, extension } from 'common/util';
import { TracerApi } from 'apis';
import { actions } from 'reducers';
import { BaseComponent, Button, ProgressBar } from 'components';
import styles from './Player.module.scss';
class Player extends BaseComponent {
constructor(props) {
super(props);
this.state = {
speed: 2,
playing: false,
building: false,
};
this.tracerApiSource = null;
this.reset();
}
componentDidMount() {
const { editingFile, shouldBuild } = this.props.current;
if (shouldBuild) this.build(editingFile);
}
componentWillReceiveProps(nextProps) {
const { editingFile, shouldBuild } = nextProps.current;
if (editingFile !== this.props.current.editingFile) {
if (shouldBuild) this.build(editingFile);
}
}
reset(commands = []) {
const chunks = [{
commands: [],
lineNumber: undefined,
}];
while (commands.length) {
const command = commands.shift();
const { key, method, args } = command;
if (key === null && method === 'delay') {
const [lineNumber] = args;
chunks[chunks.length - 1].lineNumber = lineNumber;
chunks.push({
commands: [],
lineNumber: undefined,
});
} else {
chunks[chunks.length - 1].commands.push(command);
}
}
this.props.setChunks(chunks);
this.props.setCursor(0);
this.pause();
this.props.setLineIndicator(undefined);
}
build(file) {
this.reset();
if (!file) return;
if (this.tracerApiSource) this.tracerApiSource.cancel();
this.tracerApiSource = axios.CancelToken.source();
this.setState({ building: true });
const ext = extension(file.name);
if (ext in TracerApi) {
TracerApi[ext]({ code: file.content }, undefined, this.tracerApiSource.token)
.then(commands => {
this.tracerApiSource = null;
this.setState({ building: false });
this.reset(commands);
this.next();
})
.catch(error => {
if (axios.isCancel(error)) return;
this.tracerApiSource = null;
this.setState({ building: false });
this.handleError(error);
});
} else {
this.setState({ building: false });
this.handleError(new Error('Language Not Supported'));
}
}
isValidCursor(cursor) {
const { chunks } = this.props.player;
return 1 <= cursor && cursor <= chunks.length;
}
prev() {
this.pause();
const cursor = this.props.player.cursor - 1;
if (!this.isValidCursor(cursor)) return false;
this.props.setCursor(cursor);
return true;
}
resume(wrap = false) {
this.pause();
if (this.next() || (wrap && this.props.setCursor(1))) {
const interval = 4000 / Math.pow(Math.E, this.state.speed);
this.timer = window.setTimeout(() => this.resume(), interval);
this.setState({ playing: true });
}
}
pause() {
if (this.timer) {
window.clearTimeout(this.timer);
this.timer = undefined;
this.setState({ playing: false });
}
}
next() {
this.pause();
const cursor = this.props.player.cursor + 1;
if (!this.isValidCursor(cursor)) return false;
this.props.setCursor(cursor);
return true;
}
handleChangeSpeed(speed) {
this.setState({ speed });
}
handleChangeProgress(progress) {
const { chunks } = this.props.player;
const cursor = Math.max(1, Math.min(chunks.length, Math.round(progress * chunks.length)));
this.pause();
this.props.setCursor(cursor);
}
render() {
const { className } = this.props;
const { editingFile } = this.props.current;
const { chunks, cursor } = this.props.player;
const { speed, playing, building } = this.state;
return (
<div className={classes(styles.player, className)}>
<Button icon={faWrench} primary disabled={building} inProgress={building}
onClick={() => this.build(editingFile)}>
{building ? 'Building' : 'Build'}
</Button>
{
playing ? (
<Button icon={faPause} primary active onClick={() => this.pause()}>Pause</Button>
) : (
<Button icon={faPlay} primary onClick={() => this.resume(true)}>Play</Button>
)
}
<Button icon={faChevronLeft} primary disabled={!this.isValidCursor(cursor - 1)} onClick={() => this.prev()}/>
<ProgressBar className={styles.progress_bar} current={cursor} total={chunks.length}
onChangeProgress={progress => this.handleChangeProgress(progress)}/>
<Button icon={faChevronRight} reverse primary disabled={!this.isValidCursor(cursor + 1)}
onClick={() => this.next()}/>
<div className={styles.speed}>
Speed
<InputRange
classNames={{
inputRange: styles.range,
labelContainer: styles.range_label_container,
slider: styles.range_slider,
track: styles.range_track,
}} minValue={0} maxValue={4} step={.5} value={speed}
onChange={speed => this.handleChangeSpeed(speed)}/>
</div>
</div>
);
}
}
export default connect(({ current, player }) => ({ current, player }), actions)(
Player,
);

@ -0,0 +1,31 @@
@import "~common/stylesheet/index";
.progress_bar {
display: flex;
align-items: center;
justify-content: center;
position: relative;
background-color: $theme-light;
cursor: pointer;
pointer-events: auto;
> * {
pointer-events: none;
}
.active {
position: absolute;
height: 100%;
left: 0;
background-color: $color-active;
}
.label {
position: absolute;
color: $theme-dark;
.current {
font-weight: bold;
}
}
}

@ -0,0 +1,48 @@
import React from 'react';
import { classes } from 'common/util';
import styles from './ProgressBar.module.scss';
class ProgressBar extends React.Component {
constructor(props) {
super(props);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
}
handleMouseDown(e) {
this.target = e.target;
this.handleMouseMove(e);
document.addEventListener('mousemove', this.handleMouseMove);
document.addEventListener('mouseup', this.handleMouseUp);
}
handleMouseMove(e) {
const { left } = this.target.getBoundingClientRect();
const { offsetWidth } = this.target;
const { onChangeProgress } = this.props;
const progress = (e.clientX - left) / offsetWidth;
if (onChangeProgress) onChangeProgress(progress);
}
handleMouseUp(e) {
document.removeEventListener('mousemove', this.handleMouseMove);
document.removeEventListener('mouseup', this.handleMouseUp);
}
render() {
const { className, total, current } = this.props;
return (
<div className={classes(styles.progress_bar, className)} onMouseDown={this.handleMouseDown}>
<div className={styles.active} style={{ width: `${current / total * 100}%` }} />
<div className={styles.label}>
<span className={styles.current}>{current}</span> / {total}
</div>
</div>
);
}
}
export default ProgressBar;

@ -0,0 +1,26 @@
@import "~common/stylesheet/index";
.resizable_container {
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
min-width: 0;
min-height: 0;
&.horizontal {
flex-direction: row;
}
.wrapper {
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
overflow: hidden;
&.horizontal {
flex-direction: row;
}
}
}

@ -0,0 +1,67 @@
import React from 'react';
import { classes } from 'common/util';
import { Divider } from 'components';
import styles from './ResizableContainer.module.scss';
class ResizableContainer extends React.Component {
handleResize(prevIndex, index, targetElement, clientX, clientY) {
const { horizontal, visibles, onChangeWeights } = this.props;
const weights = [...this.props.weights];
const { left, top } = targetElement.getBoundingClientRect();
const { offsetWidth, offsetHeight } = targetElement.parentElement;
const position = horizontal ? clientX - left : clientY - top;
const containerSize = horizontal ? offsetWidth : offsetHeight;
let totalWeight = 0;
let subtotalWeight = 0;
weights.forEach((weight, i) => {
if (visibles && !visibles[i]) return;
totalWeight += weight;
if (i < index) subtotalWeight += weight;
});
const newWeight = position / containerSize * totalWeight;
let deltaWeight = newWeight - subtotalWeight;
deltaWeight = Math.max(deltaWeight, -weights[prevIndex]);
deltaWeight = Math.min(deltaWeight, weights[index]);
weights[prevIndex] += deltaWeight;
weights[index] -= deltaWeight;
onChangeWeights(weights);
}
render() {
const { className, children, horizontal, weights, visibles } = this.props;
const elements = [];
let lastIndex = -1;
const totalWeight = weights.filter((weight, i) => !visibles || visibles[i])
.reduce((sumWeight, weight) => sumWeight + weight, 0);
children.forEach((child, i) => {
if (!visibles || visibles[i]) {
if (~lastIndex) {
const prevIndex = lastIndex;
elements.push(
<Divider key={`divider-${i}`} horizontal={horizontal}
onResize={((target, dx, dy) => this.handleResize(prevIndex, i, target, dx, dy))} />,
);
}
elements.push(
<div key={i} className={classes(styles.wrapper)} style={{
flexGrow: weights[i] / totalWeight,
}}>
{child}
</div>,
);
lastIndex = i;
}
});
return (
<div className={classes(styles.resizable_container, horizontal && styles.horizontal, className)}>
{elements}
</div>
);
}
}
export default ResizableContainer;

@ -0,0 +1,70 @@
@import "~common/stylesheet/index";
.tab_container {
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
min-width: 0;
min-height: 0;
position: relative;
.tab_bar {
display: flex;
align-items: stretch;
height: $line-height;
overflow-x: auto;
white-space: nowrap;
flex-shrink: 0;
.title {
display: flex;
align-items: center;
cursor: pointer;
padding: 0 12px;
margin: 0;
border-bottom: 1px solid $theme-light;
.input_title {
input {
&:hover,
&:focus {
margin: -4px;
padding: 4px;
background-color: $theme-normal;
}
}
}
&.selected {
border-left: 1px solid $theme-light;
border-right: 1px solid $theme-light;
margin: 0 -1px;
border-bottom: none;
background-color: $theme-dark;
}
&.fake {
pointer-events: none;
&:first-child {
flex-shrink: 0;
width: $line-height / 2;
}
&:last-child {
flex: 1;
}
}
}
}
.content {
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
background-color: $theme-dark;
overflow: hidden;
}
}

@ -0,0 +1,59 @@
import React from 'react';
import { connect } from 'react-redux';
import AutosizeInput from 'react-input-autosize';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import faPlus from '@fortawesome/fontawesome-free-solid/faPlus';
import { classes } from 'common/util';
import { actions } from 'reducers';
import { languages } from 'common/config';
import styles from './TabContainer.module.scss';
class TabContainer extends React.Component {
handleAddFile() {
const { ext } = this.props.env;
const { files } = this.props.current;
const language = languages.find(language => language.ext === ext);
const newFile = { ...language.skeleton };
let count = 0;
while (files.some(file => file.name === newFile.name)) newFile.name = `code-${++count}.${ext}`;
this.props.addFile(newFile);
}
render() {
const { className, children } = this.props;
const { editingFile, files } = this.props.current;
return (
<div className={classes(styles.tab_container, className)}>
<div className={styles.tab_bar}>
<div className={classes(styles.title, styles.fake)}/>
{
files.map((file, i) => file === editingFile ? (
<div className={classes(styles.title, styles.selected)} key={i}
onClick={() => this.props.setEditingFile(file)}>
<AutosizeInput className={styles.input_title} value={file.name}
onClick={e => e.stopPropagation()}
onChange={e => this.props.renameFile(file, e.target.value)}/>
</div>
) : (
<div className={styles.title} key={i} onClick={() => this.props.setEditingFile(file)}>
{file.name}
</div>
))
}
<div className={styles.title} onClick={() => this.handleAddFile()}>
<FontAwesomeIcon fixedWidth icon={faPlus}/>
</div>
<div className={classes(styles.title, styles.fake)}/>
</div>
<div className={styles.content}>
{children}
</div>
</div>
);
}
}
export default connect(({ current, env }) => ({ current, env }), actions)(
TabContainer,
);

@ -0,0 +1,29 @@
@import "~common/stylesheet/index";
.toast_container {
display: flex;
flex-direction: column-reverse;
padding: 16px;
pointer-events: none;
}
.toast {
width: 280px;
border: 1px solid;
border-radius: 4px;
padding: 16px;
margin: 8px;
white-space: pre-wrap;
pointer-events: auto;
font-family: $font-family-monospace;
&.success {
border-color: rgb(0, 150, 0);
background-color: rgba(0, 120, 0, .8);
}
&.error {
border-color: rgb(150, 0, 0);
background-color: rgba(120, 0, 0, .8);
}
}

@ -0,0 +1,36 @@
import React from 'react';
import { connect } from 'react-redux';
import { actions } from 'reducers';
import { classes } from 'common/util';
import styles from './ToastContainer.module.scss';
class ToastContainer extends React.Component {
componentWillReceiveProps(nextProps) {
const newToasts = nextProps.toast.toasts.filter(toast => !this.props.toast.toasts.includes(toast));
newToasts.forEach(toast => {
window.setTimeout(() => this.props.hideToast(toast.id), 3000);
});
}
render() {
const { className } = this.props;
const { toasts } = this.props.toast;
return (
<div className={classes(styles.toast_container, className)}>
{
toasts.map(toast => (
<div className={classes(styles.toast, styles[toast.type])} key={toast.id}>
{toast.message}
</div>
))
}
</div>
);
}
}
export default connect(({ toast }) => ({ toast }), actions)(
ToastContainer,
);

@ -0,0 +1,9 @@
@import "~common/stylesheet/index";
.visualization_viewer {
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
min-height: 0;
}

@ -0,0 +1,97 @@
import React from 'react';
import { connect } from 'react-redux';
import { BaseComponent } from 'components';
import { actions } from 'reducers';
import styles from './VisualizationViewer.module.scss';
import * as TracerClasses from 'core/tracers';
import * as LayoutClasses from 'core/layouts';
import { classes } from 'common/util';
class VisualizationViewer extends BaseComponent {
constructor(props) {
super(props);
this.reset();
}
reset() {
this.root = null;
this.objects = {};
}
componentDidMount() {
const { chunks, cursor } = this.props.player;
this.update(chunks, cursor);
}
componentWillReceiveProps(nextProps) {
const { chunks, cursor } = nextProps.player;
const { chunks: oldChunks, cursor: oldCursor } = this.props.player;
if (chunks !== oldChunks || cursor !== oldCursor) {
this.update(chunks, cursor, oldChunks, oldCursor);
}
}
update(chunks, cursor, oldChunks = [], oldCursor = 0) {
let applyingChunks;
if (cursor > oldCursor) {
applyingChunks = chunks.slice(oldCursor, cursor);
} else {
this.reset();
applyingChunks = chunks.slice(0, cursor);
}
applyingChunks.forEach(chunk => this.applyChunk(chunk));
const lastChunk = applyingChunks[applyingChunks.length - 1];
if (lastChunk && lastChunk.lineNumber !== undefined) {
this.props.setLineIndicator({ lineNumber: lastChunk.lineNumber, cursor });
} else {
this.props.setLineIndicator(undefined);
}
}
applyCommand(command) {
const { key, method, args } = command;
try {
if (key === null && method === 'setRoot') {
const [root] = args;
this.root = this.objects[root];
} else if (method === 'destroy') {
delete this.objects[key];
} else if (method in LayoutClasses) {
const [children] = args;
const LayoutClass = LayoutClasses[method];
this.objects[key] = new LayoutClass(key, key => this.objects[key], children);
} else if (method in TracerClasses) {
const className = method;
const [title = className] = args;
const TracerClass = TracerClasses[className];
this.objects[key] = new TracerClass(key, key => this.objects[key], title);
} else {
this.objects[key][method](...args);
}
} catch (error) {
this.handleError(error);
}
}
applyChunk(chunk) {
chunk.commands.forEach(command => this.applyCommand(command));
}
render() {
const { className } = this.props;
return (
<div className={classes(styles.visualization_viewer, className)}>
{
this.root && this.root.render()
}
</div>
);
}
}
export default connect(({ player }) => ({ player }), actions)(
VisualizationViewer,
);

@ -0,0 +1,17 @@
export { default as App } from './App';
export { default as BaseComponent } from './BaseComponent';
export { default as Button } from './Button';
export { default as CodeEditor } from './CodeEditor';
export { default as Divider } from './Divider';
export { default as Ellipsis } from './Ellipsis';
export { default as ExpandableListItem } from './ExpandableListItem';
export { default as FoldableAceEditor } from './FoldableAceEditor';
export { default as Header } from './Header';
export { default as ListItem } from './ListItem';
export { default as Navigator } from './Navigator';
export { default as Player } from './Player';
export { default as ProgressBar } from './ProgressBar';
export { default as ResizableContainer } from './ResizableContainer';
export { default as TabContainer } from './TabContainer';
export { default as ToastContainer } from './ToastContainer';
export { default as VisualizationViewer } from './VisualizationViewer';

@ -0,0 +1,6 @@
import { Layout } from 'core/layouts';
class HorizontalLayout extends Layout {
}
export default HorizontalLayout;

@ -0,0 +1,55 @@
import React from 'react';
import { ResizableContainer } from 'components';
import { HorizontalLayout } from 'core/layouts';
class Layout {
constructor(key, getObject, children) {
this.key = key;
this.getObject = getObject;
this.children = children.map(key => this.getObject(key));
this.weights = children.map(() => 1);
this.ref = React.createRef();
this.handleChangeWeights = this.handleChangeWeights.bind(this);
}
add(key, index = this.children.length) {
const child = this.getObject(key);
this.children.splice(index, 0, child);
this.weights.splice(index, 0, 1);
}
remove(key) {
const child = this.getObject(key);
const index = this.children.indexOf(child);
if (~index) {
this.children.splice(index, 1);
this.weights.splice(index, 1);
}
}
removeAll() {
this.children = [];
this.weights = [];
}
handleChangeWeights(weights) {
this.weights.splice(0, this.weights.length, ...weights);
this.ref.current.forceUpdate();
}
render() {
const horizontal = this instanceof HorizontalLayout;
return (
<ResizableContainer key={this.key} ref={this.ref} weights={this.weights} horizontal={horizontal}
onChangeWeights={this.handleChangeWeights}>
{
this.children.map(tracer => tracer && tracer.render())
}
</ResizableContainer>
);
}
}
export default Layout;

@ -0,0 +1,6 @@
import { Layout } from 'core/layouts';
class VerticalLayout extends Layout {
}
export default VerticalLayout;

@ -0,0 +1,3 @@
export { default as Layout } from './Layout';
export { default as HorizontalLayout } from './HorizontalLayout';
export { default as VerticalLayout } from './VerticalLayout';

@ -0,0 +1,7 @@
import { Array2DRenderer } from 'core/renderers';
class Array1DRenderer extends Array2DRenderer {
}
export default Array1DRenderer;

@ -0,0 +1,39 @@
@import "~common/stylesheet/index";
.array_2d {
flex-shrink: 0;
display: table;
border-collapse: collapse;
.row {
display: table-row;
height: 28px;
.col {
display: table-cell;
text-align: center;
min-width: 28px;
background-color: $theme-normal;
border: 1px solid $theme-light;
padding: 0 4px;
.value {
font-size: 12px;
}
&.selected {
background-color: $color-selected;
}
&.patched {
background-color: $color-patched;
}
&.index {
background: none;
border: none;
color: $theme-light;
}
}
}
}

@ -0,0 +1,64 @@
import React from 'react';
import { Array1DRenderer, Renderer } from 'core/renderers';
import styles from './Array2DRenderer.module.scss';
import { classes } from 'common/util';
class Array2DRenderer extends Renderer {
constructor(props) {
super(props);
this.togglePan(true);
this.toggleZoom(true);
}
renderData() {
const { data } = this.props.data;
const isArray1D = this instanceof Array1DRenderer;
let longestRow = data.reduce((longestRow, row) => longestRow.length < row.length ? row : longestRow, []);
return (
<table className={styles.array_2d}
style={{ marginLeft: -this.centerX * 2, marginTop: -this.centerY * 2, transform: `scale(${this.zoom})` }}>
<tbody>
<tr className={styles.row}>
{
!isArray1D &&
<td className={classes(styles.col, styles.index)} />
}
{
longestRow.map((_, i) => (
<td className={classes(styles.col, styles.index)} key={i}>
<span className={styles.value}>{i}</span>
</td>
))
}
</tr>
{
data.map((row, i) => (
<tr className={styles.row} key={i}>
{
!isArray1D &&
<td className={classes(styles.col, styles.index)}>
<span className={styles.value}>{i}</span>
</td>
}
{
row.map((col, j) => (
<td className={classes(styles.col, col.selected && styles.selected, col.patched && styles.patched)}
key={j}>
<span className={styles.value}>{this.toString(col.value)}</span>
</td>
))
}
</tr>
))
}
</tbody>
</table>
);
}
}
export default Array2DRenderer;

@ -0,0 +1 @@
@import "~common/stylesheet/index";

@ -0,0 +1,36 @@
import React from 'react';
import { Bar } from 'react-chartjs-2';
import { Array1DRenderer } from 'core/renderers';
import styles from './ChartRenderer.module.scss';
class ChartRenderer extends Array1DRenderer {
renderData() {
const { data: [row] } = this.props.data;
const chartData = {
labels: row.map(col => `${col.value}`),
datasets: [{
backgroundColor: row.map(col => col.patched ? styles.colorPatched : col.selected ? styles.colorSelected : styles.colorFont),
data: row.map(col => col.value),
}],
};
return (
<Bar data={chartData} options={{
scales: {
yAxes: [{
ticks: {
beginAtZero: true
}
}]
},
animation: false,
legend: false,
responsive: true,
maintainAspectRatio: false
}} />
);
}
}
export default ChartRenderer;

@ -0,0 +1,98 @@
@import "~common/stylesheet/index";
.graph {
flex: 1;
align-self: stretch;
.node {
.circle {
fill: $theme-light;
stroke: $color-font;
stroke-width: 1;
}
.id {
fill: $color-font;
alignment-baseline: central;
text-anchor: middle;
}
.weight {
fill: white;
font-weight: bold;
alignment-baseline: central;
text-anchor: left;
}
&.selected {
.circle {
fill: $color-selected;
stroke: $color-selected;
}
}
&.visited {
.circle {
fill: $color-patched;
stroke: $color-patched;
}
}
}
.edge {
.line {
stroke: $color-font;
stroke-width: 2;
&.directed {
marker-end: url(#markerArrow);
}
}
.weight {
fill: $color-font;
alignment-baseline: baseline;
text-anchor: middle;
}
&.selected {
.line {
stroke: $color-selected;
&.directed {
marker-end: url(#markerArrowSelected);
}
}
.weight {
fill: $color-selected;
}
}
&.visited {
.line {
stroke: $color-patched;
&.directed {
marker-end: url(#markerArrowVisited);
}
}
.weight {
fill: $color-patched;
}
}
}
.arrow {
fill: $color-font;
&.selected {
fill: $color-selected;
}
&.visited {
fill: $color-patched;
}
}
}

@ -0,0 +1,126 @@
import React from 'react';
import { Renderer } from 'core/renderers';
import { classes, distance } from 'common/util';
import styles from './GraphRenderer.module.scss';
class GraphRenderer extends Renderer {
constructor(props) {
super(props);
this.elementRef = React.createRef();
this.selectedNode = null;
this.togglePan(true);
this.toggleZoom(true);
}
handleMouseDown(e) {
super.handleMouseDown(e);
const coords = this.computeCoords(e);
const { nodes, dimensions } = this.props.data;
const { nodeRadius } = dimensions;
this.selectedNode = nodes.find(node => distance(coords, node) <= nodeRadius);
}
handleMouseMove(e) {
if (this.selectedNode) {
const { x, y } = this.computeCoords(e);
const node = this.props.data.findNode(this.selectedNode.id);
node.x = x;
node.y = y;
this.refresh();
} else {
super.handleMouseMove(e);
}
}
computeCoords(e) {
const svg = this.elementRef.current;
const s = svg.createSVGPoint();
s.x = e.clientX;
s.y = e.clientY;
const { x, y } = s.matrixTransform(svg.getScreenCTM().inverse());
return { x, y };
}
renderData() {
const { nodes, edges, isDirected, isWeighted, dimensions } = this.props.data;
const { baseWidth, baseHeight, nodeRadius, arrowGap, nodeWeightGap, edgeWeightGap } = dimensions;
const viewBox = [
(this.centerX - baseWidth / 2) * this.zoom,
(this.centerY - baseHeight / 2) * this.zoom,
baseWidth * this.zoom,
baseHeight * this.zoom,
];
return (
<svg className={styles.graph} viewBox={viewBox} ref={this.elementRef}>
<defs>
<marker id="markerArrow" markerWidth="4" markerHeight="4" refX="2" refY="2" orient="auto">
<path d="M0,0 L0,4 L4,2 L0,0" className={styles.arrow} />
</marker>
<marker id="markerArrowSelected" markerWidth="4" markerHeight="4" refX="2" refY="2" orient="auto">
<path d="M0,0 L0,4 L4,2 L0,0" className={classes(styles.arrow, styles.selected)} />
</marker>
<marker id="markerArrowVisited" markerWidth="4" markerHeight="4" refX="2" refY="2" orient="auto">
<path d="M0,0 L0,4 L4,2 L0,0" className={classes(styles.arrow, styles.visited)} />
</marker>
</defs>
{
edges.sort((a, b) => a.visitedCount - b.visitedCount).map(edge => {
const { source, target, weight, visitedCount, selectedCount } = edge;
const sourceNode = this.props.data.findNode(source);
const targetNode = this.props.data.findNode(target);
if (!sourceNode || !targetNode) return undefined;
const { x: sx, y: sy } = sourceNode;
let { x: ex, y: ey } = targetNode;
const mx = (sx + ex) / 2;
const my = (sy + ey) / 2;
const dx = ex - sx;
const dy = ey - sy;
const degree = Math.atan2(dy, dx) / Math.PI * 180;
if (isDirected) {
const length = Math.sqrt(dx * dx + dy * dy);
if (length !== 0) {
ex = sx + dx / length * (length - nodeRadius - arrowGap);
ey = sy + dy / length * (length - nodeRadius - arrowGap);
}
}
return (
<g className={classes(styles.edge, selectedCount && styles.selected, visitedCount && styles.visited)}
key={`${source}-${target}`}>
<path d={`M${sx},${sy} L${ex},${ey}`} className={classes(styles.line, isDirected && styles.directed)} />
{
isWeighted &&
<g transform={`translate(${mx},${my})`}>
<text className={styles.weight} transform={`rotate(${degree})`}
y={-edgeWeightGap}>{this.toString(weight)}</text>
</g>
}
</g>
);
})
}
{
nodes.map(node => {
const { id, x, y, weight, visitedCount, selectedCount } = node;
return (
<g className={classes(styles.node, selectedCount && styles.selected, visitedCount && styles.visited)}
key={id} transform={`translate(${x},${y})`}>
<circle className={styles.circle} r={nodeRadius} />
<text className={styles.id}>{id}</text>
{
isWeighted &&
<text className={styles.weight} x={nodeRadius + nodeWeightGap}>{this.toString(weight)}</text>
}
</g>
);
})
}
</svg>
);
}
}
export default GraphRenderer;

@ -0,0 +1,17 @@
@import "~common/stylesheet/index";
.log {
flex: 1;
align-self: stretch;
display: flex;
flex-direction: column;
align-items: stretch;
overflow-y: auto;
.content {
padding: 24px;
font-family: $font-family-monospace;
white-space: pre-wrap;
line-height: 1.6;
}
}

@ -0,0 +1,30 @@
import React from 'react';
import { Renderer } from 'core/renderers';
import styles from './LogRenderer.module.scss';
class LogRenderer extends Renderer {
constructor(props) {
super(props);
this.elementRef = React.createRef();
}
componentDidUpdate(prevProps, prevState, snapshot) {
super.componentDidUpdate(prevProps, prevState, snapshot);
const div = this.elementRef.current;
div.scrollTop = div.scrollHeight;
}
renderData() {
const { log } = this.props.data;
return (
<div className={styles.log} ref={this.elementRef}>
<div className={styles.content} dangerouslySetInnerHTML={{ __html: log }} />
</div>
);
}
}
export default LogRenderer;

@ -0,0 +1,19 @@
@import "~common/stylesheet/index";
.markdown {
flex: 1;
align-self: stretch;
display: flex;
flex-direction: column;
align-items: stretch;
overflow-y: auto;
.content {
padding: 24px;
font-size: $font-size-large;
a {
text-decoration: underline;
}
}
}

@ -0,0 +1,75 @@
import React from 'react';
import { Renderer } from 'core/renderers';
import styles from './MarkdownRenderer.module.scss';
import ReactMarkdown from 'react-markdown';
class MarkdownRenderer extends Renderer {
renderData() {
const { markdown } = this.props.data;
const heading = ({ level, children, ...rest }) => {
const HeadingComponent = [
props => <h1 {...props} />,
props => <h2 {...props} />,
props => <h3 {...props} />,
props => <h4 {...props} />,
props => <h5 {...props} />,
props => <h6 {...props} />,
][level - 1];
const idfy = text => text.toLowerCase().trim().replace(/[^\w \-]/g, '').replace(/ /g, '-');
const getText = children => {
return React.Children.map(children, child => {
if (!child) return '';
if (typeof child === 'string') return child;
if ('props' in child) return getText(child.props.children);
return '';
}).join('');
};
const id = idfy(getText(children));
return (
<HeadingComponent id={id} {...rest}>
{children}
</HeadingComponent>
);
};
const link = ({ href, ...rest }) => {
return /^#/.test(href) ? (
<a href={href} {...rest} />
) : (
<a href={href} rel="noopener" target="_blank" {...rest} />
);
};
const image = ({ src, ...rest }) => {
let newSrc = src;
let style = { maxWidth: '100%' };
const CODECOGS = 'https://latex.codecogs.com/svg.latex?';
const WIKIMEDIA_IMAGE = 'https://upload.wikimedia.org/wikipedia/';
const WIKIMEDIA_MATH = 'https://wikimedia.org/api/rest_v1/media/math/render/svg/';
if (src.startsWith(CODECOGS)) {
const latex = src.substring(CODECOGS.length);
newSrc = `${CODECOGS}\\color{White}${latex}`;
} else if (src.startsWith(WIKIMEDIA_IMAGE)) {
style.backgroundColor = 'white';
} else if (src.startsWith(WIKIMEDIA_MATH)) {
style.filter = 'invert(100%)';
}
return <img src={newSrc} style={style} {...rest} />;
};
return (
<div className={styles.markdown}>
<ReactMarkdown className={styles.content} source={markdown} renderers={{ heading, link, image }}
escapeHtml={false}/>
</div>
);
}
}
export default MarkdownRenderer;

@ -0,0 +1,25 @@
@import "~common/stylesheet/index";
.renderer {
position: relative;
flex: 1;
flex-direction: column;
display: flex;
align-items: center;
justify-content: center;
min-height: 0;
&:first-child {
border-top: none;
}
.title {
position: absolute;
top: 0;
left: 0;
background-color: $theme-light;
color: $color-font;
padding: 4px 6px;
font-size: $font-size-large;
}
}

@ -0,0 +1,111 @@
import React from 'react';
import styles from './Renderer.module.scss';
import { Ellipsis } from 'components';
import { classes } from 'common/util';
class Renderer extends React.Component {
constructor(props) {
super(props);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleWheel = this.handleWheel.bind(this);
this._handleMouseDown = this.handleMouseDown;
this._handleWheel = this.handleWheel;
this.togglePan(false);
this.toggleZoom(false);
this.lastX = null;
this.lastY = null;
this.centerX = 0;
this.centerY = 0;
this.zoom = 1;
this.zoomFactor = 1.01;
this.zoomMax = 20;
this.zoomMin = 1 / 20;
}
componentDidUpdate(prevProps, prevState, snapshot) {
}
togglePan(enable = !this.handleMouseDown) {
this.handleMouseDown = enable ? this._handleMouseDown : undefined;
}
toggleZoom(enable = !this.handleWheel) {
this.handleWheel = enable ? this._handleWheel : undefined;
}
handleMouseDown(e) {
const { clientX, clientY } = e;
this.lastX = clientX;
this.lastY = clientY;
document.addEventListener('mousemove', this.handleMouseMove);
document.addEventListener('mouseup', this.handleMouseUp);
}
handleMouseMove(e) {
const { clientX, clientY } = e;
const dx = clientX - this.lastX;
const dy = clientY - this.lastY;
this.centerX -= dx;
this.centerY -= dy;
this.refresh();
this.lastX = clientX;
this.lastY = clientY;
}
handleMouseUp(e) {
document.removeEventListener('mousemove', this.handleMouseMove);
document.removeEventListener('mouseup', this.handleMouseUp);
}
handleWheel(e) {
e.preventDefault();
const { deltaY } = e;
this.zoom *= Math.pow(this.zoomFactor, deltaY);
this.zoom = Math.min(this.zoomMax, Math.max(this.zoomMin, this.zoom));
this.refresh();
}
toString(value) {
switch (typeof(value)) {
case 'number':
return [Number.POSITIVE_INFINITY, Number.MAX_SAFE_INTEGER, 0x7fffffff].includes(value) ? '∞' :
[Number.NEGATIVE_INFINITY, Number.MIN_SAFE_INTEGER, -0x80000000].includes(value) ? '-∞' :
Number.isInteger(value) ? value.toString() :
value.toFixed(3);
case 'boolean':
return value ? 'T' : 'F';
default:
return value;
}
}
refresh() {
this.forceUpdate();
}
renderData() {
return null;
}
render() {
const { className, title } = this.props;
return (
<div className={classes(styles.renderer, className)} onMouseDown={this.handleMouseDown}
onWheel={this.handleWheel}>
<Ellipsis className={styles.title}>{title}</Ellipsis>
{
this.renderData()
}
</div>
);
}
}
export default Renderer;

@ -0,0 +1,53 @@
import React from 'react'
import {Scatter} from 'react-chartjs-2'
import Array2DRenderer from '../Array2DRenderer'
const convertToObjectArray = ([x, y]) => ({ x, y })
const colors = ['white', 'green', 'blue', 'red', 'yellow', 'cyan']
class ScatterRenderer extends Array2DRenderer {
renderData() {
const { data } = this.props.data
const datasets = data.map((series, index) => (
{
backgroundColor: colors[index],
data: series.map(s => convertToObjectArray(s.value)),
label: Math.random(),
radius: (index + 1) * 2,
}))
const chartData = {
datasets,
}
return <Scatter data={chartData} options={{
legend: false,
animation: false,
layout: {
padding: {
left: 20,
right: 20,
top: 20,
bottom: 20,
},
},
scales: {
yAxes: [
{
ticks: {
beginAtZero: false,
},
}],
xAxes: [
{
ticks: {
beginAtZero: false,
},
}],
},
}}/>
}
}
export default ScatterRenderer

@ -0,0 +1,8 @@
export { default as Renderer } from './Renderer';
export { default as MarkdownRenderer } from './MarkdownRenderer';
export { default as LogRenderer } from './LogRenderer';
export { default as Array2DRenderer } from './Array2DRenderer';
export { default as Array1DRenderer } from './Array1DRenderer';
export { default as ChartRenderer } from './ChartRenderer';
export { default as GraphRenderer } from './GraphRenderer';
export { default as ScatterRenderer } from './ScatterRenderer';

@ -0,0 +1,46 @@
import { Array2DTracer } from 'core/tracers';
import { Array1DRenderer } from 'core/renderers';
class Array1DTracer extends Array2DTracer {
getRendererClass() {
return Array1DRenderer;
}
init() {
super.init();
this.chartTracer = null;
}
set(array1d = []) {
const array2d = [array1d];
super.set(array2d);
this.syncChartTracer();
}
patch(x, v) {
super.patch(0, x, v);
}
depatch(x) {
super.depatch(0, x);
}
select(sx, ex = sx) {
super.select(0, sx, 0, ex);
}
deselect(sx, ex = sx) {
super.deselect(0, sx, 0, ex);
}
chart(key) {
this.chartTracer = key ? this.getObject(key) : null;
this.syncChartTracer();
}
syncChartTracer() {
if (this.chartTracer) this.chartTracer.data = this.data;
}
}
export default Array1DTracer;

@ -0,0 +1,65 @@
import { Tracer } from 'core/tracers';
import { Array2DRenderer } from 'core/renderers';
class Element {
constructor(value) {
this.value = value;
this.patched = false;
this.selected = false;
}
}
class Array2DTracer extends Tracer {
getRendererClass() {
return Array2DRenderer;
}
set(array2d = []) {
this.data = array2d.map(array1d => [...array1d].map(value => new Element(value)));
super.set();
}
patch(x, y, v = this.data[x][y].value) {
if (!this.data[x][y]) this.data[x][y] = new Element();
this.data[x][y].value = v;
this.data[x][y].patched = true;
}
depatch(x, y) {
this.data[x][y].patched = false;
}
select(sx, sy, ex = sx, ey = sy) {
for (let x = sx; x <= ex; x++) {
for (let y = sy; y <= ey; y++) {
this.data[x][y].selected = true;
}
}
}
selectRow(x, sy, ey) {
this.select(x, sy, x, ey);
}
selectCol(y, sx, ex) {
this.select(sx, y, ex, y);
}
deselect(sx, sy, ex = sx, ey = sy) {
for (let x = sx; x <= ex; x++) {
for (let y = sy; y <= ey; y++) {
this.data[x][y].selected = false;
}
}
}
deselectRow(x, sy, ey) {
this.deselect(x, sy, x, ey);
}
deselectCol(y, sx, ex) {
this.deselect(sx, y, ex, y);
}
}
export default Array2DTracer;

@ -0,0 +1,10 @@
import { Array1DTracer } from 'core/tracers';
import { ChartRenderer } from 'core/renderers';
class ChartTracer extends Array1DTracer {
getRendererClass() {
return ChartRenderer;
}
}
export default ChartTracer;

@ -0,0 +1,261 @@
import { Tracer } from 'core/tracers';
import { distance } from 'common/util';
import { GraphRenderer } from 'core/renderers';
class GraphTracer extends Tracer {
getRendererClass() {
return GraphRenderer;
}
init() {
super.init();
this.dimensions = {
baseWidth: 320,
baseHeight: 320,
padding: 32,
nodeRadius: 12,
arrowGap: 4,
nodeWeightGap: 4,
edgeWeightGap: 4,
};
this.isDirected = true;
this.isWeighted = false;
this.callLayout = { method: this.layoutCircle, args: [] };
this.logTracer = null;
}
set(array2d = []) {
this.nodes = [];
this.edges = [];
for (let i = 0; i < array2d.length; i++) {
this.addNode(i);
for (let j = 0; j < array2d.length; j++) {
const value = array2d[i][j];
if (value) {
this.addEdge(i, j, this.isWeighted ? value : null);
}
}
}
this.layout();
super.set();
}
directed(isDirected = true) {
this.isDirected = isDirected;
}
weighted(isWeighted = true) {
this.isWeighted = isWeighted;
}
addNode(id, weight = null, x = 0, y = 0, visitedCount = 0, selectedCount = 0) {
if (this.findNode(id)) return;
this.nodes.push({ id, weight, x, y, visitedCount, selectedCount });
this.layout();
}
updateNode(id, weight, x, y, visitedCount, selectedCount) {
const node = this.findNode(id);
const update = { weight, x, y, visitedCount, selectedCount };
Object.keys(update).forEach(key => {
if (update[key] === undefined) delete update[key];
});
Object.assign(node, update);
}
removeNode(id) {
const node = this.findNode(id);
if (!node) return;
const index = this.nodes.indexOf(node);
this.nodes.splice(index, 1);
this.layout();
}
addEdge(source, target, weight = null, visitedCount = 0, selectedCount = 0) {
if (this.findEdge(source, target)) return;
this.edges.push({ source, target, weight, visitedCount, selectedCount });
this.layout();
}
updateEdge(source, target, weight, visitedCount, selectedCount) {
const edge = this.findEdge(source, target);
const update = { weight, visitedCount, selectedCount };
Object.keys(update).forEach(key => {
if (update[key] === undefined) delete update[key];
});
Object.assign(edge, update);
}
removeEdge(source, target) {
const edge = this.findEdge(source, target);
if (!edge) return;
const index = this.edges.indexOf(edge);
this.edges.splice(index, 1);
this.layout();
}
findNode(id) {
return this.nodes.find(node => node.id === id);
}
findEdge(source, target, isDirected = this.isDirected) {
if (isDirected) {
return this.edges.find(edge => edge.source === source && edge.target === target);
} else {
return this.edges.find(edge =>
(edge.source === source && edge.target === target) ||
(edge.source === target && edge.target === source));
}
}
findLinkedEdges(source, isDirected = this.isDirected) {
if (isDirected) {
return this.edges.filter(edge => edge.source === source);
} else {
return this.edges.filter(edge => edge.source === source || edge.target === source);
}
}
findLinkedNodeIds(source, isDirected = this.isDirected) {
const edges = this.findLinkedEdges(source, isDirected);
return edges.map(edge => edge.source === source ? edge.target : edge.source);
}
findLinkedNodes(source, isDirected = this.isDirected) {
const ids = this.findLinkedNodeIds(source, isDirected);
return ids.map(id => this.findNode(id));
}
getRect() {
const { baseWidth, baseHeight, padding } = this.dimensions;
const left = -baseWidth / 2 + padding;
const top = -baseHeight / 2 + padding;
const right = baseWidth / 2 - padding;
const bottom = baseHeight / 2 - padding;
const width = right - left;
const height = bottom - top;
return { left, top, right, bottom, width, height };
}
layout() {
const { method, args } = this.callLayout;
method.apply(this, args);
}
layoutCircle() {
this.callLayout = { method: this.layoutCircle, args: arguments };
const rect = this.getRect();
const unitAngle = 2 * Math.PI / this.nodes.length;
let angle = -Math.PI / 2;
for (const node of this.nodes) {
const x = Math.cos(angle) * rect.width / 2;
const y = Math.sin(angle) * rect.height / 2;
node.x = x;
node.y = y;
angle += unitAngle;
}
}
layoutTree(root = 0, sorted = false) {
this.callLayout = { method: this.layoutTree, args: arguments };
const rect = this.getRect();
if (this.nodes.length === 1) {
const [node] = this.nodes;
node.x = (rect.left + rect.right) / 2;
node.y = (rect.top + rect.bottom) / 2;
return;
}
let maxDepth = 0;
const leafCounts = {};
let marked = {};
const recursiveAnalyze = (id, depth) => {
marked[id] = true;
leafCounts[id] = 0;
if (maxDepth < depth) maxDepth = depth;
const linkedNodeIds = this.findLinkedNodeIds(id, false);
for (const linkedNodeId of linkedNodeIds) {
if (marked[linkedNodeId]) continue;
leafCounts[id] += recursiveAnalyze(linkedNodeId, depth + 1);
}
if (leafCounts[id] === 0) leafCounts[id] = 1;
return leafCounts[id];
};
recursiveAnalyze(root, 0);
const hGap = rect.width / leafCounts[root];
const vGap = rect.height / maxDepth;
marked = {};
const recursivePosition = (node, h, v) => {
marked[node.id] = true;
node.x = rect.left + (h + leafCounts[node.id] / 2) * hGap;
node.y = rect.top + v * vGap;
const linkedNodes = this.findLinkedNodes(node.id, false);
if (sorted) linkedNodes.sort((a, b) => a.id - b.id);
for (const linkedNode of linkedNodes) {
if (marked[linkedNode.id]) continue;
recursivePosition(linkedNode, h, v + 1);
h += leafCounts[linkedNode.id];
}
};
const rootNode = this.findNode(root);
recursivePosition(rootNode, 0, 0);
}
layoutRandom() {
this.callLayout = { method: this.layoutRandom, args: arguments };
const rect = this.getRect();
const placedNodes = [];
for (const node of this.nodes) {
do {
node.x = rect.left + Math.random() * rect.width;
node.y = rect.top + Math.random() * rect.height;
} while (placedNodes.find(placedNode => distance(node, placedNode) < 48));
placedNodes.push(node);
}
}
visit(target, source, weight) {
this.visitOrLeave(true, target, source, weight);
}
leave(target, source, weight) {
this.visitOrLeave(false, target, source, weight);
}
visitOrLeave(visit, target, source = null, weight) {
const edge = this.findEdge(source, target);
if (edge) edge.visitedCount += visit ? 1 : -1;
const node = this.findNode(target);
if (weight !== undefined) node.weight = weight;
node.visitedCount += visit ? 1 : -1;
if (this.logTracer) {
this.logTracer.println(visit ? (source || '') + ' -> ' + target : (source || '') + ' <- ' + target);
}
}
select(target, source) {
this.selectOrDeselect(true, target, source);
}
deselect(target, source) {
this.selectOrDeselect(false, target, source);
}
selectOrDeselect(select, target, source = null) {
const edge = this.findEdge(source, target);
if (edge) edge.selectedCount += select ? 1 : -1;
const node = this.findNode(target);
node.selectedCount += select ? 1 : -1;
if (this.logTracer) {
this.logTracer.println(select ? (source || '') + ' => ' + target : (source || '') + ' <= ' + target);
}
}
log(key) {
this.logTracer = key ? this.getObject(key) : null;
}
}
export default GraphTracer;

@ -0,0 +1,28 @@
import { sprintf } from 'sprintf-js';
import { Tracer } from 'core/tracers';
import { LogRenderer } from 'core/renderers';
class LogTracer extends Tracer {
getRendererClass() {
return LogRenderer;
}
set(log = '') {
this.log = log;
super.set();
}
print(message) {
this.log += message;
}
println(message) {
this.print(message + '\n');
}
printf(format, ...args) {
this.print(sprintf(format, ...args));
}
}
export default LogTracer;

@ -0,0 +1,15 @@
import { Tracer } from 'core/tracers';
import { MarkdownRenderer } from 'core/renderers';
class MarkdownTracer extends Tracer {
getRendererClass() {
return MarkdownRenderer;
}
set(markdown = '') {
this.markdown = markdown;
super.set();
}
}
export default MarkdownTracer;

@ -0,0 +1,10 @@
import { Array2DTracer } from 'core/tracers';
import { ScatterRenderer } from 'core/renderers';
class ScatterTracer extends Array2DTracer {
getRendererClass() {
return ScatterRenderer;
}
}
export default ScatterTracer;

@ -0,0 +1,35 @@
import React from 'react';
import { Renderer } from 'core/renderers';
class Tracer {
constructor(key, getObject, title) {
this.key = key;
this.getObject = getObject;
this.title = title;
this.init();
this.reset();
}
getRendererClass() {
return Renderer;
}
init() {
}
render() {
const RendererClass = this.getRendererClass();
return (
<RendererClass key={this.key} title={this.title} data={this} />
);
}
set() {
}
reset() {
this.set();
}
}
export default Tracer;

@ -0,0 +1,8 @@
export { default as Tracer } from './Tracer';
export { default as MarkdownTracer } from './MarkdownTracer';
export { default as LogTracer } from './LogTracer';
export { default as Array2DTracer } from './Array2DTracer';
export { default as Array1DTracer } from './Array1DTracer';
export { default as ChartTracer } from './ChartTracer';
export { default as GraphTracer } from './GraphTracer';
export { default as ScatterTracer} from './ScatterTracer';

@ -0,0 +1,12 @@
import { createProjectFile, createUserFile } from 'common/util';
const getName = filePath => filePath.split('/').pop();
const getContent = filePath => require('!raw-loader!./' + filePath).default;
const readProjectFile = filePath => createProjectFile(getName(filePath), getContent(filePath));
const readUserFile = filePath => createUserFile(getName(filePath), getContent(filePath));
export const CODE_CPP = readUserFile('skeletons/code.cpp');
export const CODE_JAVA = readUserFile('skeletons/code.java');
export const CODE_JS = readUserFile('skeletons/code.js');
export const ROOT_README_MD = readProjectFile('algorithm-visualizer/README.md');
export const SCRATCH_PAPER_README_MD = readProjectFile('scratch-paper/README.md');

@ -0,0 +1,28 @@
# Scratch Paper
Visualize your own code here!
## Learning About Tracers
The project [Algorithm Visualizer](https://github.com/algorithm-visualizer) has a visualization library in each
supported language ([JavaScript](https://github.com/algorithm-visualizer/tracers.js)
, [C++](https://github.com/algorithm-visualizer/tracers.cpp),
and [Java](https://github.com/algorithm-visualizer/tracers.java)) to visualize codes.
There are five tracers in the library to visualize different types of data:
- [Array1DTracer](https://github.com/algorithm-visualizer/algorithm-visualizer/wiki/Array1DTracer)
- [Array2DTracer](https://github.com/algorithm-visualizer/algorithm-visualizer/wiki/Array2DTracer)
- [ChartTracer](https://github.com/algorithm-visualizer/algorithm-visualizer/wiki/ChartTracer)
- [GraphTracer](https://github.com/algorithm-visualizer/algorithm-visualizer/wiki/GraphTracer)
- [LogTracer](https://github.com/algorithm-visualizer/algorithm-visualizer/wiki/LogTracer)
There are also randomizers to help you create random data.
Check out the [API reference](https://github.com/algorithm-visualizer/algorithm-visualizer/wiki) for more information.
## Making Your Visualization Public
If you think other people would find your visualization useful, you can add it to the side menu
by [contributing to `algorithm-visualizer/algorithms`](https://github.com/algorithm-visualizer/algorithms/blob/master/CONTRIBUTING.md)
.

@ -0,0 +1,43 @@
// import visualization libraries {
#include "algorithm-visualizer.h"
// }
#include <vector>
#include <string>
// define tracer variables {
Array2DTracer array2dTracer = Array2DTracer("Grid");
LogTracer logTracer = LogTracer("Console");
// }
// define input variables
std::vector<std::string> messages{
"Visualize",
"your",
"own",
"code",
"here!",
};
// highlight each line of messages recursively
void highlight(int line) {
if (line >= messages.size()) return;
std::string message = messages[line];
// visualize {
logTracer.println(message);
array2dTracer.selectRow(line, 0, message.size() - 1);
Tracer::delay();
array2dTracer.deselectRow(line, 0, message.size() - 1);
// }
highlight(line + 1);
}
int main() {
// visualize {
Layout::setRoot(VerticalLayout({array2dTracer, logTracer}));
array2dTracer.set(messages);
Tracer::delay();
// }
highlight(0);
return 0;
}

@ -0,0 +1,45 @@
// import visualization libraries {
import org.algorithm_visualizer.*;
// }
class Main {
// define tracer variables {
Array2DTracer array2dTracer = new Array2DTracer("Grid");
LogTracer logTracer = new LogTracer("Console");
// }
// define input variables
String[] messages = {
"Visualize",
"your",
"own",
"code",
"here!",
};
// highlight each line of messages recursively
void highlight(int line) {
if (line >= messages.length) return;
String message = messages[line];
// visualize {
logTracer.println(message);
array2dTracer.selectRow(line, 0, message.length() - 1);
Tracer.delay();
array2dTracer.deselectRow(line, 0, message.length() - 1);
// }
highlight(line + 1);
}
Main() {
// visualize {
Layout.setRoot(new VerticalLayout(new Commander[]{array2dTracer, logTracer}));
array2dTracer.set(messages);
Tracer.delay();
// }
highlight(0);
}
public static void main(String[] args) {
new Main();
}
}

@ -0,0 +1,39 @@
// import visualization libraries {
const { Array2DTracer, Layout, LogTracer, Tracer, VerticalLayout } = require('algorithm-visualizer');
// }
// define tracer variables {
const array2dTracer = new Array2DTracer('Grid');
const logTracer = new LogTracer('Console');
// }
// define input variables
const messages = [
'Visualize',
'your',
'own',
'code',
'here!',
];
// highlight each line of messages recursively
function highlight(line) {
if (line >= messages.length) return;
const message = messages[line];
// visualize {
logTracer.println(message);
array2dTracer.selectRow(line, 0, message.length - 1);
Tracer.delay();
array2dTracer.deselectRow(line, 0, message.length - 1);
// }
highlight(line + 1);
}
(function main() {
// visualize {
Layout.setRoot(new VerticalLayout([array2dTracer, logTracer]));
array2dTracer.set(messages);
Tracer.delay();
// }
highlight(0);
})();

@ -0,0 +1,22 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { combineReducers, createStore } from 'redux';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import { Provider } from 'react-redux';
import { routerReducer } from 'react-router-redux';
import App from 'components/App';
import * as reducers from 'reducers';
import './stylesheet.scss';
const store = createStore(combineReducers({ ...reducers, routing: routerReducer }));
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<Switch>
<Route exact path="/scratch-paper/:gistId" component={App}/>
<Route exact path="/:categoryKey/:algorithmKey" component={App}/>
<Route path="/" component={App}/>
</Switch>
</BrowserRouter>
</Provider>, document.getElementById('root'));

@ -0,0 +1,144 @@
import { combineActions, createAction, handleActions } from 'redux-actions';
import { ROOT_README_MD } from 'files';
import { extension, isSaved } from 'common/util';
const prefix = 'CURRENT';
const setHome = createAction(`${prefix}/SET_HOME`, () => defaultState);
const setAlgorithm = createAction(`${prefix}/SET_ALGORITHM`, ({ categoryKey, categoryName, algorithmKey, algorithmName, files, description }) => ({
algorithm: { categoryKey, algorithmKey },
titles: [categoryName, algorithmName],
files,
description,
}));
const setScratchPaper = createAction(`${prefix}/SET_SCRATCH_PAPER`, ({ login, gistId, title, files }) => ({
scratchPaper: { login, gistId },
titles: ['Scratch Paper', title],
files,
description: homeDescription,
}));
const setEditingFile = createAction(`${prefix}/SET_EDITING_FILE`, file => ({ file }));
const modifyTitle = createAction(`${prefix}/MODIFY_TITLE`, title => ({ title }));
const addFile = createAction(`${prefix}/ADD_FILE`, file => ({ file }));
const renameFile = createAction(`${prefix}/RENAME_FILE`, (file, name) => ({ file, name }));
const modifyFile = createAction(`${prefix}/MODIFY_FILE`, (file, content) => ({ file, content }));
const deleteFile = createAction(`${prefix}/DELETE_FILE`, file => ({ file }));
export const actions = {
setHome,
setAlgorithm,
setScratchPaper,
setEditingFile,
modifyTitle,
addFile,
modifyFile,
deleteFile,
renameFile,
};
const homeTitles = ['Algorithm Visualizer'];
const homeFiles = [ROOT_README_MD];
const homeDescription = 'Algorithm Visualizer is an interactive online platform that visualizes algorithms from code.';
const defaultState = {
algorithm: {
categoryKey: 'algorithm-visualizer',
algorithmKey: 'home',
},
scratchPaper: undefined,
titles: homeTitles,
files: homeFiles,
lastTitles: homeTitles,
lastFiles: homeFiles,
description: homeDescription,
editingFile: undefined,
shouldBuild: true,
saved: true,
};
export default handleActions({
[combineActions(
setHome,
setAlgorithm,
setScratchPaper,
)]: (state, { payload }) => {
const { algorithm, scratchPaper, titles, files, description } = payload;
return {
...state,
algorithm,
scratchPaper,
titles,
files,
lastTitles: titles,
lastFiles: files,
description,
editingFile: undefined,
shouldBuild: true,
saved: true,
};
},
[setEditingFile]: (state, { payload }) => {
const { file } = payload;
return {
...state,
editingFile: file,
shouldBuild: true,
};
},
[modifyTitle]: (state, { payload }) => {
const { title } = payload;
const newState = {
...state,
titles: [state.titles[0], title],
};
return {
...newState,
saved: isSaved(newState),
};
},
[addFile]: (state, { payload }) => {
const { file } = payload;
const newState = {
...state,
files: [...state.files, file],
editingFile: file,
shouldBuild: true,
};
return {
...newState,
saved: isSaved(newState),
};
},
[combineActions(
renameFile,
modifyFile,
)]: (state, { payload }) => {
const { file, ...update } = payload;
const editingFile = { ...file, ...update };
const newState = {
...state,
files: state.files.map(oldFile => oldFile === file ? editingFile : oldFile),
editingFile,
shouldBuild: extension(editingFile.name) === 'md',
};
return {
...newState,
saved: isSaved(newState),
};
},
[deleteFile]: (state, { payload }) => {
const { file } = payload;
const index = state.files.indexOf(file);
const files = state.files.filter(oldFile => oldFile !== file);
const editingFile = files[Math.min(index, files.length - 1)];
const newState = {
...state,
files,
editingFile,
shouldBuild: true,
};
return {
...newState,
saved: isSaved(newState),
};
},
}, defaultState);

@ -0,0 +1,26 @@
import { combineActions, createAction, handleActions } from 'redux-actions';
const prefix = 'DIRECTORY';
const setCategories = createAction(`${prefix}/SET_CATEGORIES`, categories => ({ categories }));
const setScratchPapers = createAction(`${prefix}/SET_SCRATCH_PAPERS`, scratchPapers => ({ scratchPapers }));
export const actions = {
setCategories,
setScratchPapers,
};
const defaultState = {
categories: [],
scratchPapers: [],
};
export default handleActions({
[combineActions(
setCategories,
setScratchPapers,
)]: (state, { payload }) => ({
...state,
...payload,
}),
}, defaultState);

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save