├── docs └── preview.gif ├── .npmignore ├── .storybook ├── addons.js ├── config.js └── .babelrc ├── src ├── utils │ ├── points.js │ ├── scaleVector.js │ ├── repeat.js │ └── isEmissionsArr.js ├── components │ ├── chainDiagram │ │ ├── index.js │ │ ├── chainDiagram.stories.js │ │ └── chainDiagram.js │ ├── operatorDiagram │ │ ├── index.js │ │ ├── transformNote.js │ │ ├── operatorDiagram.stories.js │ │ └── operatorDiagram.js │ ├── draggable │ │ ├── index.js │ │ ├── draggableView.stories.js │ │ └── draggableView.js │ ├── transition │ │ ├── index.js │ │ ├── transitionView.stories.js │ │ └── transitionView.js │ ├── observable │ │ ├── index.js │ │ ├── emissionsView.js │ │ ├── constants.js │ │ ├── makeTransformFactor.js │ │ ├── arrow.js │ │ ├── completion.js │ │ ├── view.stories.js │ │ ├── defs.js │ │ ├── fromEmissions.js │ │ ├── separators.js │ │ ├── emission.js │ │ └── view.js │ └── ObservableRenderer.js ├── constants │ ├── font.js │ └── colors.js ├── models │ └── emissions │ │ ├── makeScheduler.js │ │ ├── makeDiagramModel.js │ │ ├── index.js │ │ ├── mapStreamToEmissions.js │ │ ├── makeVirtualStream.js │ │ ├── transformEmissions.js │ │ └── index.stories.js └── index.js ├── .editorconfig ├── .gitignore ├── scripts └── closure-minify ├── webpack.umd.js ├── package.json ├── README.md └── LICENSE.md /docs/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kitten/rxjs-diagrams/HEAD/docs/preview.gif -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | webpack.umd.js 2 | .travis.yml 3 | .editorconfig 4 | .storybook 5 | scripts/ 6 | docs/ 7 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@kadira/storybook/addons' 2 | import '@kadira/storybook-addon-knobs/register' 3 | -------------------------------------------------------------------------------- /src/utils/points.js: -------------------------------------------------------------------------------- 1 | const points = (arr = []) => ( 2 | arr.map(coor => coor.join(',')).join(' ') 3 | ) 4 | 5 | export default points 6 | -------------------------------------------------------------------------------- /src/components/chainDiagram/index.js: -------------------------------------------------------------------------------- 1 | import ChainDiagram from './chainDiagram' 2 | 3 | export default ChainDiagram 4 | 5 | export { 6 | ChainDiagram 7 | } 8 | -------------------------------------------------------------------------------- /src/components/operatorDiagram/index.js: -------------------------------------------------------------------------------- 1 | import OperatorDiagram from './operatorDiagram' 2 | 3 | export default OperatorDiagram 4 | 5 | export { 6 | OperatorDiagram 7 | } 8 | -------------------------------------------------------------------------------- /src/components/draggable/index.js: -------------------------------------------------------------------------------- 1 | import DraggableEmissionsView from './draggableView' 2 | 3 | export default DraggableEmissionsView 4 | 5 | export { 6 | DraggableEmissionsView 7 | } 8 | -------------------------------------------------------------------------------- /src/components/transition/index.js: -------------------------------------------------------------------------------- 1 | import TransitionEmissionsView from './transitionView' 2 | 3 | export default TransitionEmissionsView 4 | 5 | export { 6 | TransitionEmissionsView 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/scaleVector.js: -------------------------------------------------------------------------------- 1 | const scaleVector = (width, height) => ([ x, y ]) => ([ 2 | typeof x === 'number' ? x * width : x, 3 | typeof y === 'number' ? y * height : y 4 | ]) 5 | 6 | export default scaleVector 7 | -------------------------------------------------------------------------------- /src/constants/font.js: -------------------------------------------------------------------------------- 1 | export const fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"' 2 | export const fontSize = '14px' 3 | 4 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@kadira/storybook'; 2 | 3 | const stories = require.context('../src', true, /\.stories\.jsx?$/); 4 | configure(() => stories.keys().forEach((filename) => stories(filename)), module); 5 | 6 | -------------------------------------------------------------------------------- /src/models/emissions/makeScheduler.js: -------------------------------------------------------------------------------- 1 | import { VirtualTimeScheduler, VirtualAction } from 'rxjs/scheduler/VirtualTimeScheduler' 2 | 3 | const makeScheduler = () => new VirtualTimeScheduler(VirtualAction) 4 | 5 | export default makeScheduler 6 | -------------------------------------------------------------------------------- /src/constants/colors.js: -------------------------------------------------------------------------------- 1 | import Color from 'goethe/lib/with-better-colors' 2 | 3 | export const white = Color('white') 4 | export const blue = Color([ 41, 124, 233 ]) 5 | export const black = Color('black') 6 | export const gray = Color('gray') 7 | 8 | -------------------------------------------------------------------------------- /.storybook/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "babel-preset-philpl" 4 | ], 5 | "ignore": [ 6 | "/node_modules/" 7 | ], 8 | "plugins": [ 9 | [ "babel-plugin-transform-es2015-modules-commonjs", { "loose": true } ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/components/observable/index.js: -------------------------------------------------------------------------------- 1 | import ObservableView from './view' 2 | import EmissionsView from './emissionsView' 3 | import fromEmissions from './fromEmissions' 4 | 5 | export default ObservableView 6 | 7 | export { 8 | ObservableView, 9 | EmissionsView, 10 | fromEmissions 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/repeat.js: -------------------------------------------------------------------------------- 1 | const repeat = (start, end, amount) => { 2 | const range = end - start 3 | const step = range / amount 4 | 5 | const arr = [] 6 | 7 | for (let i = start; i <= end; i += step) { 8 | arr.push(i) 9 | } 10 | 11 | return arr 12 | } 13 | 14 | export default repeat 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /src/utils/isEmissionsArr.js: -------------------------------------------------------------------------------- 1 | export const isEmission = obj => ( 2 | typeof obj === 'object' && 3 | typeof obj.x === 'number' && 4 | obj.x === obj.x && 5 | obj.x >= 0 && 6 | obj.d !== undefined && 7 | obj.d !== null 8 | ) 9 | 10 | export const isEmissionsArr = arr => ( 11 | Array.isArray(arr) && 12 | arr.every(isEmission) 13 | ) 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules 3 | *.log 4 | 5 | # Ignore all dotfiles, except... 6 | /.* 7 | !.editorconfig 8 | !.gitignore 9 | !.jestrc.json 10 | !.travis.yml 11 | !.npmignore 12 | !.storybook 13 | 14 | # Ignore all js files in root, except... 15 | /*.js 16 | !webpack.umd.js 17 | 18 | # Build folders 19 | lib/ 20 | es/ 21 | dist/ 22 | coverage/ 23 | storybook/ 24 | 25 | -------------------------------------------------------------------------------- /src/models/emissions/makeDiagramModel.js: -------------------------------------------------------------------------------- 1 | const selectValue = obj => obj.x 2 | 3 | const makeDiagramModel = (emissions, end) => { 4 | let completion = end 5 | if (!completion) { 6 | completion = Math.max.apply(null, emissions.map(selectValue)) 7 | } 8 | 9 | return { 10 | emissions, 11 | completion 12 | } 13 | } 14 | 15 | export default makeDiagramModel 16 | -------------------------------------------------------------------------------- /src/models/emissions/index.js: -------------------------------------------------------------------------------- 1 | import makeDiagramModel from './makeDiagramModel' 2 | import makeVirtualStream from './makeVirtualStream' 3 | import mapStreamToEmissions from './mapStreamToEmissions' 4 | import transformEmissions from './transformEmissions' 5 | 6 | export { 7 | makeDiagramModel, 8 | makeVirtualStream, 9 | mapStreamToEmissions, 10 | transformEmissions, 11 | } 12 | -------------------------------------------------------------------------------- /src/components/observable/emissionsView.js: -------------------------------------------------------------------------------- 1 | import { PropTypes} from 'react' 2 | import fromEmissions from './fromEmissions' 3 | 4 | const EmissionsView = ({ emissions, end, completion, ...props }) => fromEmissions(emissions, end, completion)(props) 5 | 6 | EmissionsView.propTypes = { 7 | completion: PropTypes.number.isRequired, 8 | end: PropTypes.number.isRequired 9 | } 10 | 11 | export default EmissionsView 12 | -------------------------------------------------------------------------------- /src/components/observable/constants.js: -------------------------------------------------------------------------------- 1 | import Color from 'goethe' 2 | 3 | export const PADDING_FACTOR = 0.03 4 | export const HEIGHT_FACTOR = 0.1 5 | export const ARROW_HEIGHT_FACTOR = 0.24 6 | export const ARROW_WIDTH_FACTOR = 0.06 7 | export const EMISSION_RADIUS = 0.28 8 | export const leftGradientColor = Color([ 227, 89, 18 ]).lighten(.13) 9 | export const rightGradientColor = Color([ 197, 5, 59 ]).lighten(.13) 10 | 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import EmissionsView from './components/observable/index' 2 | import TransitionEmissionsView from './components/transition/index' 3 | import DraggableEmissionsView from './components/draggable/index' 4 | import OperatorDiagram from './components/operatorDiagram/index' 5 | import ChainDiagram from './components/chainDiagram/index' 6 | 7 | export { 8 | EmissionsView, 9 | TransitionEmissionsView, 10 | DraggableEmissionsView, 11 | OperatorDiagram, 12 | ChainDiagram 13 | } 14 | 15 | export { transformEmissions } from './models/emissions/index' 16 | 17 | export default OperatorDiagram 18 | -------------------------------------------------------------------------------- /scripts/closure-minify: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Source: https://github.com/ReactiveX/rxjs/blob/master/tools/make-closure-core.js 4 | 5 | var compiler = require('google-closure-compiler-js').compile; 6 | var fs = require('fs'); 7 | 8 | var source = fs.readFileSync('dist/rxjs-diagrams.js', 'utf8'); 9 | 10 | var compilerFlags = { 11 | jsCode: [{src: source}], 12 | languageIn: 'ES3', 13 | createSourceMap: true, 14 | }; 15 | 16 | var output = compiler(compilerFlags); 17 | 18 | fs.writeFileSync('dist/rxjs-diagrams.min.js', output.compiledCode, 'utf8'); 19 | fs.writeFileSync('dist/rxjs-diagrams.min.js.map', output.sourceMap, 'utf8'); 20 | -------------------------------------------------------------------------------- /src/components/ObservableRenderer.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | 3 | class ObservableRenderer extends PureComponent { 4 | state = { 5 | value: undefined 6 | } 7 | 8 | componentWillMount() { 9 | const { source } = this.props 10 | 11 | this.sub = source.subscribe(value => { 12 | this.setState({ value }) 13 | }) 14 | } 15 | 16 | componentWillUnmount() { 17 | this.sub.unsubscribe() 18 | } 19 | 20 | render() { 21 | const { transform } = this.props 22 | const { value } = this.state 23 | 24 | return value ? transform(value) : null 25 | } 26 | } 27 | 28 | export default ObservableRenderer 29 | -------------------------------------------------------------------------------- /src/components/observable/makeTransformFactor.js: -------------------------------------------------------------------------------- 1 | import { 2 | EMISSION_RADIUS, 3 | ARROW_WIDTH_FACTOR, 4 | PADDING_FACTOR 5 | } from './constants' 6 | 7 | const makeTransformFactor = ({ width, height }) => { 8 | const strokeFactor = 2 / height 9 | const emissionRadius = EMISSION_RADIUS + strokeFactor 10 | const boundedPadding = (PADDING_FACTOR * width > emissionRadius * height) ? PADDING_FACTOR : (emissionRadius * height) / width 11 | const upperBound = 1 - boundedPadding - ARROW_WIDTH_FACTOR 12 | 13 | const transformFactor = x => ( 14 | (upperBound - boundedPadding) * x + boundedPadding 15 | ) 16 | 17 | return transformFactor 18 | } 19 | 20 | export default makeTransformFactor 21 | -------------------------------------------------------------------------------- /src/components/draggable/draggableView.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@kadira/storybook' 3 | import { withKnobs, number, boolean } from '@kadira/storybook-addon-knobs'; 4 | 5 | import DraggableView from './draggableView' 6 | 7 | storiesOf('DraggableView', module) 8 | .addDecorator(withKnobs) 9 | .add('Default', () => ( 10 | 23 | )) 24 | 25 | -------------------------------------------------------------------------------- /webpack.umd.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var path = require('path') 3 | 4 | module.exports = { 5 | resolve: { 6 | extensions: [ '.js' ], 7 | }, 8 | plugins: [ 9 | new webpack.optimize.OccurrenceOrderPlugin() 10 | ], 11 | module: { 12 | loaders: [ 13 | { 14 | test: /\.js$/, 15 | loader: 'babel-loader', 16 | include: path.join(__dirname, 'src'), 17 | exclude: /node_modules/ 18 | } 19 | ] 20 | }, 21 | externals: { 22 | react: { 23 | root: 'React', 24 | commonjs2: 'react', 25 | commonjs: 'react', 26 | amd: 'react' 27 | } 28 | }, 29 | output: { 30 | library: 'RxJSDiagrams', 31 | libraryTarget: 'umd' 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/transition/transitionView.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@kadira/storybook' 3 | import { withKnobs, number, boolean } from '@kadira/storybook-addon-knobs'; 4 | 5 | import TransitionView from './transitionView' 6 | 7 | storiesOf('TransitionView', module) 8 | .addDecorator(withKnobs) 9 | .add('Default', () => ( 10 | 23 | )) 24 | 25 | -------------------------------------------------------------------------------- /src/components/observable/arrow.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import scaleVector from '../../utils/scaleVector' 4 | import points from '../../utils/points' 5 | import { 6 | HEIGHT_FACTOR, 7 | ARROW_WIDTH_FACTOR, 8 | ARROW_HEIGHT_FACTOR 9 | } from './constants' 10 | 11 | const Arrow = ({ height, width, id }) => ( 12 | 24 | ) 25 | 26 | export default Arrow 27 | -------------------------------------------------------------------------------- /src/components/transition/transitionView.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { EmissionsView } from '../observable' 3 | import { Motion, spring, presets } from 'react-motion' 4 | 5 | const parameters = { 6 | stiffness: 30, 7 | damping: 10 8 | } 9 | 10 | class TransitionObservableView extends Component { 11 | static propTypes = { 12 | noTransition: PropTypes.bool 13 | } 14 | 15 | render() { 16 | const { noTransition, end } = this.props 17 | 18 | return ( 19 | 27 | { 28 | ({ end }) => ( 29 | 33 | ) 34 | } 35 | 36 | ) 37 | } 38 | } 39 | 40 | export default TransitionObservableView 41 | -------------------------------------------------------------------------------- /src/models/emissions/mapStreamToEmissions.js: -------------------------------------------------------------------------------- 1 | import { timestamp } from 'rxjs/operator/timestamp' 2 | import { reduce } from 'rxjs/operator/reduce' 3 | import { map } from 'rxjs/operator/map' 4 | 5 | import { COMPLETION_OFFSET } from './makeVirtualStream' 6 | import makeDiagramModel from './makeDiagramModel' 7 | 8 | const minZero = x => Math.max(0, x) 9 | 10 | const mapStreamToEmissions = (scheduler, emission$) => { 11 | const res = emission$ 12 | ::timestamp(scheduler) 13 | ::reduce((acc, { value, timestamp }) => { 14 | acc.push({ x: timestamp, d: value }) 15 | return acc 16 | }, []) 17 | ::map(emissions => ( 18 | makeDiagramModel(emissions.map(e => ( 19 | e.x !== scheduler.now() ? e : { 20 | ...e, 21 | x: minZero(scheduler.now() - COMPLETION_OFFSET) 22 | } 23 | )), Math.max(COMPLETION_OFFSET, scheduler.now() - COMPLETION_OFFSET)) 24 | )) 25 | 26 | return res 27 | } 28 | 29 | export default mapStreamToEmissions 30 | -------------------------------------------------------------------------------- /src/models/emissions/makeVirtualStream.js: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs/Observable' 2 | import { merge } from 'rxjs/observable/merge' 3 | import { takeUntil } from 'rxjs/operator/takeUntil' 4 | 5 | export const COMPLETION_OFFSET = 0.00001 6 | 7 | const makeVirtualEmission = (scheduler, value, delay) => { 8 | return new Observable(observer => { 9 | scheduler.schedule(() => { 10 | observer.next(value) 11 | }, delay, value) 12 | }) 13 | } 14 | 15 | const makeVirtualStream = (scheduler, diagram) => { 16 | const { emissions, completion } = diagram 17 | 18 | const partials = emissions.map(({ x, d }) => ( 19 | makeVirtualEmission(scheduler, d, x) 20 | )) 21 | 22 | const completion$ = makeVirtualEmission(scheduler, null, completion + COMPLETION_OFFSET) 23 | 24 | const emission$ = merge( 25 | ...partials, 26 | Number.POSITIVE_INFINITY, 27 | scheduler 28 | )::takeUntil(completion$) 29 | 30 | return emission$ 31 | } 32 | 33 | export default makeVirtualStream 34 | -------------------------------------------------------------------------------- /src/components/observable/completion.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { 4 | rightGradientColor, 5 | EMISSION_RADIUS 6 | } from './constants' 7 | 8 | const COMPLETION_HEIGHT = 2.4 * EMISSION_RADIUS 9 | const BOLD_FACTOR = 1.2 10 | 11 | const rectStyle = isDraggable => ({ 12 | cursor: isDraggable ? 'ew-resize' : 'default' 13 | }) 14 | 15 | const Completion = ({ 16 | isDraggable, 17 | x, 18 | height, 19 | width, 20 | bold, 21 | onMouseDown 22 | }) => ( 23 | { 30 | evt.preventDefault() 31 | onMouseDown && onMouseDown({ x }) 32 | }} 33 | onTouchStart={evt => { 34 | onMouseDown && onMouseDown({ x }) 35 | }} 36 | style={rectStyle(isDraggable)} 37 | /> 38 | ) 39 | 40 | export default Completion 41 | 42 | -------------------------------------------------------------------------------- /src/components/operatorDiagram/transformNote.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { white, black, gray } from '../../constants/colors' 3 | import { fontFamily, fontSize } from '../../constants/font' 4 | 5 | const textStyle = height => ({ 6 | fontFamily, 7 | fontSize: `${height * 0.24}px`, 8 | lineHeight: `${height * 0.24}px`, 9 | textShadow: 'none' 10 | }) 11 | 12 | const TransformNote = ({ 13 | stroke, 14 | children, 15 | width = 500, 16 | height = 50, 17 | x = 0, 18 | y = 0 19 | }) => ( 20 | 21 | 30 | 31 | 40 | {children} 41 | 42 | 43 | ) 44 | 45 | export default TransformNote 46 | -------------------------------------------------------------------------------- /src/models/emissions/transformEmissions.js: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs/Observable' 2 | import { from } from 'rxjs/observable/from' 3 | import makeScheduler from './makeScheduler' 4 | import makeVirtualStream from './makeVirtualStream' 5 | import makeDiagramModel from './makeDiagramModel' 6 | import mapStreamToEmissions from './mapStreamToEmissions' 7 | 8 | const transformEmissions = (transform, completion, ...emissionsArr) => new Observable(observer => { 9 | const scheduler = makeScheduler() 10 | const emission$Arr = emissionsArr.map(emissions => ( 11 | makeVirtualStream(scheduler, makeDiagramModel(emissions, completion)) 12 | )) 13 | 14 | let emission$ 15 | try { 16 | emission$ = from(transform(...emission$Arr, scheduler)) 17 | } catch (err) { 18 | observer.error(err) 19 | return undefined 20 | } 21 | 22 | const result = mapStreamToEmissions(scheduler, emission$) 23 | const sub = result.subscribe(observer) 24 | 25 | scheduler.flush() 26 | 27 | return sub.unsubscribe.bind(sub) 28 | }) 29 | 30 | export default transformEmissions 31 | -------------------------------------------------------------------------------- /src/components/observable/view.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@kadira/storybook' 3 | import { withKnobs, number, boolean } from '@kadira/storybook-addon-knobs'; 4 | 5 | import ObservableView from './view' 6 | import EmissionsView from './emissionsView' 7 | 8 | storiesOf('Observable', module) 9 | .addDecorator(withKnobs) 10 | .add('View', () => ( 11 | 22 | )) 23 | .add('EmissionsView', () => ( 24 | 37 | )) 38 | 39 | -------------------------------------------------------------------------------- /src/components/observable/defs.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { 4 | leftGradientColor, 5 | rightGradientColor 6 | } from './constants' 7 | 8 | const Defs = ({ id, x = 1 }) => ( 9 | 10 | 11 | 12 | 13 | 14 | {(x && x < 1) && ( 15 | 16 | )} 17 | 18 | {(x && x < 1) && ( 19 | 20 | )} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 35 | 36 | 37 | 38 | 39 | ) 40 | 41 | export default Defs 42 | -------------------------------------------------------------------------------- /src/components/observable/fromEmissions.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ObservableView from './view' 3 | import { isEmissionsArr } from '../../utils/isEmissionsArr' 4 | 5 | const selectValue = obj => obj.x 6 | 7 | function fromEmissions(arr, range, completion) { 8 | if (!isEmissionsArr(arr)) { 9 | console.error([ 10 | 'Expected each value in `emissions` to be an emission', 11 | '({ x: [number], d: [string] })' 12 | ].join('. ')) 13 | 14 | return props => ( 15 | 22 | ) 23 | } 24 | 25 | const min = Math.min.apply(null, arr.map(selectValue)) 26 | const max = typeof range === 'number' ? range : completion 27 | 28 | const emissions = arr 29 | .filter(({ x }) => x <= completion) 30 | .sort((a, b) => a.x - b.x) 31 | .map(({ x, ...rest }) => ({ 32 | ...rest, 33 | x: x / max 34 | })) 35 | 36 | return props => ( 37 | 44 | ) 45 | } 46 | 47 | export default fromEmissions 48 | -------------------------------------------------------------------------------- /src/components/observable/separators.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { white } from '../../constants/colors' 4 | import repeat from '../../utils/repeat' 5 | import points from '../../utils/points' 6 | import scaleVector from '../../utils/scaleVector' 7 | 8 | import { 9 | HEIGHT_FACTOR 10 | } from './constants' 11 | 12 | const WIDTH_FACTOR = 0.005 13 | 14 | const Separators = ({ height, width, transformFactor }) => ( 15 | 16 | { 17 | repeat(0, 1, 20).map((f, i) => { 18 | const x1 = Math.round((transformFactor(f) - WIDTH_FACTOR) * width) 19 | const x2 = Math.round((transformFactor(f) + WIDTH_FACTOR) * width) 20 | 21 | return ( 22 | 23 | 31 | 39 | 40 | ) 41 | }) 42 | } 43 | 44 | ) 45 | 46 | export default Separators 47 | -------------------------------------------------------------------------------- /src/components/chainDiagram/chainDiagram.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@kadira/storybook' 3 | import { distinctUntilChanged } from 'rxjs/operator/distinctUntilChanged' 4 | import { delay } from 'rxjs/operator/delay' 5 | import { withKnobs, number } from '@kadira/storybook-addon-knobs'; 6 | 7 | import ChainDiagram from './chainDiagram' 8 | import OperatorDiagram from '../operatorDiagram/index' 9 | 10 | const end = 100 11 | const completion = 80 12 | const emissions = [ 13 | { x: 5, d: 1 }, 14 | { x: 20, d: 2 }, 15 | { x: 35, d: 2 }, 16 | { x: 60, d: 1 }, 17 | { x: 70, d: 3 } 18 | ] 19 | 20 | storiesOf('ChainDiagram', module) 21 | .addDecorator(withKnobs) 22 | .add('.delay(5).distinctUntilChanged()', () => ( 23 | 29 | obs::delay(5, s)} 32 | label=".sleep(5)" 33 | /> 34 | obs::distinctUntilChanged()} 36 | label=".distinctUntilChanged()" 37 | /> 38 | obs::delay(5, s)} 40 | label=".delay(5)" 41 | /> 42 | 43 | 44 | 45 | 46 | )) 47 | -------------------------------------------------------------------------------- /src/components/observable/emission.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | 3 | import { EMISSION_RADIUS } from './constants' 4 | import { white, black } from '../../constants/colors' 5 | import { fontFamily, fontSize } from '../../constants/font' 6 | 7 | const circleStyle = isDraggable => ({ 8 | cursor: isDraggable ? 'ew-resize' : 'default' 9 | }) 10 | 11 | const textStyle = height => ({ 12 | fontFamily, 13 | fontSize: `${height * 0.24}px`, 14 | lineHeight: `${height * 0.24}px`, 15 | textShadow: 'none', 16 | userSelect: 'none', 17 | pointerEvents: 'none' 18 | }) 19 | 20 | class Emission extends PureComponent { 21 | render() { 22 | const { 23 | x, 24 | d, 25 | width, 26 | height, 27 | stroke, 28 | onMouseDown, 29 | isDraggable, 30 | isDragging, 31 | id, 32 | ...rest 33 | } = this.props 34 | 35 | return ( 36 | 37 | { 46 | evt.preventDefault() 47 | onMouseDown && onMouseDown({ ...rest, x, d }) 48 | }} 49 | onTouchStart={evt => { 50 | onMouseDown && onMouseDown({ ...rest, x, d }) 51 | }} 52 | filter={isDragging ? `url(#shadow-${id})` : ''} 53 | /> 54 | 55 | 64 | {d} 65 | 66 | 67 | ) 68 | } 69 | } 70 | 71 | export default Emission 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rxjs-diagrams", 3 | "version": "1.4.8", 4 | "description": "React components for visualising RxJS observables and operators", 5 | "main": "lib/index.js", 6 | "jsnext:main": "es/index.js", 7 | "module": "es/index.js", 8 | "author": "Phil Plückthun (https://github.com/philpl)", 9 | "bugs": { 10 | "url": "https://github.com/philpl/rxjs-diagrams/issues" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/philpl/rxjs-diagrams.git" 15 | }, 16 | "license": "CC0-1.0", 17 | "scripts": { 18 | "storybook": "start-storybook -p 9001 -c .storybook", 19 | "build:storybook": "build-storybook -o storybook -c .storybook", 20 | "clean": "rimraf es lib dist", 21 | "build:cjs": "BABEL_ENV=commonjs babel src --out-dir lib", 22 | "build:es": "babel src --out-dir es", 23 | "build:umd": "webpack --config webpack.umd.js src/index.js dist/rxjs-diagrams.js", 24 | "postbuild:umd": "./scripts/closure-minify", 25 | "build": "npm-run-all --parallel build:cjs build:es build:umd", 26 | "prepublish": "npm-run-all clean build", 27 | "version": "npm-run-all build", 28 | "postversion": "git push && git push --tags" 29 | }, 30 | "peerDependencies": { 31 | "react": ">=15.4.2", 32 | "rxjs": ">=5.0.3" 33 | }, 34 | "devDependencies": { 35 | "@kadira/storybook": "^2.35.2", 36 | "@kadira/storybook-addon-knobs": "^1.7.1", 37 | "@kadira/storybook-addons": "^1.6.1", 38 | "babel-cli": "^6.18.0", 39 | "babel-eslint": "^7.1.1", 40 | "babel-loader": "^6.2.10", 41 | "babel-plugin-external-helpers": "^6.18.0", 42 | "babel-preset-philpl": "^0.5.0", 43 | "cross-env": "^3.1.4", 44 | "eslint": "^3.13.0", 45 | "eslint-config-excellence": "^1.17.1", 46 | "eslint-plugin-react": "^6.9.0", 47 | "google-closure-compiler-js": "^20161201.0.0", 48 | "npm-run-all": "^4.0.0", 49 | "react": "^15.4.2", 50 | "react-dom": "^15.4.2", 51 | "rimraf": "^2.5.4", 52 | "rxjs": "^5.0.3", 53 | "webpack": "2.2.0-rc.4" 54 | }, 55 | "dependencies": { 56 | "goethe": "^1.2.1", 57 | "react-motion": "^0.4.7" 58 | }, 59 | "engines": { 60 | "npm": ">= 2.0.0", 61 | "node": ">= 0.12.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/components/chainDiagram/chainDiagram.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, PropTypes, cloneElement, Children } from 'react' 2 | import OperatorDiagram from '../operatorDiagram/index' 3 | 4 | class ChainDiagram extends PureComponent { 5 | static propTypes = { 6 | width: PropTypes.number, 7 | height: PropTypes.number, 8 | completion: PropTypes.number, 9 | end: PropTypes.number.isRequired, 10 | fit: PropTypes.bool, 11 | style: PropTypes.object 12 | } 13 | 14 | static defaultProps = { 15 | width: 500, 16 | height: 50, 17 | fit: false 18 | } 19 | 20 | state = { 21 | inputs: [] 22 | } 23 | 24 | onChange = (i, completion, emissions) => { 25 | const inputs = this.state.inputs.slice() 26 | inputs[i] = { completion, emissions } 27 | 28 | this.setState({ inputs }) 29 | } 30 | 31 | calcHeight = (i, { emissions }) => { 32 | const { height } = this.props 33 | const length = ( 34 | emissions ? 35 | (emissions.every(Array.isArray) ? emissions.length : 1) : 36 | 0 37 | ) 38 | 39 | const totalHeight = height * (2 + length) + 2 * (0.2 * height) 40 | 41 | return totalHeight 42 | } 43 | 44 | render() { 45 | const { 46 | height, 47 | width, 48 | children, 49 | completion, 50 | end, 51 | fit, 52 | style 53 | } = this.props 54 | 55 | const { inputs } = this.state 56 | 57 | let y = 0 58 | const newChildren = Children.map(children, (child, i) => { 59 | const lastY = y 60 | y += this.calcHeight(i, child.props) 61 | 62 | if (i === 0) { 63 | return cloneElement(child, { 64 | key: i, 65 | onChange: result => this.onChange(0, result.completion, result.emissions), 66 | end, 67 | completion, 68 | width, 69 | height, 70 | y: lastY 71 | }) 72 | } 73 | 74 | const input = inputs[i - 1] 75 | if (!input) { 76 | return null 77 | } 78 | 79 | const onChange = ({ completion, emissions }) => { 80 | const childEmissions = child.props.emissions || [] 81 | 82 | let inputEmissions = childEmissions.every(Array.isArray) ? 83 | childEmissions : [ childEmissions ] 84 | 85 | inputEmissions = emissions.concat(childEmissions) 86 | 87 | this.onChange( 88 | i, 89 | completion, 90 | inputEmissions 91 | ) 92 | } 93 | 94 | return cloneElement(child, { 95 | key: i, 96 | onChange, 97 | emissions: input.emissions, 98 | completion: input.completion, 99 | skip: 1, 100 | end, 101 | width, 102 | height, 103 | y: lastY 104 | }) 105 | }) 106 | 107 | return ( 108 | 114 | {newChildren} 115 | 116 | ) 117 | } 118 | } 119 | 120 | export default ChainDiagram 121 | -------------------------------------------------------------------------------- /src/components/observable/view.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | 3 | import { white, blue, black, gray } from '../../constants/colors' 4 | 5 | import { 6 | EMISSION_RADIUS, 7 | ARROW_WIDTH_FACTOR 8 | } from './constants' 9 | 10 | import Defs from './defs' 11 | import Arrow from './arrow' 12 | import Separators from './separators' 13 | import Emission from './emission' 14 | import Completion from './completion' 15 | import makeTransformFactor from './makeTransformFactor' 16 | 17 | let RUNNING_ID = 0 18 | const SEPARATORS = 20 19 | 20 | const ObservableView = ({ 21 | width = 500, 22 | height = 50, 23 | id = '', 24 | x, 25 | y, 26 | completion = 1, 27 | emissions = [], 28 | onMouseDownEmission, 29 | onMouseDownCompletion, 30 | getRef, 31 | isDragging, // NOTE: id of the emission that is being dragged 32 | onChange, // NOTE: Just for isDraggable 33 | style, 34 | className 35 | }) => { 36 | const transformFactor = makeTransformFactor({ width, height }) 37 | 38 | const last = emissions[emissions.length - 1] 39 | const lastCoincidesCompletion = last && last.x === completion 40 | 41 | const leftX = transformFactor(0) * width 42 | const rightX = transformFactor(1) * width 43 | 44 | return ( 45 | getRef && getRef(ref)} 52 | style={style} 53 | className={className} 54 | > 55 | 56 | 57 | 58 | 59 | { completion && ( 60 | { 67 | onMouseDownCompletion && onMouseDownCompletion({ 68 | ...data, 69 | leftX, 70 | rightX 71 | }) 72 | }} 73 | /> 74 | )} 75 | 76 | { 77 | emissions.map(({ x, ...props }, i) => ( 78 | { 88 | onMouseDownEmission && onMouseDownEmission({ 89 | ...data, 90 | leftX, 91 | rightX 92 | }) 93 | }} 94 | /> 95 | )) 96 | } 97 | 98 | ) 99 | } 100 | 101 | ObservableView.propTypes = { 102 | width: PropTypes.number, 103 | height: PropTypes.number, 104 | x: PropTypes.number, 105 | y: PropTypes.number, 106 | completion: PropTypes.number, 107 | emissions: PropTypes.arrayOf(PropTypes.object).isRequired, 108 | onMouseDownEmission: PropTypes.func, 109 | onMouseDownCompletion: PropTypes.func, 110 | getRef: PropTypes.func, 111 | isDragging: PropTypes.number, 112 | onChange: PropTypes.func, 113 | style: PropTypes.object, 114 | className: PropTypes.string 115 | } 116 | 117 | export default ObservableView 118 | -------------------------------------------------------------------------------- /src/components/operatorDiagram/operatorDiagram.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@kadira/storybook' 3 | import { distinctUntilChanged } from 'rxjs/operator/distinctUntilChanged' 4 | import { delay } from 'rxjs/operator/delay' 5 | import { switchMap } from 'rxjs/operator/switchMap' 6 | import { merge } from 'rxjs/operator/merge' 7 | import { elementAt } from 'rxjs/operator/elementAt' 8 | import { first } from 'rxjs/operator/first' 9 | import { combineLatest } from 'rxjs/observable/combineLatest' 10 | import { of } from 'rxjs/observable/of' 11 | import { withKnobs, number } from '@kadira/storybook-addon-knobs'; 12 | 13 | import OperatorDiagram from './operatorDiagram' 14 | 15 | const end = 100 16 | const completion = 80 17 | const emissions = [ 18 | { x: 5, d: 1 }, 19 | { x: 20, d: 2 }, 20 | { x: 35, d: 2 }, 21 | { x: 60, d: 1 }, 22 | { x: 70, d: 3 } 23 | ] 24 | 25 | const twoEmissions = [ 26 | emissions, 27 | [ 28 | { x: 5, d: 'A' }, 29 | { x: 28, d: 'B' }, 30 | { x: 35, d: 'C' }, 31 | { x: 45, d: 'D' }, 32 | { x: 70, d: 'E' } 33 | ] 34 | ] 35 | 36 | storiesOf('OperatorDiagram', module) 37 | .addDecorator(withKnobs) 38 | .add('fitting diagram', () => ( 39 | obs::distinctUntilChanged()} 45 | end={end} 46 | completion={completion} 47 | label=".distinctUntilChanged()" 48 | /> 49 | )) 50 | .add('double diagrams', () => ( 51 |
52 | obs::distinctUntilChanged()} 55 | end={end} 56 | completion={completion} 57 | label=".distinctUntilChanged()" 58 | /> 59 | 60 | obs::distinctUntilChanged()} 63 | end={end} 64 | completion={completion} 65 | label=".distinctUntilChanged()" 66 | /> 67 |
68 | )) 69 | .add('.delay(5)', () => ( 70 | obs::delay(5, scheduler)} 73 | end={end} 74 | completion={completion} 75 | label=".delay(5)" 76 | /> 77 | )) 78 | .add('.combineLatest((x, y) => \'\' + x + y)', () => ( 79 | combineLatest(a, b, (x, y) => '' + x + y)} 82 | end={end} 83 | completion={completion} 84 | label=".combineLatest((x, y) => '' + x + y)" 85 | /> 86 | )) 87 | .add('.switchMap((x) => Observable.of(x, x + 1))', () => ( 88 | obs::switchMap(x => ( 95 | of(x, s)::merge(of(x + 1, s)::delay(10, s)) 96 | ))} 97 | end={end} 98 | completion={completion} 99 | label=".switchMap((x) => Observable.of(x, x + 1))" 100 | /> 101 | )) 102 | .add('.elementAt(2)', () => ( 103 | obs::elementAt(2)} 106 | end={end} 107 | completion={completion} 108 | label=".elementAt(2)" 109 | /> 110 | )) 111 | .add('.first()', () => ( 112 | obs::first()} 115 | end={end} 116 | completion={completion} 117 | label=".first()" 118 | /> 119 | )) 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

