├── .gitignore
├── demos
├── index.html
├── time-slicing
│ ├── index.html
│ ├── README.md
│ ├── index.js
│ ├── index.css
│ ├── Clock.js
│ └── Charts.js
├── custom.css
├── components
│ ├── index.js
│ ├── timer.js
│ ├── sourceswitching.js
│ ├── blink.js
│ ├── counter.js
│ ├── crappybird.js
│ └── button.css
├── eleventy.css
└── index.js
├── .babelrc
├── reactive-react
├── index.js
├── updateProperties.js
├── element.js
├── component.js
├── swyxjs.js
├── scheduler.js
└── reconciler.js
├── prototypeAPI.js
├── netlify.toml
├── rollup.config.js
├── package.json
├── README.md
└── dependencygraph.svg
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | yarn.lock
4 | .cache
--------------------------------------------------------------------------------
/demos/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/demos/time-slicing/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/demos/time-slicing/README.md:
--------------------------------------------------------------------------------
1 | note that this isnt actually a demo of time slicing - its just meant to show the performance issues.
2 |
3 | this was originally written as a separate demo from all the rest and that may change.
--------------------------------------------------------------------------------
/demos/custom.css:
--------------------------------------------------------------------------------
1 | body {
2 | background: rgb(2,0,36);
3 | background: linear-gradient(45deg, rgba(2,0,36,1) 0%, rgba(9,9,121,1) 23%, rgba(161,0,0,1) 100%);
4 | }
5 | html {
6 | overflow: scroll;
7 | }
8 | .container {
9 | width: 700px
10 | }
11 |
12 | .titleLink {
13 | display: flex
14 | }
15 |
16 | .titleLink a {
17 | margin-left: 10px;
18 | }
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "transform-object-rest-spread",
4 | "transform-class-properties",
5 | [
6 | "transform-react-jsx",
7 | {}
8 | ]
9 | ],
10 | "presets": [
11 | [
12 | "env",
13 | {
14 | "targets": {
15 | "node": "current"
16 | }
17 | }
18 | ]
19 | ]
20 | }
--------------------------------------------------------------------------------
/reactive-react/index.js:
--------------------------------------------------------------------------------
1 | import { createElement, createHandler } from "./element";
2 | import { Component } from "./component";
3 | import { renderStream } from "./reconciler"
4 | import { mount } from "./scheduler";
5 |
6 | export default {
7 | renderStream,
8 | createElement,
9 | createHandler,
10 | Component,
11 | mount
12 | };
13 |
14 | export { createElement, createHandler, Component,
15 | renderStream,
16 | mount };
17 |
--------------------------------------------------------------------------------
/prototypeAPI.js:
--------------------------------------------------------------------------------
1 |
2 | class Abc extends Component {
3 | source($) {
4 | return Observable.of(1)
5 | }
6 | render($) {
7 | return
8 | }
9 | }
10 |
11 | class LabeledSlider extends Component {
12 | constructor() {
13 | this.myRef = Creat.createRef();
14 | }
15 | source($) {
16 | return this.myRef()
17 | }
18 | render(value, prop$) {
19 | return
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [Settings]
2 | ID = "reactive-react"
3 |
4 | # for more: https://www.netlify.com/docs/netlify-toml-reference/
5 |
6 | [build]
7 | # This is the directory to change to before starting a build.
8 | # base = "project/"
9 | # NOTE: This is where we will look for package.json/.nvmrc/etc, not root.
10 | # This is the directory that you are publishing from (relative to root of your repo)
11 | publish = "demos/dist/"
12 | # This will be your default build command
13 | command = "npm run demo:build"
14 | # This is where we will look for your lambda functions
15 | # functions = "project/functions/"
--------------------------------------------------------------------------------
/demos/components/index.js:
--------------------------------------------------------------------------------
1 | import Timer from './timer'
2 | import Blink from './blink'
3 | import Counter, {Counters} from './counter'
4 | import CrappyBird from './crappybird'
5 | import Source, {SourceSwitching} from './sourceswitching'
6 | import CrazyCharts from '../time-slicing'
7 |
8 | // barrel rollup of all neighbors
9 | // export Timer from './timer'
10 | // export Blink from './blink'
11 | // export Counter, {Counters} from './counter'
12 | // export CrappyBird from './crappybird'
13 | // export Source, {SourceSwitching} from './sourceswitching'
14 | export {Timer, Blink, Counter, Counters, CrappyBird, Source, SourceSwitching, CrazyCharts}
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | // Rollup plugins
2 | import babel from 'rollup-plugin-babel';
3 |
4 | export default {
5 | entry: 'reactive-react/index.js',
6 | dest: 'build/reactive-react.js',
7 | format: 'iife',
8 | sourceMap: 'inline',
9 | moduleName: 'reactive-react',
10 | plugins: [
11 | babel({
12 | babelrc: false,
13 | presets: [['env', { modules: false }]],
14 | plugins: [
15 | "transform-object-rest-spread",
16 | "transform-class-properties",
17 | [
18 | "transform-react-jsx",
19 | {}
20 | ]
21 | ],
22 | exclude: 'node_modules/**',
23 | }),
24 | ],
25 | };
--------------------------------------------------------------------------------
/demos/components/timer.js:
--------------------------------------------------------------------------------
1 | /** @jsx createElement */
2 | import {mount, createElement, Component, createHandler} from '../../reactive-react'
3 | import {Interval, scan, startWith, merge, mapToConstant} from '../../reactive-react/swyxjs'
4 | import Observable from 'zen-observable'
5 |
6 | export default class Timer extends Component {
7 | // demonstrate interval time
8 | initialState = 0
9 | source($) {
10 | const source = Interval(this.props.ms) // tick every second
11 | const reducer = x => x + 0.5 // count up
12 | // source returns an observable
13 | return scan(source, reducer, 0) // from zero
14 | }
15 | render(state, stateMap) {
16 | return number of seconds elapsed: {state}
17 | }
18 | }
--------------------------------------------------------------------------------
/reactive-react/updateProperties.js:
--------------------------------------------------------------------------------
1 | export function updateDomProperties(dom, prevProps, nextProps) {
2 | const isEvent = name => name.startsWith("on");
3 | const isAttribute = name => !isEvent(name) && name != "children";
4 |
5 | // Remove event listeners
6 | Object.keys(prevProps).filter(isEvent).forEach(name => {
7 | const eventType = name.toLowerCase().substring(2);
8 | dom.removeEventListener(eventType, prevProps[name]);
9 | });
10 |
11 | // Remove attributes
12 | Object.keys(prevProps).filter(isAttribute).forEach(name => {
13 | dom[name] = null;
14 | });
15 |
16 | // Set attributes
17 | Object.keys(nextProps).filter(isAttribute).forEach(name => {
18 | dom[name] = nextProps[name];
19 | });
20 |
21 | // Add event listeners
22 | Object.keys(nextProps).filter(isEvent).forEach(name => {
23 | const eventType = name.toLowerCase().substring(2);
24 | dom.addEventListener(eventType, nextProps[name]);
25 | });
26 | }
27 |
--------------------------------------------------------------------------------
/reactive-react/element.js:
--------------------------------------------------------------------------------
1 | import Observable from 'zen-observable'
2 | import { createChangeEmitter } from 'change-emitter'
3 |
4 | export const TEXT_ELEMENT = "TEXT ELEMENT";
5 |
6 | export function createElement(type, config, ...args) {
7 | const props = Object.assign({}, config);
8 | const hasChildren = args.length > 0;
9 | const rawChildren = hasChildren ? [].concat(...args) : [];
10 | props.children = rawChildren
11 | .filter(c => c != null && c !== false)
12 | .map(c => c instanceof Object ? c : createTextElement(c));
13 | return { type, props };
14 | }
15 |
16 | function createTextElement(value) {
17 | return createElement(TEXT_ELEMENT, { nodeValue: value });
18 | }
19 |
20 | export function createHandler(_fn) {
21 | const emitter = createChangeEmitter()
22 | let handler = x => {
23 | emitter.emit(x)
24 | }
25 | handler.$ = new Observable(observer => {
26 | return emitter.listen(value => {
27 | observer.next(_fn ? _fn(value) : value)
28 | }
29 | )
30 | })
31 | return handler
32 | }
--------------------------------------------------------------------------------
/demos/components/sourceswitching.js:
--------------------------------------------------------------------------------
1 | /** @jsx createElement */
2 | import {mount, createElement, Component, createHandler} from '../../reactive-react'
3 | import {Interval, scan, startWith, merge, mapToConstant} from '../../reactive-react/swyxjs'
4 | import Observable from 'zen-observable'
5 |
6 | import Timer from './timer'
7 | import Counter from './counter'
8 |
9 | export class SourceSwitching extends Component {
10 | initialState = true
11 | toggle = createHandler()
12 | source($) {
13 | const source = this.toggle.$
14 | const reducer = x => !x
15 | return {source, reducer}
16 | }
17 | render(state, stateMap) {
18 | return
19 |
20 | {
21 | state ? this.props.left : this.props.right
22 | }
23 |
24 | }
25 | }
26 |
27 | export default function Source() {
28 | // demonstrate ability to switch sources
29 | return }
31 | left={}
32 | right={}
33 | />
34 | }
--------------------------------------------------------------------------------
/demos/components/blink.js:
--------------------------------------------------------------------------------
1 | /** @jsx createElement */
2 | import {mount, createElement, Component, createHandler} from '../../reactive-react'
3 | import {Interval, scan, startWith, merge, mapToConstant} from '../../reactive-react/swyxjs'
4 | import Observable from 'zen-observable'
5 |
6 | export default class Blink extends Component {
7 | // more fun time demo
8 | // initialState = true // proper
9 | initialState = 0 // hacky
10 | source($) {
11 | // there is a bug right now where switching sources subscribes to the new source twice
12 | // havent been able to chase it down so i had to do this hacky thing
13 | // i'm sorry :( breaking demos last minute before talk sucks
14 | // const reducer = x => !x // the proper reducer
15 | const reducer = (acc, x) => acc + x // hacky reducer
16 | const source = Interval(500, 1) // tick every second
17 | return {source, reducer}
18 | }
19 | render(state) {
20 | const style = {
21 | visibility: ((state/2 + 1) % 2) ? // hacky
22 | 'visible' :
23 | 'hidden'}
24 | return Bring back the blink tag!
25 | }
26 | }
--------------------------------------------------------------------------------
/demos/components/counter.js:
--------------------------------------------------------------------------------
1 | /** @jsx createElement */
2 | import {mount, createElement, Component, createHandler} from '../../reactive-react'
3 | import {Interval, scan, startWith, merge, mapToConstant} from '../../reactive-react/swyxjs'
4 | import Observable from 'zen-observable'
5 |
6 | import './button.css'
7 |
8 | export default class Counter extends Component {
9 | // demonstrate basic counter
10 | initialState = 0
11 | increment = createHandler(e => 1)
12 | // decrement = createHandler(e => -1)
13 | source($) {
14 | // const source = merge(this.increment.$, this.decrement.$)
15 | const source = this.increment.$
16 | const reducer = (acc, n) => acc + n
17 | return {source, reducer}
18 | }
19 | render(state, stateMap) {
20 | const {name = "counter"} = this.props
21 | return
22 |
23 | {name}: {state}
24 |
25 |
+
26 | {/*
- */}
27 |
28 | }
29 | }
30 |
31 |
32 | export function Counters() {
33 | // demonstrate independent states
34 | return
35 |
36 |
37 |
38 | }
--------------------------------------------------------------------------------
/reactive-react/component.js:
--------------------------------------------------------------------------------
1 | // import { reconcile } from "./reconciler";
2 | import {Interval, scan, startWith, merge, mapToConstant} from './swyxjs'
3 |
4 | export class Component {
5 | constructor(props) {
6 | this.props = props;
7 | this.state = this.state || {};
8 | }
9 |
10 | // setState(partialState) {
11 | // this.state = Object.assign({}, this.state, partialState);
12 | // updateInstance(this.__internalInstance);
13 | // }
14 |
15 | // class method because it feeds in this.initialState
16 | combineReducer(obj) {
17 | const sources = Object.entries(obj).map(([k,fn]) => {
18 | let subReducer = fn(obj)
19 | // there are two forms of return the subreducer can have
20 | // straight stream form
21 | // or object form where we need to scan it into string
22 | if (subReducer.source) { // object form
23 | subReducer = scan(subReducer.source,
24 | subReducer.reducer || ((_, n) => n),
25 | this.initialState[k]
26 | )
27 | }
28 | return subReducer
29 | .map(x => ({[k]: x})) // map to its particular namespace
30 | })
31 | const source = merge(...sources)
32 | const reducer = (acc, n) => ({...acc, ...n})
33 | return {source, reducer}
34 | }
35 | }
36 |
37 | // function updateInstance(internalInstance) {
38 | // const parentDom = internalInstance.dom.parentNode;
39 | // const element = internalInstance.element;
40 | // reconcile(parentDom, internalInstance, element);
41 | // }
42 |
43 | export function createPublicInstance(element/*, internalInstance*/) {
44 | const { type, props } = element;
45 | const publicInstance = new type(props);
46 | // publicInstance.__internalInstance = internalInstance;
47 | return publicInstance;
48 | }
49 |
--------------------------------------------------------------------------------
/demos/eleventy.css:
--------------------------------------------------------------------------------
1 | *{margin:0;padding:0}::-moz-selection{background-color:#222;color:#067ada}::selection{background-color:#222;color:#067ada}html{background-color:#222}body{text-align:center;background-color:#067ada;color:#cde4f8;font-family:sans-serif;font-weight:30;font-size:16px;line-height:1.8}@media (min-width: 740px){body{font-size:18px}}.container{margin-left:auto;margin-right:auto;width:90%;text-align:left}h1,h2,h3{color:#fff;font-weight:600;margin-top:2.5em;margin-bottom:1em;line-height:1em}h1{margin-top:0.4em;font-size:4em;line-height:0.9em}@media (min-width: 740px){h1{font-size:5em}}@media (min-width: 1200px){h1{font-size:6em}}p{margin-bottom:1em}.subtitle{margin-top:-3em;margin-bottom:6em}ul,ol{padding-left:1em}a:link,a:visited{color:#222;text-decoration:none;border-bottom:solid 1px #1f87de}a:hover,a:focus{color:#fff;border-bottom:solid 1px #222}.nav{padding-top:3em}.nav li{display:inline}.nav a:link,.nav a:visited{display:inline-block;border-top:solid 1px #9bcaf0;border-bottom-style:none;padding-top:0.8em;padding-bottom:2em;margin-left:0;margin-right:0.7em;width:8em;text-align:left;color:#cde4f8;text-decoration:none}.nav a:hover,.nav a:focus{color:#fff;border-top:solid 1px #222;border-bottom-style:none}.nav small{display:block;font-size:0.7em;color:#9bcaf0}code{font-size:0.8em;background-color:#0971c8;color:#fff;padding-top:2px;padding-bottom:2px;padding-left:4px;padding-right:4px}pre{margin-top:2em;margin-bottom:2em;padding-top:1em;padding-bottom:1em;padding-left:2em;padding-right:2em;line-height:1.2;background-color:#0971c8;border:solid 1px #0e60a3;overflow:auto}pre code{border-style:none;padding-top:0;padding-bottom:0;padding-left:0;padding-right:0}footer{margin-top:6em;padding-top:4em;padding-bottom:6em;border-top:solid 1px #1f87de;font-size:0.7em;color:#6aafe9}footer a:link,footer a:visited{color:#b4d7f4;border-bottom:solid 1px #1a3c59}footer a:hover,footer a:focus{color:#222;border-bottom:solid 1px #fff}
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@swyx/reactive-react",
3 | "version": "1.0.2",
4 | "license": "MIT",
5 | "dependencies": {
6 | "change-emitter": "^0.1.6",
7 | "d3": "^5.5.0",
8 | "virtual-dom": "^2.1.1",
9 | "zen-observable": "^0.8.9"
10 | },
11 | "main": "build/reactive-react.umd.js",
12 | "module": "build/reactive-react.es.js",
13 | "files": [
14 | "build"
15 | ],
16 | "devDependencies": {
17 | "ava": "^0.19.0",
18 | "babel-cli": "^6.24.1",
19 | "babel-plugin-transform-class-properties": "^6.24.1",
20 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
21 | "babel-plugin-transform-react-jsx": "^6.24.1",
22 | "babel-preset-env": "^1.3.3",
23 | "babel-register": "^6.24.1",
24 | "browser-env": "^2.0.29",
25 | "npm-run-all": "^4.0.2",
26 | "parcel-bundler": "^1.9.7",
27 | "rollup": "^0.41.6",
28 | "rollup-plugin-babel": "^3.0.7"
29 | },
30 | "scripts": {
31 | "start": "npm run demo:start",
32 | "demo:start": "parcel demos/index.html --no-hmr",
33 | "demo:build": "parcel build demos/index.html -d demos/dist",
34 | "build:deptree": "depcruise --exclude '^node_modules' --output-type dot reactive-react | dot -T svg > dependencygraph.svg",
35 | "build:module": "rollup -c -f es -n reactive-react -o build/reactive-react.es.js",
36 | "build:main": "rollup -c -f umd -n reactive-react -o build/reactive-react.umd.js",
37 | "build": "run-p build:module build:main build:deptree",
38 | "prepublishOnly": "npm run build"
39 | },
40 | "babel": {
41 | "plugins": [
42 | "transform-object-rest-spread",
43 | "transform-class-properties",
44 | [
45 | "transform-react-jsx",
46 | {}
47 | ]
48 | ],
49 | "presets": [
50 | [
51 | "env",
52 | {
53 | "targets": {
54 | "node": "current"
55 | }
56 | }
57 | ]
58 | ]
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/demos/time-slicing/index.js:
--------------------------------------------------------------------------------
1 | /** @jsx createElement */
2 | import {mount, createElement, Component, createHandler} from '../../reactive-react'
3 | import {fromEvent} from '../../reactive-react/swyxjs'
4 | import Observable from 'zen-observable'
5 |
6 | import _ from 'lodash';
7 | import Charts from './Charts';
8 | // import Clock from './Clock';
9 | import './index.css';
10 |
11 | let cachedData = new Map();
12 |
13 | export default class App extends Component {
14 | initialState = ''
15 |
16 | // Random data for the chart
17 | getStreamData(input) {
18 | if (cachedData.has(input)) {
19 | return cachedData.get(input);
20 | }
21 | const multiplier = input.length !== 0 ? input.length : 1;
22 | const complexity =
23 | (parseInt(window.location.search.substring(1), 10) / 100) * 25 || 25;
24 | const data = _.range(5).map(t =>
25 | _.range(complexity * multiplier).map((j, i) => {
26 | return {
27 | x: j,
28 | y: (t + 1) * _.random(0, 255),
29 | };
30 | })
31 | );
32 | cachedData.set(input, data);
33 | return data;
34 | }
35 |
36 | handleChange = createHandler(e => e.target.value)
37 | source() {
38 | return {
39 | source: this.handleChange.$,
40 | reducer: (_, x) => x
41 | }
42 | }
43 |
44 | render(state, stateMap) {
45 | const Wrapper = 'div'
46 | const data = this.getStreamData(state);
47 | return (
48 |
49 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | );
63 | }
64 | }
65 |
66 |
67 | const container = document.getElementById('app');
68 | mount(, container);
69 |
--------------------------------------------------------------------------------
/reactive-react/swyxjs.js:
--------------------------------------------------------------------------------
1 | import Observable from 'zen-observable'
2 | export { merge, combineLatest, zip } from 'zen-observable/extras'
3 |
4 | export function Interval(tick = 1000, tickData = Symbol('tick')) {
5 | return new Observable(observer => {
6 | let timer = () => setTimeout(() => {
7 | if (typeof tickData === 'function') tickData = tickData()
8 | observer.next(tickData);
9 | timer()
10 | // observer.complete();
11 | }, tick);
12 | timer()
13 |
14 | // On unsubscription, cancel the timer
15 | return () => clearTimeout(timer);
16 |
17 | })
18 | }
19 |
20 | export function fromEvent(el, eventType) {
21 | return new Observable(observer => {
22 | const handler = e => observer.next(e)
23 | el.addEventListener(eventType, handler)
24 | // on unsub, remove event listener
25 | return () => el.removeEventListener(eventType, handler)
26 | })
27 | }
28 |
29 | const NOINIT = Symbol('NO_INITIAL_VALUE')
30 | export function scan(obs, cb, seed = NOINIT) {
31 | let sub, acc = seed, hasValue = false
32 | const hasSeed = acc !== NOINIT
33 | return new Observable(observer => {
34 | sub = obs.subscribe(value => {
35 | if (observer.closed) return
36 | let first = !hasValue;
37 | hasValue = true
38 | if (!first || hasSeed ) {
39 | try { acc = cb(acc, value) }
40 | catch (e) { return observer.error(e) }
41 | observer.next(acc);
42 | }
43 | else {
44 | acc = value
45 | }
46 | })
47 | return sub
48 | })
49 | }
50 |
51 | // Flatten a collection of observables and only output the newest from each
52 | export function switchLatest(higherObservable) {
53 | return new Observable(observer => {
54 | let currentObs = null
55 | return higherObservable.subscribe({
56 | next(obs) {
57 | if (currentObs) currentObs.unsubscribe() // unsub and switch
58 | currentObs = obs.subscribe(observer.subscribe)
59 | },
60 | error(e) {
61 | observer.error(e) // untested
62 | },
63 | complete() {
64 | // i dont think it should complete?
65 | // observer.complete()
66 | }
67 | })
68 | });
69 | }
70 |
71 | export function mapToConstant(obs, val) {
72 | return new Observable(observer => {
73 | const handler = obs.subscribe(() => observer.next(val))
74 | return handler
75 | })
76 | }
77 |
78 | export function startWith(obs, val) {
79 | return new Observable(observer => {
80 | observer.next(val) // immediately output this value
81 | const handler = obs.subscribe(x => observer.next(x))
82 | return () => handler()
83 | })
84 | }
--------------------------------------------------------------------------------
/demos/time-slicing/index.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0px;
4 | margin: 0px;
5 | user-select: none;
6 | font-family: Karla, Helvetica Neue, Helvetica, sans-serif;
7 | background: rgb(34, 34, 34);
8 | color: white;
9 | overflow: hidden;
10 | }
11 |
12 | .VictoryContainer {
13 | opacity: 0.8;
14 | }
15 |
16 | * {
17 | box-sizing: border-box;
18 | }
19 |
20 | #root {
21 | height: 100vh;
22 | }
23 |
24 | .container {
25 | width: 100%;
26 | max-width: 960px;
27 | margin: auto;
28 | padding: 10px;
29 | }
30 |
31 | .rendering {
32 | margin-top: 20px;
33 | margin-bottom: 20px;
34 | zoom: 1.8;
35 | }
36 |
37 | label {
38 | zoom: 1;
39 | margin-right: 50px;
40 | font-size: 30px;
41 | }
42 |
43 | label.selected {
44 | font-weight: bold;
45 | }
46 |
47 | label:nth-child(1).selected {
48 | color: rgb(253, 25, 153);
49 | }
50 |
51 | label:nth-child(2).selected {
52 | color: rgb(255, 240, 1);
53 | }
54 |
55 | label:nth-child(3).selected {
56 | color: #61dafb;
57 | }
58 |
59 | .chart {
60 | width: 100%;
61 | height: 100%;
62 | }
63 |
64 | .input {
65 | padding: 16px;
66 | font-size: 30px;
67 | width: 100%;
68 | display: block;
69 | }
70 | .input.sync {
71 | outline-color: rgba(253, 25, 153, 0.1);
72 | }
73 | .input.debounced {
74 | outline-color: rgba(255, 240, 1, 0.1);
75 | }
76 | .input.async {
77 | outline-color: rgba(97, 218, 251, 0.1);
78 | }
79 |
80 |
81 | label {
82 | font-size: 20px;
83 | }
84 |
85 | label label {
86 | display: 'inline-block';
87 | margin-left: 20px;
88 | }
89 |
90 | .row {
91 | flex: 1;
92 | display: flex;
93 | margin-top: 20px;
94 | min-height: 100%;
95 | }
96 |
97 | .column {
98 | flex: 1;
99 | }
100 |
101 | .demo {
102 | position: relative;
103 | min-height: 100vh;
104 | }
105 |
106 | .stutterer {
107 | transform: scale(1.5);
108 | height: 310px;
109 | width: 310px;
110 | position: absolute;
111 | left: 0;
112 | right: 0;
113 | top: -256px;
114 | bottom: 0;
115 | margin: auto;
116 | box-shadow: 0 0 10px 10px rgba(0, 0, 0, 0.2);
117 | border-radius: 200px;
118 | }
119 |
120 | .clockHand {
121 | stroke: white;
122 | stroke-width: 10px;
123 | stroke-linecap: round;
124 | }
125 |
126 | .clockFace {
127 | stroke: white;
128 | stroke-width: 10px;
129 | }
130 |
131 | .arcHand {
132 | }
133 |
134 | .innerLine {
135 | border-radius: 6px;
136 | position: absolute;
137 | height: 149px;
138 | left: 47.5%;
139 | top: 0%;
140 | width: 5%;
141 | background-color: red;
142 | transform-origin: bottom center;
143 | }
144 |
--------------------------------------------------------------------------------
/demos/components/crappybird.js:
--------------------------------------------------------------------------------
1 | /** @jsx createElement */
2 | import {mount, createElement, Component, createHandler} from '../../reactive-react'
3 | import {Interval, scan, startWith, merge, mapToConstant} from '../../reactive-react/swyxjs'
4 | import Observable from 'zen-observable'
5 |
6 | export default class CrappyBird extends Component {
7 | // merging time and coutner
8 | initialState = {
9 | input: 50,
10 | target: 50
11 | }
12 | increment = createHandler(e => 3)
13 | source($) {
14 | return this.combineReducer({
15 | input: () => {
16 | const source = merge(this.increment.$, Interval(200,-1))
17 | const reducer = (acc, x) => Math.max(0,acc + x)
18 | return {source, reducer}
19 | },
20 | target: () => {
21 | const source = Interval(200)
22 | const reducer = (acc) => {
23 | const int = acc + Math.random() * 8 - 4
24 | return int - (int-50)/40 // bias toward 50
25 | }
26 | return {source, reducer}
27 | }
28 | })
29 | }
30 | render(state, stateMap) {
31 | const {input, target} = state
32 | return
33 |
Crappy Bird: Match the bird to the target!
34 |
35 |
{ Math.abs(input - target) < 20 ? '👍 Good 👍' : '☠️ LOSING! ☠️'}
36 |
Bird
37 |
Target
38 |
39 | {/*
*/}
40 |
+
41 |
42 |
49 |
56 |
57 |
58 | }
59 | }
60 |
61 |
62 | // {/* Bird:
63 | // Target:
*/}
--------------------------------------------------------------------------------
/reactive-react/scheduler.js:
--------------------------------------------------------------------------------
1 | import Observable from 'zen-observable'
2 | import {fromEvent, scan, merge, startWith, switchLatest} from './swyxjs'
3 | import diff from 'virtual-dom/diff';
4 | import patch from 'virtual-dom/patch';
5 | import createElement from 'virtual-dom/create-element';
6 | import { createChangeEmitter } from 'change-emitter'
7 | import { renderStream } from './reconciler'
8 |
9 | export const stateMapPointer = new Map()
10 |
11 | const circuitBreakerflag = !true // set true to enable debugger in infinite loops
12 | let circuitBreaker = -200
13 |
14 | const emitter = createChangeEmitter()
15 | // single UI thread; this is the observable that sticks around and swaps out source
16 | const UIthread = new Observable(observer => {
17 | emitter.listen(x => {
18 | observer.next(x)
19 | })
20 | })
21 | // mount the vdom on to the dom and
22 | // set up the runtime from sources and
23 | // patch the vdom
24 | // ---
25 | // returns an unsubscribe method you can use to unmount
26 | export function mount(rootElement, container) {
27 | // initial, throwaway-ish frame
28 | let {source, instance} = renderStream(rootElement, {}, undefined, stateMapPointer)
29 | let instancePointer = instance
30 | const rootNode = createElement(instance.dom)
31 | const containerChild = container.firstElementChild
32 | if (containerChild) {
33 | container.replaceChild(rootNode,containerChild) // hot reloaded mount
34 | } else {
35 | container.appendChild(rootNode) // initial mount
36 | }
37 | let currentSrc$ = null
38 | let SoS = startWith(UIthread, source) // stream of streams
39 | return SoS.subscribe(
40 | src$ => { // this is the current sourceStream we are working with
41 | if (currentSrc$) console.log('unsub!') || currentSrc$.unsubscribe() // unsub from old stream
42 | if (circuitBreakerflag && circuitBreaker++ > 0) debugger
43 | /**** main */
44 | const source2$ = scan(
45 | src$,
46 | ({instance, stateMap}, nextState) => {
47 | const streamOutput = renderStream(rootElement, instance, nextState, stateMap)
48 | if (streamOutput.isNewStream) { // quick check
49 | const nextSource$ = streamOutput.source
50 | instancePointer = streamOutput.instance
51 | patch(rootNode, diff(instance.dom, instancePointer.dom)) // render to screen
52 | emitter.emit(nextSource$) // update the UI thread; source will switch
53 | } else {
54 | const nextinstance = streamOutput.instance
55 | patch(rootNode, diff(instance.dom, nextinstance.dom)) // render to screen
56 | return {instance: nextinstance, stateMap: stateMap}
57 | }
58 | },
59 | {instance: instancePointer, stateMap: stateMapPointer} // accumulator
60 | )
61 | /**** end main */
62 | currentSrc$ =
63 | source2$
64 | .subscribe()
65 | }
66 | )
67 | }
68 |
--------------------------------------------------------------------------------
/demos/index.js:
--------------------------------------------------------------------------------
1 | /** @jsx createElement */
2 | import {mount, createElement, Component, createHandler} from '../reactive-react'
3 | import {Interval, scan, startWith, merge, mapToConstant} from '../reactive-react/swyxjs'
4 | import Observable from 'zen-observable'
5 |
6 | import {Timer, Blink, Counter, Counters, CrappyBird, Source, SourceSwitching, CrazyCharts} from './components'
7 |
8 | import './eleventy.css'
9 | import './custom.css'
10 |
11 | class App extends Component {
12 | initialState = "counter"
13 | router = createHandler(e => e.target.name)
14 | source($) {
15 | return this.router.$
16 | }
17 | render(state) {
18 | const Display = {
19 | 'counter': () => ,
20 | // 'timer': () => ,
21 | 'blink': () => ,
22 | 'crappy': () => ,
23 | 'charts': () => ,
24 | }[state] || (() => )
25 |
26 | function selectedLink(name) {
27 | return {
28 | name,
29 | style: state === name && {color: 'yellow'}
30 | }
31 | }
32 | return
33 |
34 |
reactive-react Demo: {state}
35 |
41 |
42 |
61 | {Display()}
62 |
65 |
66 | }
67 | }
68 |
69 | mount(, document.getElementById('app'))
70 |
--------------------------------------------------------------------------------
/demos/time-slicing/Clock.js:
--------------------------------------------------------------------------------
1 | // /** @jsx createElement */
2 | // import {mount, createElement, Component, createHandler} from '../../reactive-react'
3 |
4 | // const SPEED = 0.003 / Math.PI;
5 | // const FRAMES = 10;
6 |
7 | // export default class Clock extends PureComponent {
8 | // faceRef = createRef();
9 | // arcGroupRef = createRef();
10 | // clockHandRef = createRef();
11 | // frame = null;
12 | // hitCounter = 0;
13 | // rotation = 0;
14 | // t0 = Date.now();
15 | // arcs = [];
16 |
17 | // animate = () => {
18 | // const now = Date.now();
19 | // const td = now - this.t0;
20 | // this.rotation = (this.rotation + SPEED * td) % (2 * Math.PI);
21 | // this.t0 = now;
22 |
23 | // this.arcs.push({rotation: this.rotation, td});
24 |
25 | // let lx, ly, tx, ty;
26 | // if (this.arcs.length > FRAMES) {
27 | // this.arcs.forEach(({rotation, td}, i) => {
28 | // lx = tx;
29 | // ly = ty;
30 | // const r = 145;
31 | // tx = 155 + r * Math.cos(rotation);
32 | // ty = 155 + r * Math.sin(rotation);
33 | // const bigArc = SPEED * td < Math.PI ? '0' : '1';
34 | // const path = `M${tx} ${ty}A${r} ${r} 0 ${bigArc} 0 ${lx} ${ly}L155 155`;
35 | // const hue = 120 - Math.min(120, td / 4);
36 | // const colour = `hsl(${hue}, 100%, ${60 - i * (30 / FRAMES)}%)`;
37 | // if (i !== 0) {
38 | // const arcEl = this.arcGroupRef.current.children[i - 1];
39 | // arcEl.setAttribute('d', path);
40 | // arcEl.setAttribute('fill', colour);
41 | // }
42 | // });
43 | // this.clockHandRef.current.setAttribute('d', `M155 155L${tx} ${ty}`);
44 | // this.arcs.shift();
45 | // }
46 |
47 | // if (this.hitCounter > 0) {
48 | // this.faceRef.current.setAttribute(
49 | // 'fill',
50 | // `hsla(0, 0%, ${this.hitCounter}%, 0.95)`
51 | // );
52 | // this.hitCounter -= 1;
53 | // } else {
54 | // this.hitCounter = 0;
55 | // this.faceRef.current.setAttribute('fill', 'hsla(0, 0%, 5%, 0.95)');
56 | // }
57 |
58 | // this.frame = requestAnimationFrame(this.animate);
59 | // };
60 |
61 | // componentDidMount() {
62 | // this.frame = requestAnimationFrame(this.animate);
63 | // if (this.faceRef.current) {
64 | // this.faceRef.current.addEventListener('click', this.handleClick);
65 | // }
66 | // }
67 |
68 | // componentDidUpdate() {
69 | // console.log('componentDidUpdate()', this.faceRef.current);
70 | // }
71 |
72 | // componentWillUnmount() {
73 | // this.faceRef.current.removeEventListener('click', this.handleClick);
74 | // if (this.frame) {
75 | // cancelAnimationFrame(this.frame);
76 | // }
77 | // }
78 |
79 | // handleClick = e => {
80 | // e.stopPropagation();
81 | // this.hitCounter = 50;
82 | // };
83 |
84 | // render() {
85 | // const paths = new Array(FRAMES);
86 | // for (let i = 0; i < FRAMES; i++) {
87 | // paths.push();
88 | // }
89 | // return (
90 | //
91 | //
103 | //
104 | // );
105 | // }
106 | // }
107 |
--------------------------------------------------------------------------------
/reactive-react/reconciler.js:
--------------------------------------------------------------------------------
1 | import Observable from 'zen-observable'
2 | import {fromEvent, scan, merge, startWith, switchLatest} from './swyxjs'
3 | // import { updateDomProperties } from "./updateProperties";
4 | import { TEXT_ELEMENT } from "./element";
5 | import { createPublicInstance } from "./component";
6 | import h from 'virtual-dom/h'
7 | // import VNode from "virtual-dom/vnode/vnode"
8 | import VText from "virtual-dom/vnode/vtext"
9 |
10 | const circuitBreakerflag = !true // set true to enable debugger in infinite loops
11 | let circuitBreaker = -500
12 | // traverse all children and collect a stream of all sources
13 | // AND render. a bit of duplication, but we get persistent instances which is good
14 | export function renderStream(element, instance, state, stateMap) {
15 | // this is a separate function because scope gets messy when being recursive
16 | let isNewStream = false // assume no stream switching by default
17 | // this is the first ping of data throughout the app
18 | let source = Observable.of(state)
19 | const addToStream = _source => {
20 | // visit each source and merge with source
21 | if (_source) return source = merge(source, _source)
22 | }
23 | const markNewStream = () => isNewStream = true
24 | const newInstance = render(source, addToStream, markNewStream)(element, instance, state, stateMap)
25 | return {source, instance: newInstance, isNewStream}
26 | }
27 |
28 | /** core render logic */
29 | export function render(source, addToStream, markNewStream) { // this is the nonrecursive part
30 | return function renderWithStream(element, instance, state, stateMap) { // recursive part
31 | let newInstance
32 | const { type, props } = element
33 |
34 | const isDomElement = typeof type === "string";
35 | if (circuitBreakerflag && circuitBreaker++ > 0) debugger
36 | const {children = [], ...rest} = props
37 | if (isDomElement) {
38 | const childInstances = children.map(
39 | (el, i) => {
40 | // ugly but necessary to allow functional children
41 | // mapping element's children to instance's childInstances
42 | const _childInstances = instance && (instance.childInstance || instance.childInstances[i])
43 | return renderWithStream( // recursion
44 | el,
45 | _childInstances,
46 | state,
47 | stateMap)
48 | }
49 | );
50 | const childDoms = childInstances.map(childInstance => childInstance.dom);
51 | let lcaseProps = {}
52 | Object.entries(rest).forEach(([k, v]) => lcaseProps[formatProps(k)] = v)
53 | const dom = type === TEXT_ELEMENT
54 | ? new VText(props.nodeValue)
55 | : h(type, lcaseProps, childDoms); // equivalent of appendchild
56 | newInstance = { dom, element, childInstances };
57 | } else { // component element
58 | let publicInstance
59 | // might have to do more diffing of props in future; further research needed
60 | // used to compare instance.element === element; this proved too easy to be false so went for types
61 | if (instance && instance.publicInstance && instance.element.type === element.type) {
62 | // just reuse old instance if it already exists
63 | publicInstance = instance && instance.publicInstance
64 | } else {
65 | markNewStream() // mark as dirty in parent scope; will rerender
66 | if (circuitBreakerflag && circuitBreaker++ > 0) debugger
67 | if (stateMap.has(publicInstance)) stateMap.delete(publicInstance)
68 | publicInstance = createPublicInstance(element);
69 | }
70 | let localState = stateMap.get(publicInstance)
71 | if (localState === undefined) localState = publicInstance.initialState
72 | publicInstance.state = localState // for access with this.state
73 | if (Object.keys(rest).length) publicInstance.props = rest // update with new props // TODO: potentially buggy
74 | // console.log({rest})
75 | if (publicInstance.source) {
76 | const src = publicInstance.source(source)
77 | // there are two forms of Component.source
78 | const src$ = src.reducer && publicInstance.initialState !== undefined ?
79 | // 1. the reducer form
80 | scan(src.source, src.reducer, publicInstance.initialState) :
81 | // 2. and raw stream form
82 | src
83 | addToStream(src$
84 | .map(event => {
85 | stateMap.set(publicInstance, event)
86 | return {instance: publicInstance, event} // tag it to the instance
87 | })
88 | );
89 | }
90 | const childElement = publicInstance.render ?
91 | publicInstance.render(localState, stateMap) :
92 | publicInstance;
93 |
94 | const childInstance = renderWithStream(childElement, instance && instance.childInstance, state, stateMap)
95 | const dom = childInstance.dom
96 | newInstance = { dom, element, childInstance, publicInstance }
97 | }
98 | return newInstance
99 | }
100 | }
101 |
102 | function formatProps(k) {
103 | if (k.startsWith('on')) return k.toLowerCase()
104 | return k
105 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a prototype mockup of a "Reactive" version of React. Do not use unless you are swyx.
2 |
3 | ## Watch the React Rally talk
4 |
5 | Talk video: https://www.youtube.com/watch?v=nyFHR0dDZo0
6 |
7 | TL;DR with writeup: https://www.swyx.io/ReactRally
8 |
9 | ## Description
10 |
11 | **Note: we are aware of the double subscribe bug when a source is switched. didnt have time to figure out the fix before react rally. Blink tag example has a hacky patch for this.**
12 |
13 | In this alternate universe, Observables became a part of Javascript.
14 |
15 | We take a minimal implementation of Observables, zen-observable.
16 |
17 |
18 | # Try it out
19 |
20 | `yarn start` to run the demo locally
21 |
22 | # The `reactive-react` API
23 |
24 | The React API we are targeting looks something like this (see `/demos` for actual examples):
25 |
26 | ```js
27 | class Counter extends Component {
28 | // demonstrate basic counter
29 | initialState = 0
30 | increment = createHandler(e => 1)
31 | decrement = createHandler(e => -1)
32 | source($) {
33 | const source = merge(this.increment.$, this.decrement.$)
34 | const reducer = (acc, n) => acc + n
35 | return {source, reducer}
36 | }
37 | render(state, stateMap) {
38 | const {name = "counter"} = this.props
39 | return
40 | {name}: {state}
41 |
42 |
43 |
44 | }
45 | }
46 |
47 | // taking info from event handler
48 | class Echo extends Component {
49 | handler = createHandler(e => e.target.value)
50 | initialState = 'hello world'
51 | source($) {
52 | const source = this.handler.$
53 | const reducer = (acc, n) => n
54 | return {source, reducer}
55 | }
56 | render(state, prevState) {
57 | return
58 |
59 | {state}
60 |
61 | }
62 | }
63 |
64 | class Timer extends Component {
65 | // demonstrate interval time
66 | initialState = 0
67 | source($) {
68 | const reducer = x => x + 1 // count up
69 | const source = Interval(this.props.ms) // tick every second
70 | // source returns an observable
71 | return scan(source, reducer, 0) // from zero
72 | }
73 | render(state, stateMap) {
74 | return number of seconds elapsed: {state}
75 | }
76 | }
77 |
78 | class Blink extends Component {
79 | // more fun time demo
80 | initialState = true
81 | source($) {
82 | const reducer = x => !x
83 | // tick every ms milliseconds
84 | const source = Interval(this.props.ms)
85 | // source can also return an observable
86 | return scan(source, reducer, true)
87 | }
88 | render(state) {
89 | const style = {display: state ? 'block' : 'none'}
90 | return Bring back the blink tag!
91 | }
92 | }
93 |
94 | class CrappyBird extends Component {
95 | // merging time and counter
96 | initialState = {
97 | input: 50,
98 | target: 50
99 | }
100 | increment = createHandler(e => 3)
101 | source($) {
102 | return this.combineReducer({
103 | input: () => {
104 | const source = merge(this.increment.$, Interval(200,-1))
105 | const reducer = (acc, x) => Math.max(0,acc + x)
106 | return {source, reducer}
107 | },
108 | target: () => {
109 | const source = Interval(200)
110 | const reducer = (acc) => {
111 | const int = acc + Math.random() * 8 - 4
112 | return int - (int-50)/30 // bias toward 50
113 | }
114 | return {source, reducer}
115 | }
116 | })
117 | }
118 | render(state, stateMap) {
119 | const {input, target} = state
120 | return
121 |
122 |
Crappy bird
123 |
Bird:
124 |
Target:
125 |
126 | }
127 | }
128 |
129 | function Counters() {
130 | // demonstrate independent states
131 | return
132 |
133 |
134 |
135 | }
136 | function Source() {
137 | // demonstrate ability to switch sources
138 | return }
140 | right={}
141 | />
142 | }
143 |
144 | class SourceSwitching extends Component {
145 | initialState = true
146 | toggle = createHandler()
147 | source($) {
148 | const source = this.toggle.$
149 | const reducer = x => !x
150 | return {source, reducer}
151 | }
152 | render(state, stateMap) {
153 | return
154 |
155 | {
156 | state ? this.props.left : this.props.right
157 | }
158 |
159 | }
160 | }
161 |
162 | ```
163 |
164 | # Local development
165 |
166 | `yarn run build` and then `npm publish` (but its under my namespace @swyx/reactive-react cos someone else has the generic one)
167 |
168 | Here is the project structure:
169 |
170 | 
171 |
--------------------------------------------------------------------------------
/demos/time-slicing/Charts.js:
--------------------------------------------------------------------------------
1 | /** @jsx createElement */
2 | import {mount, createElement, Component, createHandler} from '../../reactive-react'
3 | import * as d3 from "d3";
4 |
5 | const colors = ['#fff489', '#fa57c1', '#b166cc', '#7572ff', '#69a6f9'];
6 | const containerWidth = 565 / 2
7 | const containerHeight = 400 / 2
8 |
9 |
10 | export default class Charts extends Component {
11 | render() {
12 | const data = this.props.data;
13 | const container = document.getElementById("demo")
14 | if (!container) return Type something
// initial load only
15 | bottomChart(data)
16 | leftChart(data)
17 | rightChart(data)
18 | return (
19 |
20 |
number of datapoints: {data[0].length * 5}
21 |
25 |
33 |
34 | );
35 | }
36 | }
37 |
38 |
39 | function responsivefy(svg) {
40 | // get container + svg aspect ratio
41 | if (!svg.node()) return
42 | var container = d3.select(svg.node().parentNode),
43 | width = parseInt(svg.style("width")),
44 | height = parseInt(svg.style("height")),
45 | aspect = width / height;
46 |
47 | // add viewBox and preserveAspectRatio properties,
48 | // and call resize so that svg resizes on inital page load
49 | svg.attr("viewBox", "0 0 " + width + " " + height)
50 | .attr("preserveAspectRatio", "xMinYMid")
51 | .call(resize);
52 |
53 | // to register multiple listeners for same event type,
54 | // you need to add namespace, i.e., 'click.foo'
55 | // necessary if you call invoke this function for multiple svgs
56 | // api docs: https://github.com/mbostock/d3/wiki/Selections#on
57 | d3.select(window).on("resize." + container.attr("id"), resize);
58 |
59 | // get width of container and resize svg to fit it
60 | function resize() {
61 | var targetWidth = parseInt(container.style("width"));
62 | svg.attr("width", targetWidth);
63 | svg.attr("height", Math.round(targetWidth / aspect));
64 | }
65 | }
66 |
67 | function bottomChart(data) {
68 |
69 | var margin = { top: 10, right: 20, bottom: 30, left: 30 };
70 | var width = containerWidth * 2 - margin.left - margin.right;
71 | var height = containerHeight - margin.top - margin.bottom;
72 |
73 | // really janky clearance
74 | const temp = d3.select(".bottomchart")
75 | .selectAll("svg")
76 | if (temp._groups[0] && temp._groups[0].length) temp._groups[0]
77 | .forEach((d, i) => d.parentNode.removeChild(d))
78 |
79 | var svg = d3.select('.bottomchart')
80 | .append('svg')
81 | .attr('width', width + margin.left + margin.right)
82 | .attr('height', height + margin.top + margin.bottom)
83 | // .call(responsivefy)
84 | .append('g')
85 | .attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
86 |
87 | var xScale = d3.scaleLinear()
88 | .domain([
89 | d3.min(data, co => d3.min(co, d => d.x)),
90 | d3.max(data, co => d3.max(co, d => d.x))
91 | ])
92 | .range([0, width]);
93 | svg
94 | .append('g')
95 | .attr('transform', `translate(0, ${height})`)
96 | .style('stroke', 'yellow')
97 | .call(d3.axisBottom(xScale).ticks(5));
98 |
99 | var yScale = d3.scaleLinear()
100 | .domain([
101 | d3.min(data, co => d3.min(co, d => d.y)),
102 | d3.max(data, co => d3.max(co, d => d.y))
103 | ])
104 | .range([height, 0]);
105 | svg
106 | .append('g')
107 | .style('stroke', 'yellow')
108 | .call(d3.axisLeft(yScale));
109 |
110 | var area = d3.area()
111 | .x(d => xScale(d.x))
112 | .y0(yScale(yScale.domain()[0]))
113 | .y1(d => yScale(d.y))
114 | .curve(d3.curveCatmullRom.alpha(0.5));
115 |
116 | svg
117 | .selectAll('.area')
118 | .data(data)
119 | .enter()
120 | .append('path')
121 | .attr('class', 'area')
122 | .attr('d', d => area(d))
123 | .style('stroke', (d, i) => colors[i])
124 | .style('stroke-width', 2)
125 | .style('fill', (d, i) => colors[i])
126 | .style('fill-opacity', 0.5)
127 | }
128 |
129 |
130 |
131 | function rightChart(data) {
132 |
133 | var margin = { top: 10, right: 20, bottom: 30, left: 30 };
134 | var width = containerWidth - margin.left - margin.right;
135 | var height = containerHeight - margin.top - margin.bottom;
136 |
137 | // really janky clearance
138 | const temp = d3.select(".rightchart")
139 | .selectAll("svg")
140 | if (temp._groups[0] && temp._groups[0].length) temp._groups[0]
141 | .forEach((d, i) => d.parentNode.removeChild(d))
142 |
143 | var svg = d3.select('.rightchart')
144 | .append('svg')
145 | .attr('width', width + margin.left + margin.right)
146 | .attr('height', height + margin.top + margin.bottom)
147 | // .call(responsivefy)
148 | .append('g')
149 | .attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
150 |
151 | var xScale = d3.scaleLinear()
152 | .domain([
153 | d3.min(data, co => d3.min(co, d => d.x)),
154 | d3.max(data, co => d3.max(co, d => d.x))
155 | ])
156 | .range([0, width]);
157 | svg
158 | .append('g')
159 | .attr('transform', `translate(0, ${height})`)
160 | .style('stroke', 'yellow')
161 | .call(d3.axisBottom(xScale).ticks(5));
162 |
163 | var yScale = d3.scaleLinear()
164 | .domain([
165 | d3.min(data, co => d3.min(co, d => d.y)),
166 | d3.max(data, co => d3.max(co, d => d.y))
167 | ])
168 | .range([height, 0]);
169 | svg
170 | .append('g')
171 | .style('stroke', 'yellow')
172 | .call(d3.axisLeft(yScale));
173 |
174 |
175 | var line = d3.line()
176 | .x(d => xScale(d.x))
177 | .y(d => yScale(d.y))
178 | .curve(d3.curveCatmullRom.alpha(0.5));
179 |
180 | svg
181 | .selectAll('.line')
182 | .data(data)
183 | .enter()
184 | .append('path')
185 | .attr('class', 'line')
186 | .attr('d', d => line(d))
187 | // .style('stroke', (d, i) => ['#FF9900', '#3369E8'][i])
188 | .style('stroke', (d, i) => colors[i])
189 | .style('stroke-width', 2)
190 | .style('fill', 'none');
191 | }
192 |
193 |
194 | function leftChart(data) {
195 |
196 | var margin = { top: 10, right: 20, bottom: 30, left: 30 };
197 | var width = containerWidth - margin.left - margin.right;
198 | var height = containerHeight - margin.top - margin.bottom;
199 |
200 | // really janky clearance
201 | const temp = d3.select(".leftchart")
202 | .selectAll("svg")
203 | if (temp._groups[0] && temp._groups[0].length) temp._groups[0]
204 | .forEach((d, i) => d.parentNode.removeChild(d))
205 |
206 | var svg = d3.select('.leftchart')
207 | .append('svg')
208 | .attr('width', width + margin.left + margin.right)
209 | .attr('height', height + margin.top + margin.bottom)
210 | // .call(responsivefy)
211 | .append('g')
212 | .attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
213 |
214 | var xScale = d3.scaleLinear()
215 | .domain([
216 | d3.min(data, co => d3.min(co, d => d.x)),
217 | d3.max(data, co => d3.max(co, d => d.x))
218 | ])
219 | .range([0, width]);
220 | svg
221 | .append('g')
222 | .attr('transform', `translate(0, ${height})`)
223 | .style('stroke', 'yellow')
224 | .call(d3.axisBottom(xScale).ticks(5));
225 |
226 | var yScale = d3.scaleLinear()
227 | .domain([
228 | d3.min(data, co => d3.min(co, d => d.y)),
229 | d3.max(data, co => d3.max(co, d => d.y))
230 | ])
231 | .range([height, 0]);
232 | svg
233 | .append('g')
234 | .style('stroke', 'yellow')
235 | .call(d3.axisLeft(yScale));
236 |
237 | var circlesArr = svg
238 | .selectAll('.ball')
239 | .data(data)
240 | .enter()
241 | .append('g')
242 | .each(function(d, i) {
243 | var node = d3.select(this).selectAll('g')
244 | .data(d)
245 | .enter()
246 | .append('g')
247 | .attr('class', 'ball')
248 | .attr('transform', d => {
249 | return `translate(${xScale(d.x)}, ${yScale(d.y)})`;
250 | });
251 | node
252 | .append('circle')
253 | .attr('cx', 0)
254 | .attr('cy', 0)
255 | .attr('r', d => 5)
256 | .style('fill-opacity', 0.5)
257 | .style('fill', colors[i]);
258 | })
259 | }
--------------------------------------------------------------------------------
/dependencygraph.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
142 |
--------------------------------------------------------------------------------
/demos/components/button.css:
--------------------------------------------------------------------------------
1 |
2 |
3 | /* BonBon Buttons 1.1 by simurai.com
4 |
5 | 1.1 Added unprefixed attributes, :focus style,