From 0be0e02dc7febefcc9fc32c8b4fd4dfa02755079 Mon Sep 17 00:00:00 2001
From: Jason Park <parkjs814@gmail.com>
Date: Sat, 11 Aug 2018 21:19:59 +0900
Subject: [PATCH] Improve visualization performance and revise toolbar buttons

---
 src/frontend/apis/index.js                    |  11 +-
 src/frontend/common/stylesheet/colors.scss    |   4 +-
 src/frontend/components/App/index.jsx         |  48 ++--
 src/frontend/components/App/stylesheet.scss   |   6 +-
 src/frontend/components/Button/index.jsx      |   6 +-
 .../components/Button/stylesheet.scss         |   4 +-
 src/frontend/components/CodeEditor/index.jsx  |  56 ++---
 src/frontend/components/Header/index.jsx      |  65 +----
 .../components/Header/stylesheet.scss         |  47 +---
 .../components/MarkdownViewer/index.jsx       |  38 ---
 src/frontend/components/Player/index.jsx      | 174 ++++++++++++++
 .../components/Player/stylesheet.scss         |  52 ++++
 src/frontend/components/ProgressBar/index.jsx |  49 ++++
 .../components/ProgressBar/stylesheet.scss    |  31 +++
 .../components/VisualizationViewer/index.jsx  | 108 +++++++--
 src/frontend/components/index.js              |   2 +-
 src/frontend/core/datas/Array1DData.js        |  10 +-
 src/frontend/core/datas/Array2DData.js        |   7 +-
 src/frontend/core/datas/ChartData.js          |   6 +-
 src/frontend/core/datas/Data.js               |  30 ---
 src/frontend/core/datas/Data.jsx              |  39 +++
 src/frontend/core/datas/GraphData.js          |  10 +-
 src/frontend/core/datas/LogData.js            |   7 +-
 src/frontend/core/datas/MarkdownData.js       |  15 ++
 src/frontend/core/datas/index.js              |   1 +
 src/frontend/core/index.js                    |   1 -
 .../core/renderers/MarkdownRenderer/index.jsx |  35 +++
 .../MarkdownRenderer}/stylesheet.scss         |  10 +-
 .../core/renderers/Renderer/index.jsx         |  30 ---
 .../core/renderers/Renderer/stylesheet.scss   |   4 +-
 src/frontend/core/renderers/index.js          |   1 +
 src/frontend/core/tracerManager.jsx           | 224 ------------------
 src/frontend/reducers/index.js                |   5 +-
 src/frontend/reducers/player.js               |  34 +++
 34 files changed, 627 insertions(+), 543 deletions(-)
 delete mode 100644 src/frontend/components/MarkdownViewer/index.jsx
 create mode 100644 src/frontend/components/Player/index.jsx
 create mode 100644 src/frontend/components/Player/stylesheet.scss
 create mode 100644 src/frontend/components/ProgressBar/index.jsx
 create mode 100644 src/frontend/components/ProgressBar/stylesheet.scss
 delete mode 100644 src/frontend/core/datas/Data.js
 create mode 100644 src/frontend/core/datas/Data.jsx
 create mode 100644 src/frontend/core/datas/MarkdownData.js
 delete mode 100644 src/frontend/core/index.js
 create mode 100644 src/frontend/core/renderers/MarkdownRenderer/index.jsx
 rename src/frontend/{components/MarkdownViewer => core/renderers/MarkdownRenderer}/stylesheet.scss (68%)
 delete mode 100644 src/frontend/core/tracerManager.jsx
 create mode 100644 src/frontend/reducers/player.js