RxJS Diagrams

3 |

4 | React Components for visualising RxJS observables and operators 5 |

6 | 7 | 8 |

9 | 10 | **RxJS Diagrams** provides React Components for interactively visualising RxJS observables and operators. 11 | It is a rewrite (and redesign) of the amazing [RxMarbles](http://rxmarbles.com/). 12 | The goal is to provide simple and reusable components for quickly explaining how RxJS works. 13 | 14 | ```bash 15 | npm install --save rxjs-diagrams 16 | ``` 17 | 18 | Don't forget to install its peer dependencies, `react` and `rxjs`. 19 | 20 | ## Usage 21 | 22 | ### One input stream 23 | 24 | This renders an SVG showing the input values and the result. 25 | The input values are converted to an observables and then transformed 26 | to an output using the transform prop. 27 | 28 | ```js 29 | import 'rxjs' // This imports all observables, operators, etc 30 | import OperatorDiagram from 'rxjs-diagrams' 31 | 32 | // Somewhere in your components... 33 | obs.distinctUntilChanged()} 36 | emissions={[ 37 | { x: 5, d: 1 }, 38 | { x: 35, d: 1 }, 39 | { x: 70, d: 3 } 40 | ]} 41 | end={80} 42 | completion={80} 43 | /> 44 | ``` 45 | 46 | ### Two input streams 47 | 48 | Having multiple input streams is as simple as passing multiple value arrays 49 | and accepting them in your transform function. 50 | 51 | ```js 52 | import { Observable } from 'rxjs' 53 | import OperatorDiagram from 'rxjs-diagrams' 54 | 55 | // Somewhere in your components... 56 | Observable.combineLatest(a, b, (x, y) => '' + x + y)} 59 | emissions={[ 60 | [ 61 | { x: 5, d: 1 }, 62 | { x: 35, d: 2 }, 63 | { x: 70, d: 3 } 64 | ], [ 65 | { x: 10, d: 'A' }, 66 | { x: 45, d: 'B' }, 67 | { x: 80, d: 'C' } 68 | ] 69 | ]} 70 | end={80} 71 | completion={80} 72 | /> 73 | ``` 74 | 75 | ## API 76 | 77 | Exports: 78 | 79 | - transformEmissions 80 | - EmissionsView (Docs TODO) 81 | - TransitionEmissionsView (Docs TODO) 82 | - DraggableEmissionsView (Docs TODO) 83 | - ChainDiagram (Docs TODO) 84 | - OperatorDiagram (also the default export) 85 | 86 | ### Emissions, End & Completion 87 | 88 | The common three values that describe your input are: emissions, end, and completion. 89 | This is enough for this library to generate an input observable. 90 | 91 | **Emissions** are an array of objects, which have a time value `x` and a label `d`. 92 | The value `x` must be a number. (Example: `{ x: 5, d: 'A' }`) 93 | 94 | **Completion** is the time value when your observable completes. It is a number 95 | and usually you'll want it to be larger than all `x` values of your emissions. 96 | 97 | **End** is where the component stops to draw your observable. It basically defines 98 | how long in time the diagram is. So if your `end` is `20` and an emission's `x` 99 | is `10`, then the emission will be drawn right in the center. 100 | 101 | ### OperatorDiagram 102 | 103 | #### Props 104 | 105 | - `label?: string`: Some text that describes your transformation. 106 | 107 | - `transform: (...input, scheduler)`: A function that transforms the input observables and 108 | produces an output. It receives the input observables as the first arguments and the scheduler 109 | last. You will need the scheduler to transform the virtual observable's time. For example for 110 | `delay`. More information on Schedulers 111 | [here](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/schedulers.md) 112 | 113 | - `emissions: Emission[] | Emission[][]`: Here you can pass an array of emissions (described above) 114 | or an array of an array of emissions, in case you want multiple input observables. 115 | 116 | - `end: number`: Described above. 117 | 118 | - `completion: number`: Described above. 119 | 120 | - `width: number`: The width of the resulting SVG. 121 | 122 | - `height: number`: The height of the resulting SVG component. 123 | 124 | -------------------------------------------------------------------------------- /src/components/draggable/draggableView.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import TransitionView from '../transition/transitionView' 3 | import { COMPLETION_OFFSET } from '../../models/emissions/makeVirtualStream' 4 | import { empty } from 'rxjs/observable/empty' 5 | import { fromEvent } from 'rxjs/observable/fromEvent' 6 | import { share } from 'rxjs/operator/share' 7 | import { takeUntil } from 'rxjs/operator/takeUntil' 8 | import { map } from 'rxjs/operator/map' 9 | import { merge } from 'rxjs/operator/merge' 10 | import { throttleTime } from 'rxjs/operator/throttleTime' 11 | import { _finally } from 'rxjs/operator/finally' 12 | 13 | const mousemove$ = typeof window === 'undefined' ? 14 | empty() : ( 15 | fromEvent(window, 'mousemove') 16 | ::merge( 17 | fromEvent(window, 'touchmove') 18 | ::map(({ touches }) => touches[0]) 19 | ) 20 | ::share() 21 | ) 22 | 23 | const mouseup$ = typeof window === 'undefined' ? 24 | empty() : ( 25 | fromEvent(window, 'mouseup') 26 | ::merge(fromEvent(window, 'touchend')) 27 | ::merge(fromEvent(window, 'touchcancel')) 28 | ::share() 29 | ) 30 | 31 | const transformEmissions = emissions => ( 32 | emissions 33 | .map((data, i) => ({ 34 | ...data, 35 | index: i 36 | })) 37 | ) 38 | 39 | class DraggableView extends PureComponent { 40 | state = { 41 | isDragging: -1, 42 | emissions: transformEmissions(this.props.emissions), 43 | completion: this.props.completion 44 | } 45 | 46 | storeRef = ref => { 47 | this.svg = ref 48 | } 49 | 50 | getMax = () => { 51 | const { end, completion } = this.props 52 | return typeof end === 'number' ? end : completion 53 | } 54 | 55 | updateX = (index, x) => { 56 | const { emissions } = this.state 57 | const { onChangeEmissions } = this.props 58 | 59 | const newEmissions = emissions.map(emission => ( 60 | emission.index === index ? 61 | { ...emission, x } : 62 | emission 63 | )) 64 | 65 | this.setState({ 66 | emissions: newEmissions 67 | }) 68 | 69 | if (onChangeEmissions) { 70 | onChangeEmissions(newEmissions) 71 | } 72 | } 73 | 74 | transformMove = (isCompletion, leftX, rightX, { clientX }) => { 75 | const { svg } = this 76 | const { completion } = this.props 77 | const { left, width } = svg.getBoundingClientRect() 78 | 79 | const scale = this.props.width / width 80 | const max = this.getMax() 81 | const range = rightX - leftX 82 | 83 | const relativeX = Math.max(0, scale * (clientX - left) - leftX) 84 | const newX = relativeX / range * max 85 | 86 | return Math.min( 87 | Math.max(isCompletion ? COMPLETION_OFFSET : 0, newX), 88 | isCompletion ? max : completion 89 | ) 90 | } 91 | 92 | onMouseDownEmission = ({ index, leftX, rightX }) => { 93 | const { emissions } = this.state 94 | 95 | this.setState({ isDragging: index }) 96 | 97 | mousemove$ 98 | ::takeUntil(mouseup$) 99 | ::_finally(() => { 100 | this.setState({ isDragging: -1 }) 101 | }) 102 | ::throttleTime(1000 / 60) // NOTE: Throttle to 60 FPS 103 | ::map(this.transformMove.bind(this, false, leftX, rightX)) 104 | .subscribe(x => this.updateX(index, x)) 105 | } 106 | 107 | onMouseDownCompletion = ({ leftX, rightX }) => { 108 | const { svg } = this 109 | 110 | mousemove$ 111 | ::takeUntil(mouseup$) 112 | ::throttleTime(1000 / 60) // NOTE: Throttle to 60 FPS 113 | ::map(this.transformMove.bind(this, true, leftX, rightX)) 114 | .subscribe(x => { 115 | const { onChangeCompletion } = this.props 116 | 117 | this.setState({ completion: x }) 118 | 119 | if (onChangeCompletion) { 120 | onChangeCompletion(x) 121 | } 122 | }) 123 | } 124 | 125 | componentWillReceiveProps(nextProps) { 126 | if (this.props.emissions !== nextProps.emissions) { 127 | this.setState({ 128 | emissions: transformEmissions(nextProps.emissions) 129 | }) 130 | } 131 | 132 | if (this.props.completion !== nextProps.completion) { 133 | this.setState({ 134 | completion: nextProps.completion 135 | }) 136 | } 137 | } 138 | 139 | render() { 140 | const { completion, emissions, isDragging } = this.state 141 | 142 | return ( 143 | 152 | ) 153 | } 154 | } 155 | 156 | export default DraggableView 157 | -------------------------------------------------------------------------------- /src/components/operatorDiagram/operatorDiagram.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, PropTypes } from 'react' 2 | import { transformEmissions, makeDiagramModel } from '../../models/emissions/index' 3 | import DraggableView from '../draggable/index' 4 | import TransitionView from '../transition/index' 5 | import TransformNote from './transformNote' 6 | 7 | import { 8 | leftGradientColor, 9 | rightGradientColor 10 | } from '../observable/constants' 11 | 12 | const PADDING_FACTOR = 0.2 13 | 14 | const getInput = emissions => { 15 | const hasMultipleInputs = emissions.some(Array.isArray) 16 | 17 | return hasMultipleInputs ? 18 | emissions : 19 | [ emissions ] 20 | } 21 | 22 | class OperatorDiagram extends PureComponent { 23 | static propTypes = { 24 | label: PropTypes.string, 25 | transform: PropTypes.func.isRequired, 26 | skip: PropTypes.number, 27 | emissions: PropTypes.oneOfType([ 28 | PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.object)), 29 | PropTypes.arrayOf(PropTypes.object) 30 | ]).isRequired, 31 | onChange: PropTypes.func, 32 | x: PropTypes.number, 33 | y: PropTypes.number, 34 | fit: PropTypes.bool, 35 | style: PropTypes.object 36 | } 37 | 38 | static defaultProps = { 39 | skip: 0, 40 | width: 500, 41 | height: 50, 42 | fit: false 43 | } 44 | 45 | state = { 46 | completion: this.props.completion 47 | } 48 | 49 | processInput = (input, completion) => { 50 | const { transform, onChange } = this.props 51 | 52 | const output$ = transformEmissions(transform, completion, ...input) 53 | 54 | this.input = input // Store input for next use 55 | 56 | output$.subscribe(output => { 57 | this.setState({ output, completion }) 58 | 59 | if (onChange) { 60 | onChange(output) 61 | } 62 | }, err => { 63 | console.error(err) 64 | }) 65 | } 66 | 67 | updateEmissions = (i, emissions) => { 68 | const input = this.input.slice() 69 | input[i] = emissions 70 | 71 | this.processInput(input, this.state.completion) 72 | } 73 | 74 | updateCompletion = completion => { 75 | this.processInput(this.input, completion) 76 | } 77 | 78 | componentWillMount() { 79 | this.processInput(getInput(this.props.emissions), this.props.completion) 80 | } 81 | 82 | componentWillReceiveProps(nextProps) { 83 | if ( 84 | this.props.emissions !== nextProps.emissions || 85 | this.props.completion !== nextProps.completion 86 | ) { 87 | this.processInput(getInput(nextProps.emissions), nextProps.completion) 88 | } 89 | } 90 | 91 | render() { 92 | const { 93 | end, 94 | width, 95 | height, 96 | transform, 97 | label, 98 | emissions, 99 | skip, 100 | x, 101 | y, 102 | fit, 103 | style 104 | } = this.props 105 | 106 | const { 107 | output, 108 | completion 109 | } = this.state 110 | 111 | if (!output) { 112 | return null 113 | } 114 | 115 | const input = getInput(emissions) 116 | const totalHeight = height * (2 + input.length - skip) + 2 * (PADDING_FACTOR * height) 117 | 118 | return ( 119 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | { 135 | input.slice(skip).map((e, i) => ( 136 | this.updateEmissions(i, input)} 143 | onChangeCompletion={this.updateCompletion} 144 | y={i * height} 145 | /> 146 | )) 147 | } 148 | 149 | 156 | {label || transfom.toString()} 157 | 158 | 159 | 166 | 167 | ) 168 | } 169 | } 170 | 171 | export default OperatorDiagram 172 | -------------------------------------------------------------------------------- /src/models/emissions/index.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@kadira/storybook' 3 | import { distinctUntilChanged } from 'rxjs/operator/distinctUntilChanged' 4 | import { map } from 'rxjs/operator/map' 5 | import { first } from 'rxjs/operator/first' 6 | import { delay } from 'rxjs/operator/delay' 7 | import { combineLatest } from 'rxjs/observable/combineLatest' 8 | import { scan } from 'rxjs/operator/scan' 9 | 10 | import ObservableRenderer from '../../components/ObservableRenderer' 11 | import { transformEmissions } from './index' 12 | import TransitionView from '../../components/transition/transitionView' 13 | 14 | storiesOf('Emissions', module) 15 | .add('.distinctUntilChanged()', () => { 16 | const width = 80 17 | const input = [ 18 | { x: 5, d: 1 }, 19 | { x: 20, d: 2 }, 20 | { x: 35, d: 2 }, 21 | { x: 60, d: 1 }, 22 | { x: 70, d: 3 } 23 | ] 24 | 25 | const output = transformEmissions( 26 | obs => obs::distinctUntilChanged(), 27 | width, 28 | input 29 | ) 30 | 31 | return ( 32 |
33 | 38 | 39 | ( 42 | 47 | )} 48 | /> 49 |
50 | ) 51 | }) 52 | .add('.map(x => x * 10)', () => { 53 | const width = 80 54 | const input = [ 55 | { x: 5, d: 1 }, 56 | { x: 20, d: 2 }, 57 | { x: 35, d: 2 }, 58 | { x: 60, d: 1 }, 59 | { x: 70, d: 3 } 60 | ] 61 | 62 | const output = transformEmissions( 63 | obs => obs::map(x => x * 10), 64 | width, 65 | input 66 | ) 67 | 68 | return ( 69 |
70 | 75 | 76 | ( 79 | 84 | )} 85 | /> 86 |
87 | ) 88 | }) 89 | .add('.first()', () => { 90 | const width = 80 91 | const input = [ 92 | { x: 5, d: 1 }, 93 | { x: 20, d: 2 }, 94 | { x: 35, d: 2 }, 95 | { x: 60, d: 1 }, 96 | { x: 70, d: 3 } 97 | ] 98 | 99 | const output = transformEmissions( 100 | obs => obs::first(), 101 | width, 102 | input 103 | ) 104 | 105 | return ( 106 |
107 | 112 | 113 | ( 116 | 121 | )} 122 | /> 123 |
124 | ) 125 | }) 126 | .add('.delay(5)', () => { 127 | const width = 80 128 | const input = [ 129 | { x: 5, d: 1 }, 130 | { x: 20, d: 2 }, 131 | { x: 35, d: 2 }, 132 | { x: 60, d: 1 }, 133 | { x: 70, d: 3 } 134 | ] 135 | 136 | const output = transformEmissions( 137 | (obs, scheduler) => obs::delay(5, scheduler), 138 | width, 139 | input 140 | ) 141 | 142 | return ( 143 |
144 | 149 | 150 | ( 153 | 158 | )} 159 | /> 160 |
161 | ) 162 | }) 163 | .add('.combineLatest((x, y) => `${x}${y}`)', () => { 164 | const width = 80 165 | const inputA = [ 166 | { x: 5, d: 1 }, 167 | { x: 20, d: 2 }, 168 | { x: 35, d: 3 }, 169 | { x: 60, d: 4 }, 170 | { x: 70, d: 5 } 171 | ] 172 | 173 | const inputB = [ 174 | { x: 5, d: 'A' }, 175 | { x: 28, d: 'B' }, 176 | { x: 35, d: 'C' }, 177 | { x: 45, d: 'D' }, 178 | { x: 70, d: 'E' } 179 | ] 180 | 181 | const output = transformEmissions( 182 | (a, b, scheduler) => combineLatest(a, b, (x, y) => `${x}${y}`, scheduler), 183 | width, 184 | inputA, 185 | inputB 186 | ) 187 | 188 | return ( 189 |
190 | 195 | 200 | 201 | ( 204 | 209 | )} 210 | /> 211 |
212 | ) 213 | }) 214 | .add('.scan((acc, d) => acc + d)', () => { 215 | const width = 80 216 | const input = [ 217 | { x: 5, d: 1 }, 218 | { x: 20, d: 2 }, 219 | { x: 35, d: 3 }, 220 | { x: 60, d: 4 }, 221 | { x: 70, d: 5 } 222 | ] 223 | 224 | const reducer = (acc, d) => acc + d 225 | 226 | const output = transformEmissions( 227 | obs => obs::scan((acc, d) => acc + d), 228 | width, 229 | input 230 | ) 231 | 232 | return ( 233 |
234 | 239 | 240 | ( 243 | 248 | )} 249 | /> 250 |
251 | ) 252 | }) 253 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | 117 | 118 | --------------------------------------------------------------------------------