diff --git a/src/frontend/apis/index.js b/src/frontend/apis/index.js
index 60631b1..da3aa86 100644
--- a/src/frontend/apis/index.js
+++ b/src/frontend/apis/index.js
@@ -70,6 +70,15 @@ const GitHubApi = {
 
 let jsWorker = null;
 const TracerApi = {
+  md: ({ code }) => Promise.resolve([{
+    tracerKey: '0-MarkdownTracer-Markdown',
+    method: 'construct',
+    args: ['MarkdownTracer', 'Markdown'],
+  }, {
+    tracerKey: '0-MarkdownTracer-Markdown',
+    method: 'set',
+    args: [code],
+  }]),
   js: ({ code }) => new Promise((resolve, reject) => {
     if (jsWorker) jsWorker.terminate();
     jsWorker = new Worker('/api/tracers/js');
@@ -85,4 +94,4 @@ export {
   CategoryApi,
   GitHubApi,
   TracerApi,
-};
\ No newline at end of file
+};
diff --git a/src/frontend/common/stylesheet/colors.scss b/src/frontend/common/stylesheet/colors.scss
index e829675..6882265 100644
--- a/src/frontend/common/stylesheet/colors.scss
+++ b/src/frontend/common/stylesheet/colors.scss
@@ -8,6 +8,7 @@ $color-alert: #f3bd58;
 $color-selected: #2962ff;
 $color-patched: #c51162;
 $color-highlight: #29d;
+$color-active: #00e676;
 
 :export {
   themeDark: $theme-dark;
@@ -20,4 +21,5 @@ $color-highlight: #29d;
   colorSelected: $color-selected;
   colorPatched: $color-patched;
   colorHighlight: $color-highlight;
-}
\ No newline at end of file
+  colorActive: $color-active;
+}
diff --git a/src/frontend/components/App/index.jsx b/src/frontend/components/App/index.jsx
index a50f027..44cf4c3 100644
--- a/src/frontend/components/App/index.jsx
+++ b/src/frontend/components/App/index.jsx
@@ -11,7 +11,6 @@ import 'axios-progress-bar/dist/nprogress.css';
 import {
   CodeEditor,
   Header,
-  MarkdownViewer,
   Navigator,
   ResizableContainer,
   TabContainer,
@@ -19,10 +18,9 @@ import {
   VisualizationViewer,
 } from '/components';
 import { CategoryApi, GitHubApi } from '/apis';
-import { tracerManager } from '/core';
 import { actions } from '/reducers';
 import { extension, refineGist } from '/common/util';
-import { languages, exts, us } from '/common/config';
+import { exts, languages, us } from '/common/config';
 import { README_MD, SCRATCH_PAPER_MD } from '/skeletons';
 import styles from './stylesheet.scss';
 
@@ -36,7 +34,6 @@ class App extends React.Component {
     this.state = {
       navigatorOpened: true,
       workspaceWeights: [1, 2, 2],
-      viewerTabIndex: 0,
       editorTabIndex: -1,
     };
   }
@@ -53,15 +50,11 @@ class App extends React.Component {
     CategoryApi.getCategories()
       .then(({ categories }) => this.props.setCategories(categories))
       .catch(this.props.showErrorToast);
-
-    tracerManager.setOnError(error => this.props.showErrorToast({ name: error.name, message: error.message }));
   }
 
   componentWillUnmount() {
     delete window.signIn;
     delete window.signOut;
-
-    tracerManager.setOnError(null);
   }
 
   componentWillReceiveProps(nextProps) {
@@ -172,10 +165,6 @@ class App extends React.Component {
     this.setState({ workspaceWeights });
   }
 
-  handleChangeViewerTabIndex(viewerTabIndex) {
-    this.setState({ viewerTabIndex });
-  }
-
   handleChangeEditorTabIndex(editorTabIndex) {
     const { files } = this.props.current;
     if (editorTabIndex === files.length) this.handleAddFile();
@@ -203,8 +192,11 @@ class App extends React.Component {
   handleDeleteFile(file) {
     const { files } = this.props.current;
     const { editorTabIndex } = this.state;
-    if (files.indexOf(file) < editorTabIndex) this.handleChangeEditorTabIndex(editorTabIndex - 1);
-    else this.handleChangeEditorTabIndex(Math.min(editorTabIndex, files.length - 2));
+    if (files.indexOf(file) < editorTabIndex) {
+      this.handleChangeEditorTabIndex(editorTabIndex - 1);
+    } else {
+      this.handleChangeEditorTabIndex(Math.min(editorTabIndex, files.length - 2));
+    }
     this.props.deleteFile(file);
   }
 
@@ -220,19 +212,21 @@ class App extends React.Component {
       serializeFiles(files) === serializeFiles(lastFiles);
   }
 
+  getDescription() {
+    const { files } = this.props.current;
+    const readmeFile = files.find(file => file.name === 'README.md');
+    if (!readmeFile) return '';
+    const groups = /^\s*# .*\n+([^\n]+)/.exec(readmeFile.content);
+    return groups && groups[1] || '';
+  }
+
   render() {
-    const { navigatorOpened, workspaceWeights, viewerTabIndex, editorTabIndex } = this.state;
+    const { navigatorOpened, workspaceWeights, editorTabIndex } = this.state;
     const { titles, files } = this.props.current;
 
     const gistSaved = this.isGistSaved();
 
-    const readmeFile = files.find(file => file.name === 'README.md') || {
-      name: 'README.md',
-      content: `# ${titles[1]}\nREADME.md not found`,
-      contributors: [us],
-    };
-    const groups = /^\s*# .*\n+([^\n]+)/.exec(readmeFile.content);
-    const description = groups && groups[1] || '';
+    const description = this.getDescription();
 
     const editorTitles = files.map(file => file.name);
     if (files[editorTabIndex]) {
@@ -253,17 +247,13 @@ class App extends React.Component {
         </Helmet>
         <Header className={styles.header} onClickTitleBar={() => this.toggleNavigatorOpened()}
                 navigatorOpened={navigatorOpened} loadScratchPapers={() => this.loadScratchPapers()}
-                loadAlgorithm={params => this.loadAlgorithm(params)}
-                onAction={() => this.handleChangeViewerTabIndex(1)} gistSaved={gistSaved} />
+                loadAlgorithm={params => this.loadAlgorithm(params)} gistSaved={gistSaved}
+                file={files[editorTabIndex]} />
         <ResizableContainer className={styles.workspace} horizontal weights={workspaceWeights}
                             visibles={[navigatorOpened, true, true]}
                             onChangeWeights={weights => this.handleChangeWorkspaceWeights(weights)}>
           <Navigator loadAlgorithm={params => this.loadAlgorithm(params)} />
-          <TabContainer titles={['Description', 'Visualization']} tabIndex={viewerTabIndex}
-                        onChangeTabIndex={tabIndex => this.handleChangeViewerTabIndex(tabIndex)}>
-            <MarkdownViewer source={readmeFile.content} />
-            <VisualizationViewer />
-          </TabContainer>
+          <VisualizationViewer className={styles.visualization_viewer} />
           <TabContainer className={styles.editor_tab_container} titles={editorTitles} tabIndex={editorTabIndex}
                         onChangeTabIndex={tabIndex => this.handleChangeEditorTabIndex(tabIndex)}>
             {
diff --git a/src/frontend/components/App/stylesheet.scss b/src/frontend/components/App/stylesheet.scss
index 165d7e9..34d3b5a 100644
--- a/src/frontend/components/App/stylesheet.scss
+++ b/src/frontend/components/App/stylesheet.scss
@@ -55,6 +55,10 @@ button {
   .workspace {
     flex: 1;
 
+    .visualization_viewer {
+      background-color: $theme-dark;
+    }
+
     .editor_tab_container {
       .input_title {
         input {
@@ -75,4 +79,4 @@ button {
     right: 0;
     z-index: 99;
   }
-}
\ No newline at end of file
+}
diff --git a/src/frontend/components/Button/index.jsx b/src/frontend/components/Button/index.jsx
index 1285f04..d0a0364 100644
--- a/src/frontend/components/Button/index.jsx
+++ b/src/frontend/components/Button/index.jsx
@@ -2,6 +2,7 @@ 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 './stylesheet.scss';
@@ -22,7 +23,7 @@ class Button extends React.Component {
   }
 
   render() {
-    let { className, children, to, href, onClick, icon, reverse, selected, disabled, primary, active, confirmNeeded, ...rest } = this.props;
+    let { className, children, to, href, onClick, icon, reverse, selected, disabled, primary, active, confirmNeeded, inProgress, ...rest } = this.props;
     const { confirming } = this.state;
 
     if (confirmNeeded) {
@@ -54,7 +55,8 @@ class Button extends React.Component {
           typeof icon === 'string' ?
             <div className={classes(styles.icon, styles.image)} key="icon"
                  style={{ backgroundImage: `url(${icon})` }} /> :
-            <FontAwesomeIcon className={styles.icon} fixedWidth icon={icon} key="icon" />
+            <FontAwesomeIcon className={styles.icon} fixedWidth icon={inProgress ? faSpinner : icon} spin={inProgress}
+                             key="icon" />
         ),
         children,
       ],
diff --git a/src/frontend/components/Button/stylesheet.scss b/src/frontend/components/Button/stylesheet.scss
index 1b6cc85..a99db85 100644
--- a/src/frontend/components/Button/stylesheet.scss
+++ b/src/frontend/components/Button/stylesheet.scss
@@ -54,7 +54,7 @@
       font-weight: bold;
 
       .icon {
-        color: #00e676;
+        color: $color-active;
       }
     }
   }
@@ -76,4 +76,4 @@
   &.confirming {
     color: $color-alert;
   }
-}
\ No newline at end of file
+}
diff --git a/src/frontend/components/CodeEditor/index.jsx b/src/frontend/components/CodeEditor/index.jsx
index 49f8da0..4568a88 100644
--- a/src/frontend/components/CodeEditor/index.jsx
+++ b/src/frontend/components/CodeEditor/index.jsx
@@ -10,7 +10,6 @@ import 'brace/theme/tomorrow_night_eighties';
 import 'brace/ext/searchbox';
 import faTrashAlt from '@fortawesome/fontawesome-free-solid/faTrashAlt';
 import faUser from '@fortawesome/fontawesome-free-solid/faUser';
-import { tracerManager } from '/core';
 import { classes, extension } from '/common/util';
 import { actions } from '/reducers';
 import { connect } from 'react-redux';
@@ -18,58 +17,22 @@ import { languages } from '/common/config';
 import { Button, Ellipsis } from '/components';
 import styles from './stylesheet.scss';
 
-@connect(({ current, env }) => ({ current, env }), actions)
+@connect(({ current, env, player }) => ({ current, env, player }), actions)
 class CodeEditor extends React.Component {
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      lineMarker: null,
-    };
-  }
-
   componentDidMount() {
-    const { file } = this.props;
-    tracerManager.setFile(file, true);
-
-    tracerManager.setOnUpdateLineIndicator(lineIndicator => this.setState({ lineMarker: this.createLineMarker(lineIndicator) }));
-  }
-
-  componentWillReceiveProps(nextProps) {
-    const { file } = nextProps;
-    if (file !== this.props.file) {
-      tracerManager.setFile(file, extension(file.name) === 'js');
-    }
-  }
-
-  componentWillUnmount() {
-    tracerManager.setOnUpdateLineIndicator(null);
-  }
-
-  createLineMarker(lineIndicator) {
-    if (lineIndicator === null) return null;
-    const { lineNumber, cursor } = lineIndicator;
-    return {
-      startRow: lineNumber,
-      startCol: 0,
-      endRow: lineNumber,
-      endCol: Infinity,
-      className: styles.current_line_marker,
-      type: 'line',
-      inFront: true,
-      _key: cursor,
-    };
+    this.props.shouldBuild();
   }
 
   handleChangeCode(code) {
     const { file } = this.props;
     this.props.modifyFile({ ...file, content: code });
+    if (extension(file.name) === 'md') this.props.shouldBuild();
   }
 
   render() {
     const { className, file, onDeleteFile } = this.props;
     const { user } = this.props.env;
-    const { lineMarker } = this.state;
+    const { lineIndicator } = this.props.player;
 
     const fileExt = extension(file.name);
     const language = languages.find(language => language.ext === fileExt);
@@ -84,7 +47,16 @@ class CodeEditor extends React.Component {
           name="code_editor"
           editorProps={{ $blockScrolling: true }}
           onChange={code => this.handleChangeCode(code)}
-          markers={lineMarker ? [lineMarker] : []}
+          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={file.content} />
         <div className={classes(styles.contributors_viewer, className)}>
           <span className={classes(styles.contributor, styles.label)}>Contributed by</span>
diff --git a/src/frontend/components/Header/index.jsx b/src/frontend/components/Header/index.jsx
index c122337..4004637 100644
--- a/src/frontend/components/Header/index.jsx
+++ b/src/frontend/components/Header/index.jsx
@@ -1,6 +1,5 @@
 import React from 'react';
 import { connect } from 'react-redux';
-import InputRange from 'react-input-range';
 import AutosizeInput from 'react-input-autosize';
 import screenfull from 'screenfull';
 import Promise from 'bluebird';
@@ -8,9 +7,6 @@ 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 faPlay from '@fortawesome/fontawesome-free-solid/faPlay';
-import faChevronLeft from '@fortawesome/fontawesome-free-solid/faChevronLeft';
-import faPause from '@fortawesome/fontawesome-free-solid/faPause';
 import faExpandArrowsAlt from '@fortawesome/fontawesome-free-solid/faExpandArrowsAlt';
 import faGithub from '@fortawesome/fontawesome-free-brands/faGithub';
 import faTrashAlt from '@fortawesome/fontawesome-free-solid/faTrashAlt';
@@ -21,29 +17,11 @@ import { GitHubApi } from '/apis';
 import { classes, refineGist } from '/common/util';
 import { actions } from '/reducers';
 import { languages } from '/common/config';
-import { Button, Ellipsis, ListItem } from '/components';
-import { tracerManager } from '/core';
+import { Button, Ellipsis, ListItem, Player } from '/components';
 import styles from './stylesheet.scss';
 
 @connect(({ current, env }) => ({ current, env }), actions)
 class Header extends React.Component {
-  constructor(props) {
-    super(props);
-
-    const { interval, paused, started } = tracerManager;
-    this.state = {
-      interval, paused, started,
-    };
-  }
-
-  componentDidMount() {
-    tracerManager.setOnUpdateStatus(update => this.setState(update));
-  }
-
-  componentWillUnmount() {
-    tracerManager.setOnUpdateStatus(null);
-  }
-
   handleClickFullScreen() {
     if (screenfull.enabled) {
       if (screenfull.isFullscreen) {
@@ -97,12 +75,10 @@ class Header extends React.Component {
   }
 
   render() {
-    const { interval, paused, started } = this.state;
-    const { className, onClickTitleBar, navigatorOpened, onAction, gistSaved } = this.props;
+    const { className, onClickTitleBar, navigatorOpened, gistSaved, file } = this.props;
     const { gistId, titles } = this.props.current;
     const { ext, user } = this.props.env;
 
-    // TODO: remove the 'run' button and add 'build' and 'play' buttons
     return (
       <header className={classes(styles.header, className)}>
         <div className={styles.row}>
@@ -159,42 +135,7 @@ class Header extends React.Component {
               </div>
             </Button>
           </div>
-          <div className={styles.section} onClick={onAction}>
-            {
-              started ? (
-                <Button icon={faPlay} primary onClick={() => tracerManager.run()} active>Rerun</Button>
-              ) : (
-                <Button icon={faPlay} primary onClick={() => tracerManager.run()}>Run</Button>
-              )
-            }
-            <Button icon={faChevronLeft} primary disabled={!started}
-                    onClick={() => tracerManager.prev()}>Prev</Button>
-            {
-              paused ? (
-                <Button icon={faPause} primary onClick={() => tracerManager.resume()} active>Resume</Button>
-              ) : (
-                <Button icon={faPause} primary disabled={!started}
-                        onClick={() => tracerManager.pause()}>Pause</Button>
-              )
-            }
-            <Button icon={faCaretRight} reverse primary disabled={!started}
-                    onClick={() => tracerManager.next()}>Next</Button>
-            <div className={styles.interval}>
-              Speed
-              <InputRange
-                classNames={{
-                  inputRange: styles.range,
-                  labelContainer: styles.range_label_container,
-                  slider: styles.range_slider,
-                  track: styles.range_track,
-                }}
-                maxValue={2000}
-                minValue={100}
-                step={100}
-                value={interval}
-                onChange={interval => tracerManager.setInterval(interval)} />
-            </div>
-          </div>
+          <Player className={styles.section} file={file} />
         </div>
       </header>
     );
diff --git a/src/frontend/components/Header/stylesheet.scss b/src/frontend/components/Header/stylesheet.scss
index ee52ce3..ea55905 100644
--- a/src/frontend/components/Header/stylesheet.scss
+++ b/src/frontend/components/Header/stylesheet.scss
@@ -36,51 +36,6 @@
         }
       }
 
-      .interval {
-        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;
-          }
-        }
-      }
-
       .btn_dropdown {
         position: relative;
         font-weight: bold;
@@ -110,4 +65,4 @@
       }
     }
   }
-}
\ No newline at end of file
+}
diff --git a/src/frontend/components/MarkdownViewer/index.jsx b/src/frontend/components/MarkdownViewer/index.jsx
deleted file mode 100644
index f2d2e23..0000000
--- a/src/frontend/components/MarkdownViewer/index.jsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from 'react';
-import ReactMarkdown from 'react-markdown'
-import { classes } from '/common/util';
-import styles from './stylesheet.scss';
-
-class MarkdownViewer extends React.Component {
-  render() {
-    const { className, source, onClickLink } = this.props;
-
-    const link = ({ href, ...rest }) => {
-      return !onClickLink || /^https?:\/\//i.test(href) ? (
-        <a href={href} rel="noopener" target="_blank" {...rest} />
-      ) : (
-        <a onClick={() => onClickLink(href)} {...rest} />
-      );
-    };
-
-    const image = ({ src, ...rest }) => {
-      let newSrc;
-      const codecogs = 'https://latex.codecogs.com/svg.latex?';
-      if (src.startsWith(codecogs)) {
-        const latex = src.substring(codecogs.length);
-        newSrc = `${codecogs}\\color{White}${latex}`;
-      } else {
-        newSrc = src;
-      }
-      return <img src={newSrc} {...rest} />
-    };
-
-    return (
-      <ReactMarkdown className={classes(styles.markdown_viewer, className)} source={source}
-                     renderers={{ link, image }} escapeHtml={false} />
-    );
-  }
-}
-
-export default MarkdownViewer;
-
diff --git a/src/frontend/components/Player/index.jsx b/src/frontend/components/Player/index.jsx
new file mode 100644
index 0000000..94f3bec
--- /dev/null
+++ b/src/frontend/components/Player/index.jsx
@@ -0,0 +1,174 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import Promise from 'bluebird';
+import InputRange from 'react-input-range';
+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 { CompileError } from '/common/error';
+import { actions } from '/reducers';
+import { Button } from '/components';
+import styles from './stylesheet.scss';
+import ProgressBar from '../ProgressBar';
+
+@connect(({ player }) => ({ player }), actions)
+class Player extends React.Component {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      interval: 500,
+      playing: false,
+      building: false,
+    };
+
+    this.reset();
+  }
+
+  componentDidMount() {
+    const { file } = this.props;
+    this.build(file);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    const { file } = nextProps;
+    const { buildAt } = nextProps.player;
+    if (buildAt !== this.props.player.buildAt) {
+      this.build(file);
+    }
+  }
+
+  reset(traces = []) {
+    const chunks = [{
+      traces: [],
+      lineNumber: undefined,
+    }];
+    while (traces.length) {
+      const trace = traces.shift();
+      if (trace.method === 'delay') {
+        const [lineNumber] = trace.args;
+        chunks[chunks.length - 1].lineNumber = lineNumber;
+        chunks.push({
+          traces: [],
+          lineNumber: undefined,
+        });
+      } else {
+        chunks[chunks.length - 1].traces.push(trace);
+      }
+    }
+    this.props.setChunks(chunks);
+    this.props.setCursor(0);
+    this.pause();
+    this.props.setLineIndicator(undefined);
+  }
+
+  build(file) {
+    if (!file) return;
+    this.setState({ building: true });
+    this.reset();
+    const ext = extension(file.name);
+    (ext in TracerApi ?
+      TracerApi[ext]({ code: file.content }) :
+      Promise.reject(new CompileError('Language Not Supported')))
+      .then(traces => this.reset(traces))
+      .then(() => this.next())
+      .catch(error => this.handleError(error))
+      .finally(() => this.setState({ building: false }));
+  }
+
+  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)) {
+      this.timer = window.setTimeout(() => this.resume(), this.state.interval);
+      this.setState({ playing: true });
+    }
+  }
+
+  pause() {
+    if (this.timer) {
+      window.clearInterval(this.timer);
+      this.timer = null;
+      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;
+  }
+
+  handleError(error) {
+    console.error(error);
+    this.props.showErrorToast({ name: error.name, message: error.message });
+  }
+
+  handleChangeInterval(interval) {
+    this.setState({ interval });
+  }
+
+  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, file } = this.props;
+    const { chunks, cursor } = this.props.player;
+    const { interval, playing, building } = this.state;
+
+    return (
+      <div className={classes(styles.player, className)}>
+        <Button icon={faWrench} primary disabled={building} inProgress={building} onClick={() => this.build(file)}>
+          {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.interval}>
+          Speed
+          <InputRange
+            classNames={{
+              inputRange: styles.range,
+              labelContainer: styles.range_label_container,
+              slider: styles.range_slider,
+              track: styles.range_track,
+            }} maxValue={2000} minValue={100} step={100} value={interval}
+            onChange={interval => this.handleChangeInterval(interval)} />
+        </div>
+      </div>
+    );
+  }
+}
+
+export default Player;
diff --git a/src/frontend/components/Player/stylesheet.scss b/src/frontend/components/Player/stylesheet.scss
new file mode 100644
index 0000000..8aeba88
--- /dev/null
+++ b/src/frontend/components/Player/stylesheet.scss
@@ -0,0 +1,52 @@
+@import "~/common/stylesheet/index";
+
+.player {
+  .progress_bar {
+    width: 160px;
+  }
+
+  .interval {
+    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;
+      }
+    }
+  }
+}
diff --git a/src/frontend/components/ProgressBar/index.jsx b/src/frontend/components/ProgressBar/index.jsx
new file mode 100644
index 0000000..96c9ceb
--- /dev/null
+++ b/src/frontend/components/ProgressBar/index.jsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import { classes } from '/common/util';
+import styles from './stylesheet.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;
+    console.log(this.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;
diff --git a/src/frontend/components/ProgressBar/stylesheet.scss b/src/frontend/components/ProgressBar/stylesheet.scss
new file mode 100644
index 0000000..e5663b9
--- /dev/null
+++ b/src/frontend/components/ProgressBar/stylesheet.scss
@@ -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;
+    }
+  }
+}
diff --git a/src/frontend/components/VisualizationViewer/index.jsx b/src/frontend/components/VisualizationViewer/index.jsx
index 880b141..2669c0c 100644
--- a/src/frontend/components/VisualizationViewer/index.jsx
+++ b/src/frontend/components/VisualizationViewer/index.jsx
@@ -1,44 +1,120 @@
 import React from 'react';
+import { connect } from 'react-redux';
 import { classes } from '/common/util';
 import { ResizableContainer } from '/components';
+import { actions } from '/reducers';
 import styles from './stylesheet.scss';
-import { tracerManager } from '../../core';
+import { Array1DData, Array2DData, ChartData, Data, GraphData, LogData, MarkdownData } from '/core/datas';
 
+@connect(({ player }) => ({ player }), actions)
 class VisualizationViewer extends React.Component {
   constructor(props) {
     super(props);
 
     this.state = {
-      renderers: [],
-      renderersWeights: [],
+      dataWeights: {},
     };
+
+    this.datas = [];
   }
 
   componentDidMount() {
-    // TODO: rendereres should remain resized
-    tracerManager.setOnChangeRenderers(renderers => {
-      const renderersWeights = renderers.map(() => 1);
-      this.setState({ renderers, renderersWeights });
+    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.datas = [];
+      applyingChunks = chunks.slice(0, cursor);
+    }
+    applyingChunks.forEach(chunk => this.applyChunk(chunk));
+
+    const dataWeights = chunks === oldChunks ? { ...this.state.dataWeights } : {};
+    this.datas.forEach(data => {
+      if (!(data.tracerKey in dataWeights)) {
+        dataWeights[data.tracerKey] = 1;
+      }
     });
+    this.setState({ dataWeights });
+
+    const lastChunk = applyingChunks[applyingChunks.length - 1];
+    if (lastChunk && lastChunk.lineNumber !== undefined) {
+      this.props.setLineIndicator({ lineNumber: lastChunk.lineNumber, cursor });
+    } else {
+      this.props.setLineIndicator(undefined);
+    }
   }
 
-  componentWillUnmount() {
-    tracerManager.setOnChangeRenderers(null);
+  addTracer(className, tracerKey, title) {
+    const DataClass = {
+      Tracer: Data,
+      MarkdownTracer: MarkdownData,
+      LogTracer: LogData,
+      Array2DTracer: Array2DData,
+      Array1DTracer: Array1DData,
+      ChartTracer: ChartData,
+      GraphTracer: GraphData,
+    }[className];
+    const data = new DataClass(tracerKey, title, this.datas);
+    this.datas.push(data);
   }
 
-  handleChangeRenderersWeights(renderersWeights) {
-    this.setState({ renderersWeights });
+  applyTrace(trace) {
+    const { tracerKey, method, args } = trace;
+    try {
+      if (method === 'construct') {
+        const [className, title] = args;
+        this.addTracer(className, tracerKey, title);
+      } else {
+        const data = this.datas.find(data => data.tracerKey === tracerKey);
+        data[method](...args);
+      }
+    } catch (error) {
+      this.handleError(error);
+    }
+  }
+
+  handleError(error) {
+    console.error(error);
+    this.props.showErrorToast({ name: error.name, message: error.message });
+  }
+
+  applyChunk(chunk) {
+    chunk.traces.forEach(trace => this.applyTrace(trace));
+  }
+
+  handleChangeWeights(weights) {
+    const dataWeights = {};
+    weights.forEach((weight, i) => {
+      dataWeights[this.datas[i].tracerKey] = weight;
+    });
+    this.setState({ dataWeights });
   }
 
   render() {
     const { className } = this.props;
-    const { renderers, renderersWeights } = this.state;
+    const { dataWeights } = this.state;
 
     return (
-      <ResizableContainer className={classes(styles.visualization_viewer, className)} weights={renderersWeights}
-                          visibles={renderers.map(() => true)}
-                          onChangeWeights={weights => this.handleChangeRenderersWeights(weights)}>
-        {renderers}
+      <ResizableContainer className={classes(styles.visualization_viewer, className)}
+                          weights={this.datas.map(data => dataWeights[data.tracerKey])}
+                          visibles={this.datas.map(() => true)}
+                          onChangeWeights={weights => this.handleChangeWeights(weights)}>
+        {
+          this.datas.map(data => data.render())
+        }
       </ResizableContainer>
     );
   }
diff --git a/src/frontend/components/index.js b/src/frontend/components/index.js
index 15ad653..23b9849 100644
--- a/src/frontend/components/index.js
+++ b/src/frontend/components/index.js
@@ -6,8 +6,8 @@ export { default as Ellipsis } from './Ellipsis';
 export { default as ExpandableListItem } from './ExpandableListItem';
 export { default as Header } from './Header';
 export { default as ListItem } from './ListItem';
-export { default as MarkdownViewer } from './MarkdownViewer';
 export { default as Navigator } from './Navigator';
+export { default as Player } from './Player';
 export { default as ResizableContainer } from './ResizableContainer';
 export { default as TabContainer } from './TabContainer';
 export { default as ToastContainer } from './ToastContainer';
diff --git a/src/frontend/core/datas/Array1DData.js b/src/frontend/core/datas/Array1DData.js
index c2fa0b0..5bda332 100644
--- a/src/frontend/core/datas/Array1DData.js
+++ b/src/frontend/core/datas/Array1DData.js
@@ -1,7 +1,11 @@
 import { Array2DData } from '/core/datas';
-import { tracerManager } from '/core';
+import { Array1DRenderer } from '/core/renderers';
 
 class Array1DData extends Array2DData {
+  getRendererClass() {
+    return Array1DRenderer;
+  }
+
   init() {
     super.init();
     this.chartData = null;
@@ -30,7 +34,7 @@ class Array1DData extends Array2DData {
   }
 
   chart(tracerKey) {
-    this.chartData = tracerKey ? tracerManager.datas[tracerKey] : null;
+    this.chartData = tracerKey ? this.findData(tracerKey) : null;
     this.syncChartData();
   }
 
@@ -39,4 +43,4 @@ class Array1DData extends Array2DData {
   }
 }
 
-export default Array1DData;
\ No newline at end of file
+export default Array1DData;
diff --git a/src/frontend/core/datas/Array2DData.js b/src/frontend/core/datas/Array2DData.js
index 7f8aa54..416d20c 100644
--- a/src/frontend/core/datas/Array2DData.js
+++ b/src/frontend/core/datas/Array2DData.js
@@ -1,6 +1,11 @@
 import { Data } from '/core/datas';
+import { Array2DRenderer } from '/core/renderers';
 
 class Array2DData extends Data {
+  getRendererClass() {
+    return Array2DRenderer;
+  }
+
   set(array2d = []) {
     this.data = [];
     for (const array1d of array2d) {
@@ -60,4 +65,4 @@ class Array2DData extends Data {
   }
 }
 
-export default Array2DData;
\ No newline at end of file
+export default Array2DData;
diff --git a/src/frontend/core/datas/ChartData.js b/src/frontend/core/datas/ChartData.js
index 5e81b25..ab8149b 100644
--- a/src/frontend/core/datas/ChartData.js
+++ b/src/frontend/core/datas/ChartData.js
@@ -1,6 +1,10 @@
 import { Array1DData } from '/core/datas';
+import { ChartRenderer } from '/core/renderers';
 
 class ChartData extends Array1DData {
+  getRendererClass() {
+    return ChartRenderer;
+  }
 }
 
-export default ChartData;
\ No newline at end of file
+export default ChartData;
diff --git a/src/frontend/core/datas/Data.js b/src/frontend/core/datas/Data.js
deleted file mode 100644
index 3a5d31a..0000000
--- a/src/frontend/core/datas/Data.js
+++ /dev/null
@@ -1,30 +0,0 @@
-class Data {
-  constructor() {
-    this.init();
-    this.reset();
-  }
-
-  init() {
-  }
-
-  setOnRender(onRender) {
-    this.onRender = onRender;
-    this.render();
-  }
-
-  render() {
-    if (this.onRender) this.onRender();
-  }
-
-  set() {
-  }
-
-  reset() {
-    this.set();
-  }
-
-  delay() {
-  }
-}
-
-export default Data;
\ No newline at end of file
diff --git a/src/frontend/core/datas/Data.jsx b/src/frontend/core/datas/Data.jsx
new file mode 100644
index 0000000..c090ec0
--- /dev/null
+++ b/src/frontend/core/datas/Data.jsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import { Renderer } from '/core/renderers';
+
+class Data {
+  constructor(tracerKey, title, datas) {
+    this.tracerKey = tracerKey;
+    this.title = title;
+    this.datas = datas;
+    this.init();
+    this.reset();
+  }
+
+  findData(tracerKey) {
+    return this.datas.find(data => data.tracerKey === tracerKey);
+  }
+
+  getRendererClass() {
+    return Renderer;
+  }
+
+  init() {
+  }
+
+  render() {
+    const RendererClass = this.getRendererClass();
+    return (
+      <RendererClass key={this.tracerKey} title={this.title} data={this} />
+    );
+  }
+
+  set() {
+  }
+
+  reset() {
+    this.set();
+  }
+}
+
+export default Data;
diff --git a/src/frontend/core/datas/GraphData.js b/src/frontend/core/datas/GraphData.js
index 1f56766..1f887ce 100644
--- a/src/frontend/core/datas/GraphData.js
+++ b/src/frontend/core/datas/GraphData.js
@@ -1,8 +1,12 @@
 import { Data } from '/core/datas';
 import { distance } from '/common/util';
-import { tracerManager } from '/core';
+import { GraphRenderer } from '/core/renderers';
 
 class GraphData extends Data {
+  getRendererClass() {
+    return GraphRenderer;
+  }
+
   init() {
     super.init();
     this.dimensions = {
@@ -221,8 +225,8 @@ class GraphData extends Data {
   }
 
   log(tracerKey) {
-    this.logData = tracerKey ? tracerManager.datas[tracerKey] : null;
+    this.logData = tracerKey ? this.findData(tracerKey) : null;
   }
 }
 
-export default GraphData;
\ No newline at end of file
+export default GraphData;
diff --git a/src/frontend/core/datas/LogData.js b/src/frontend/core/datas/LogData.js
index 98bca09..3549ca6 100644
--- a/src/frontend/core/datas/LogData.js
+++ b/src/frontend/core/datas/LogData.js
@@ -1,6 +1,11 @@
 import { Data } from '/core/datas';
+import { LogRenderer } from '/core/renderers';
 
 class LogData extends Data {
+  getRendererClass() {
+    return LogRenderer;
+  }
+
   set(messages = []) {
     this.messages = messages;
     super.set();
@@ -11,4 +16,4 @@ class LogData extends Data {
   }
 }
 
-export default LogData;
\ No newline at end of file
+export default LogData;
diff --git a/src/frontend/core/datas/MarkdownData.js b/src/frontend/core/datas/MarkdownData.js
new file mode 100644
index 0000000..50ff994
--- /dev/null
+++ b/src/frontend/core/datas/MarkdownData.js
@@ -0,0 +1,15 @@
+import { Data } from '/core/datas';
+import { MarkdownRenderer } from '/core/renderers';
+
+class MarkdownData extends Data {
+  getRendererClass() {
+    return MarkdownRenderer;
+  }
+
+  set(markdown = '') {
+    this.markdown = markdown;
+    super.set();
+  }
+}
+
+export default MarkdownData;
diff --git a/src/frontend/core/datas/index.js b/src/frontend/core/datas/index.js
index ea4b89f..e8dc772 100644
--- a/src/frontend/core/datas/index.js
+++ b/src/frontend/core/datas/index.js
@@ -1,4 +1,5 @@
 export { default as Data } from './Data';
+export { default as MarkdownData } from './MarkdownData';
 export { default as LogData } from './LogData';
 export { default as Array2DData } from './Array2DData';
 export { default as Array1DData } from './Array1DData';
diff --git a/src/frontend/core/index.js b/src/frontend/core/index.js
deleted file mode 100644
index 105770f..0000000
--- a/src/frontend/core/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default as tracerManager } from './tracerManager';
\ No newline at end of file
diff --git a/src/frontend/core/renderers/MarkdownRenderer/index.jsx b/src/frontend/core/renderers/MarkdownRenderer/index.jsx
new file mode 100644
index 0000000..e8a29c4
--- /dev/null
+++ b/src/frontend/core/renderers/MarkdownRenderer/index.jsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { Renderer } from '/core/renderers';
+import styles from './stylesheet.scss';
+import ReactMarkdown from 'react-markdown';
+
+class MarkdownRenderer extends Renderer {
+  renderData() {
+    const { markdown } = this.props.data;
+
+    const link = ({ href, ...rest }) => {
+      return (
+        <a href={href} rel="noopener" target="_blank" {...rest} />
+      );
+    };
+
+    const image = ({ src, ...rest }) => {
+      let newSrc;
+      const codecogs = 'https://latex.codecogs.com/svg.latex?';
+      if (src.startsWith(codecogs)) {
+        const latex = src.substring(codecogs.length);
+        newSrc = `${codecogs}\\color{White}${latex}`;
+      } else {
+        newSrc = src;
+      }
+      return <img src={newSrc} {...rest} />;
+    };
+
+    return (
+      <ReactMarkdown className={styles.markdown} source={markdown} renderers={{ link, image }} escapeHtml={false} />
+    );
+  }
+}
+
+export default MarkdownRenderer;
+
diff --git a/src/frontend/components/MarkdownViewer/stylesheet.scss b/src/frontend/core/renderers/MarkdownRenderer/stylesheet.scss
similarity index 68%
rename from src/frontend/components/MarkdownViewer/stylesheet.scss
rename to src/frontend/core/renderers/MarkdownRenderer/stylesheet.scss
index c8b8a7f..581639d 100644
--- a/src/frontend/components/MarkdownViewer/stylesheet.scss
+++ b/src/frontend/core/renderers/MarkdownRenderer/stylesheet.scss
@@ -1,15 +1,15 @@
 @import "~/common/stylesheet/index";
 
-.markdown_viewer {
+.markdown {
   display: flex;
   flex-direction: column;
-  align-items: stretch;
-  padding: 16px;
+  flex: 1;
+  align-self: stretch;
+  padding: 24px;
   font-size: $font-size-large;
   overflow-y: auto;
 
   a {
     text-decoration: underline;
-    cursor: pointer;
   }
-}
\ No newline at end of file
+}
diff --git a/src/frontend/core/renderers/Renderer/index.jsx b/src/frontend/core/renderers/Renderer/index.jsx
index 6ef937e..0f56e86 100644
--- a/src/frontend/core/renderers/Renderer/index.jsx
+++ b/src/frontend/core/renderers/Renderer/index.jsx
@@ -21,39 +21,9 @@ class Renderer extends React.Component {
     this.zoomMin = 1 / 20;
   }
 
-  componentDidMount() {
-    const { data } = this.props;
-    this.mountData(data);
-  }
-
-  componentWillUnmount() {
-    const { data } = this.props;
-    this.unmountData(data);
-  }
-
-  componentWillReceiveProps(nextProps) {
-    const { data } = nextProps;
-    if (data !== this.props.data) {
-      this.unmountData(this.props.data);
-      this.mountData(data);
-    }
-  }
-
-  shouldComponentUpdate() {
-    return false;
-  }
-
   componentDidUpdate(prevProps, prevState, snapshot) {
   }
 
-  mountData(data) {
-    data.setOnRender(() => this.refresh());
-  }
-
-  unmountData(data) {
-    data.setOnRender(null);
-  }
-
   handleMouseDown(e) {
     const { clientX, clientY } = e;
     this.lastX = clientX;
diff --git a/src/frontend/core/renderers/Renderer/stylesheet.scss b/src/frontend/core/renderers/Renderer/stylesheet.scss
index 1829dfd..419e2c9 100644
--- a/src/frontend/core/renderers/Renderer/stylesheet.scss
+++ b/src/frontend/core/renderers/Renderer/stylesheet.scss
@@ -3,7 +3,7 @@
 .renderer {
   position: relative;
   flex: 1;
-  flex-direction: column-reverse;
+  flex-direction: column;
   display: flex;
   align-items: center;
   justify-content: center;
@@ -21,4 +21,4 @@
     padding: 4px 6px;
     font-size: $font-size-large;
   }
-}
\ No newline at end of file
+}
diff --git a/src/frontend/core/renderers/index.js b/src/frontend/core/renderers/index.js
index 4d2b9f7..ead0460 100644
--- a/src/frontend/core/renderers/index.js
+++ b/src/frontend/core/renderers/index.js
@@ -1,4 +1,5 @@
 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';
diff --git a/src/frontend/core/tracerManager.jsx b/src/frontend/core/tracerManager.jsx
deleted file mode 100644
index 939b1c7..0000000
--- a/src/frontend/core/tracerManager.jsx
+++ /dev/null
@@ -1,224 +0,0 @@
-import React from 'react';
-import Promise from 'bluebird';
-import { extension } from '/common/util';
-import { Array1DData, Array2DData, ChartData, Data, GraphData, LogData } from '/core/datas';
-import { Array1DRenderer, Array2DRenderer, ChartRenderer, GraphRenderer, LogRenderer, Renderer } from '/core/renderers';
-import { TracerApi } from '/apis';
-import { CompileError } from '/common/error';
-
-class TracerManager {
-  constructor() {
-    this.interval = 500;
-    this.paused = false;
-    this.started = false;
-    this.lineIndicator = null;
-    this.file = { name: '', content: '', contributors: [] };
-    this.reset();
-  }
-
-  setOnChangeRenderers(onChangeRenderers) {
-    this.onChangeRenderers = onChangeRenderers;
-    if (this.onChangeRenderers) this.onChangeRenderers(this.renderers);
-  }
-
-  setOnUpdateStatus(onUpdateStatus) {
-    this.onUpdateStatus = onUpdateStatus;
-    if (this.onUpdateStatus) {
-      const { interval, paused, started } = this;
-      this.onUpdateStatus({ interval, paused, started });
-    }
-  }
-
-  setOnUpdateLineIndicator(onUpdateLineIndicator) {
-    this.onUpdateLineIndicator = onUpdateLineIndicator;
-    if (this.onUpdateLineIndicator) this.onUpdateLineIndicator(this.lineIndicator);
-  }
-
-  setOnError(onError) {
-    this.onError = onError;
-  }
-
-  render() {
-    Object.values(this.datas).forEach(data => data.render());
-  }
-
-  setInterval(interval) {
-    this.interval = interval;
-    if (this.onUpdateStatus) this.onUpdateStatus({ interval });
-  }
-
-  setPaused(paused) {
-    this.paused = paused;
-    if (this.onUpdateStatus) this.onUpdateStatus({ paused });
-  }
-
-  setStarted(started) {
-    this.started = started;
-    if (this.onUpdateStatus) this.onUpdateStatus({ started });
-  }
-
-  setLineIndicator(lineIndicator) {
-    this.lineIndicator = lineIndicator;
-    if (this.onUpdateLineIndicator) this.onUpdateLineIndicator(lineIndicator);
-  }
-
-  setFile(file, initialRun) {
-    this.file = file;
-    if (initialRun) this.runInitial();
-  }
-
-  reset(traces = []) {
-    this.traces = traces;
-    this.resetCursor();
-    this.stopTimer();
-    this.setPaused(false);
-    this.setStarted(false);
-    this.setLineIndicator(null);
-  }
-
-  resetCursor() {
-    this.renderers = [];
-    this.datas = {};
-    this.cursor = 0;
-    this.chunkCursor = 0;
-    if (this.onChangeRenderers) this.onChangeRenderers(this.renderers);
-  }
-
-  addTracer(className, tracerKey, title) {
-    const [DataClass, RendererClass] = {
-      Tracer: [Data, Renderer],
-      LogTracer: [LogData, LogRenderer],
-      Array2DTracer: [Array2DData, Array2DRenderer],
-      Array1DTracer: [Array1DData, Array1DRenderer],
-      ChartTracer: [ChartData, ChartRenderer],
-      GraphTracer: [GraphData, GraphRenderer],
-    }[className];
-    const data = new DataClass();
-    this.datas[tracerKey] = data;
-    const renderer = (
-      <RendererClass key={tracerKey} title={title} data={data} wsProps={{ fixed: true }} />
-    );
-    this.renderers.push(renderer);
-    if (this.onChangeRenderers) this.onChangeRenderers(this.renderers);
-  }
-
-  applyTrace() {
-    if (this.cursor >= this.traces.length) return false;
-    const trace = this.traces[this.cursor++];
-    const { tracerKey, method, args } = trace;
-    try {
-      if (method === 'construct') {
-        const [className, title] = args;
-        this.addTracer(className, tracerKey, title);
-        return true;
-      } else {
-        const data = this.datas[tracerKey];
-        const delay = method === 'delay';
-        const newArgs = [...args];
-        if (delay) {
-          const lineNumber = newArgs.shift();
-          this.setLineIndicator({ lineNumber, cursor: this.cursor });
-        }
-        data[method](...newArgs);
-        return !delay;
-      }
-    } catch (error) {
-      if (this.started) this.handleError(error);
-      return false;
-    }
-  }
-
-  applyTraceChunk(render = true) {
-    if (this.cursor >= this.traces.length) return false;
-    while (this.applyTrace()) ;
-    this.chunkCursor++;
-    if (render) this.render();
-    return true;
-  }
-
-  startTimer() {
-    this.stopTimer();
-    if (this.applyTraceChunk()) {
-      this.timer = window.setTimeout(() => this.startTimer(), this.interval);
-    } else {
-      this.setPaused(false);
-      this.setStarted(false);
-    }
-  }
-
-  stopTimer() {
-    if (this.timer) {
-      window.clearInterval(this.timer);
-      this.timer = null;
-    }
-  }
-
-  execute() {
-    const { name, content } = this.file;
-    const ext = extension(name);
-    if (ext in TracerApi) {
-      return TracerApi[ext]({ code: content });
-    } else {
-      return Promise.reject(new CompileError('Language Not Supported'));
-    }
-  }
-
-  runInitial() {
-    this.reset();
-    this.render();
-    this.execute()
-      .then(traces => {
-        this.reset(traces);
-        this.applyTraceChunk();
-      })
-      .catch(error => {
-      });
-  }
-
-  run() {
-    this.reset();
-    this.render();
-    this.execute()
-      .then(traces => {
-        this.reset(traces);
-        this.resume();
-        this.setStarted(true);
-      })
-      .catch(error => {
-        this.handleError(error);
-      });
-  }
-
-  prev() {
-    this.pause();
-    const lastChunk = this.chunkCursor - 1;
-    this.resetCursor();
-    do {
-      this.applyTraceChunk(false);
-    } while (this.chunkCursor < lastChunk);
-    this.render();
-  }
-
-  resume() {
-    this.startTimer();
-    this.setPaused(false);
-  }
-
-  pause() {
-    this.stopTimer();
-    this.setPaused(true);
-  }
-
-  next() {
-    this.pause();
-    this.applyTraceChunk();
-  }
-
-  handleError(error) {
-    console.error(error);
-    if (this.onError) this.onError(error);
-  }
-}
-
-const tracerManager = new TracerManager();
-export default tracerManager;
\ No newline at end of file
diff --git a/src/frontend/reducers/index.js b/src/frontend/reducers/index.js
index 1f1dee6..c5a7d91 100644
--- a/src/frontend/reducers/index.js
+++ b/src/frontend/reducers/index.js
@@ -1,16 +1,19 @@
 import { actions as currentActions } from './current';
 import { actions as directoryActions } from './directory';
 import { actions as envActions } from './env';
+import { actions as playerActions } from './player';
 import { actions as toastActions } from './toast';
 
 export { default as current } from './current';
 export { default as directory } from './directory';
 export { default as env } from './env';
+export { default as player } from './player';
 export { default as toast } from './toast';
 
 export const actions = {
   ...currentActions,
   ...directoryActions,
   ...envActions,
+  ...playerActions,
   ...toastActions,
-};
\ No newline at end of file
+};
diff --git a/src/frontend/reducers/player.js b/src/frontend/reducers/player.js
new file mode 100644
index 0000000..ce37dd1
--- /dev/null
+++ b/src/frontend/reducers/player.js
@@ -0,0 +1,34 @@
+import { combineActions, createAction, handleActions } from 'redux-actions';
+
+const prefix = 'PLAYER';
+
+const shouldBuild = createAction(`${prefix}/SHOULD_BUILD`, () => ({ buildAt: Date.now() }));
+const setChunks = createAction(`${prefix}/SET_CHUNKS`, chunks => ({ chunks }));
+const setCursor = createAction(`${prefix}/SET_CURSOR`, cursor => ({ cursor }));
+const setLineIndicator = createAction(`${prefix}/SET_LINE_INDICATOR`, lineIndicator => ({ lineIndicator }));
+
+export const actions = {
+  shouldBuild,
+  setChunks,
+  setCursor,
+  setLineIndicator,
+};
+
+const defaultState = {
+  buildAt: 0,
+  chunks: [],
+  cursor: 0,
+  lineIndicator: undefined,
+};
+
+export default handleActions({
+  [combineActions(
+    shouldBuild,
+    setChunks,
+    setCursor,
+    setLineIndicator,
+  )]: (state, { payload }) => ({
+    ...state,
+    ...payload,
+  }),
+}, defaultState);
-- 
GitLab