diff --git a/config/partials/entryLocal.js b/config/partials/entryLocal.js
new file mode 100644
index 0000000..361703a
--- /dev/null
+++ b/config/partials/entryLocal.js
@@ -0,0 +1,12 @@
+'use strict';
+
+var partial = require('webpack-partial').default;
+
+module.exports = function (config) {
+ return partial(config, {
+ entry: {bundle: [
+ 'whatwg-fetch',
+ './local.js'
+ ]}
+ });
+};
diff --git a/config/partials/outputLocal.js b/config/partials/outputLocal.js
new file mode 100644
index 0000000..a07bf56
--- /dev/null
+++ b/config/partials/outputLocal.js
@@ -0,0 +1,18 @@
+'use strict';
+
+var path = require('path');
+var partial = require('webpack-partial').default;
+
+var ROOT = process.cwd();
+var BUILD = path.join(ROOT, 'dash_renderer');
+
+module.exports = function (config) {
+ return partial(config, {
+ output: {
+ path: BUILD,
+ publicPath: '/dash_renderer/',
+ // TODO: Bundle filename should be hashed (#10)
+ filename: 'local.js'
+ }
+ });
+};
diff --git a/config/webpack.config.local.js b/config/webpack.config.local.js
new file mode 100644
index 0000000..77ffe40
--- /dev/null
+++ b/config/webpack.config.local.js
@@ -0,0 +1,17 @@
+'use strict';
+
+var compose = require('ramda').compose;
+
+var babel = require('./partials/babel');
+var defineEnv = require('./partials/defineEnv');
+var entryLocal = require('./partials/entryLocal');
+var outputLocal = require('./partials/outputLocal');
+var baseConfig = require('./webpack.config');
+
+// TODO: support locally served source maps in production (#11)
+module.exports = compose(
+ babel,
+ defineEnv,
+ entryLocal,
+ outputLocal
+)(baseConfig);
diff --git a/index.html b/index.html
index 5da1975..d072788 100644
--- a/index.html
+++ b/index.html
@@ -3,13 +3,19 @@
-
+
-
+
-
+
+
-
+
+
+
+
+
diff --git a/package.json b/package.json
index 6b33ef0..2c0a2ee 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"scripts": {
"build-dev": "./node_modules/.bin/webpack --config=config/webpack.config.dev.js",
"build-prod": "cross-env NODE_ENV=production node node_modules/.bin/webpack --config=config/webpack.config.prod.js",
+ "build-local": "cross-env NODE_ENV=production node node_modules/.bin/webpack --config=config/webpack.config.local.js",
"dev": "./node_modules/.bin/webpack-dev-server --config=config/webpack.config.dev.js",
"hot": "./node_modules/.bin/webpack-dev-server --hot --config=config/webpack.config.hot.js",
"lint": "./node_modules/.bin/eslint --quiet --fix .",
diff --git a/src/actions/index.js b/src/actions/index.js
index bec3ada..b6e338c 100644
--- a/src/actions/index.js
+++ b/src/actions/index.js
@@ -185,7 +185,7 @@ function loadSavedStateAndTriggerVisibleInputs(dispatch, getState, skipTheseInpu
};
}
-function triggerDefaultState(dispatch, getState) {
+export function triggerDefaultState(dispatch, getState) {
const {graphs} = getState();
const {InputGraph} = graphs;
const allNodes = InputGraph.overallOrder();
@@ -448,7 +448,6 @@ export const notifyObservers = function(payload) {
}
-
promises.push(fetch('/update-component', {
method: 'POST',
headers: {
@@ -464,7 +463,6 @@ export const notifyObservers = function(payload) {
});
return res.json().then(function handleJson(data) {
-
// clear this item from the request queue
dispatch(setRequestQueue(
reject(
diff --git a/src/local.js b/src/local.js
new file mode 100644
index 0000000..9ecde38
--- /dev/null
+++ b/src/local.js
@@ -0,0 +1,185 @@
+/*eslint-env browser */
+import React from 'react';
+import ReactDOM from 'react-dom';
+require('es6-promise').polyfill();
+import {combineReducers} from 'redux';
+import {connect, Provider} from 'react-redux'
+import {createStore, applyMiddleware} from 'redux';
+import thunk from 'redux-thunk';
+import createLogger from 'redux-logger';
+import {pluck, reduce} from 'ramda';
+
+// Actions
+import {
+ computeGraphs,
+ computePaths,
+ setLayout,
+ triggerDefaultState
+} from './actions/index';
+
+// Render the app
+import renderTree from './renderTree';
+
+// Stores
+import layout from './reducers/layout';
+import graphs from './reducers/dependencyGraph';
+import paths from './reducers/paths';
+import requestQueue from './reducers/requestQueue';
+import * as API from './reducers/api';
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+/*
+ * Change the path here to try out different examples.
+ * Examples need to export appLayout and mapInputsToOutputs
+ * objects.
+*/
+import {appLayout, mapInputsToOutputs} from './local_examples/example_1_input_to_div';
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+/*
+ * You shouldn't have to edit anything below here
+ * This block below does a few things:
+ * - it transforms the API calls into
+ * some promises that call the `transform` method
+ * for the appropriate input-output pair.
+ * - it initializes the app without the API controller
+ */
+
+const dependenciesRequest = {
+ content: mapInputsToOutputs.map(object => {
+ object.events = [];
+ object.state = [];
+ return object;
+ }),
+ status: 200
+};
+
+window.fetch = function(url, options) {
+ const payload = JSON.parse(options.body);
+ return new Promise(resolveResonse => {
+ const inputOutputPair = mapInputsToOutputs.find(pair =>
+ (pair.output.id === payload.output.id) &&
+ (pair.output.property === payload.output.property)
+ );
+ resolveResonse({
+ status: 200,
+ json: () => new Promise(resolveJson => {
+ resolveJson({
+ response: {
+ props: {
+ 'id': inputOutputPair.output.id,
+ [inputOutputPair.output.property]: (
+ inputOutputPair.output.transform(
+ reduce(
+ (acc, input) => {
+ acc[`${input.id}.${input.property}`] = input.value;
+ return acc
+ },
+ {},
+ payload.inputs
+ )
+ )
+ )
+ }
+ }
+ })
+ })
+ })
+ });
+}
+
+
+// Initialize a store
+const reducer = combineReducers({
+ layout,
+ graphs,
+ paths,
+ requestQueue,
+ dependenciesRequest: API.dependenciesRequest
+});
+const logger = createLogger()
+let store;
+const initializeStore = () => {
+ if (store) {
+ return store;
+ }
+
+ store = createStore(
+ reducer,
+ applyMiddleware(thunk, logger)
+ );
+
+ window.store = store; /* global window:true */
+
+ return store;
+};
+
+store = initializeStore();
+
+store.dispatch({
+ type: 'dependenciesRequest',
+ payload: dependenciesRequest
+});
+store.dispatch(setLayout(appLayout));
+store.dispatch(computePaths({subTree: appLayout, startingPath: []}));
+store.dispatch(computeGraphs(dependenciesRequest.content));
+store.dispatch(triggerDefaultState);
+
+const ConnectedApp = connect(state => ({
+ layout: state.layout,
+ dependenciesRequest: state.dependenciesRequest
+}))(
+ props => {
+ return (
+
+ {renderTree(props.layout, props.dependenciesRequest.content)}
+
+ )
+ }
+);
+
+ReactDOM.render(
+
+
+ ,
+ document.getElementById('react-entry-point')
+);
diff --git a/src/local_examples/example_1_input_to_div.js b/src/local_examples/example_1_input_to_div.js
new file mode 100644
index 0000000..a4d4de1
--- /dev/null
+++ b/src/local_examples/example_1_input_to_div.js
@@ -0,0 +1,51 @@
+export const appLayout = {
+ 'type': 'Div',
+ 'namespace': 'dash_html_components',
+ 'props': {
+ 'content': [
+
+ {
+ 'type': 'Input',
+ 'namespace': 'dash_core_components',
+ 'props': {
+ 'id': 'my-input',
+ 'value': 'Initial value'
+ }
+ },
+
+ {
+ 'type': 'Div',
+ 'namespace': 'dash_html_components',
+ 'props': {
+ 'id': 'my-div'
+ }
+ }
+
+ ]
+ }
+};
+
+export const mapInputsToOutputs = [
+ {
+ inputs: [
+ {
+ id: 'my-input',
+ property: 'value'
+ }
+ ],
+ output: {
+ id: 'my-div',
+ property: 'content',
+
+ /*
+ * Of course, we can't have functions in the actual spec,
+ * but we could introduce a lightweight transformation
+ * language for modifying strings, accessing values in
+ * objects or arrays, arithmetic, etc.
+ */
+ transform: inputArguments => {
+ return `You've entered: ${inputArguments['my-input.value']}`;
+ }
+ }
+ }
+];
diff --git a/src/local_examples/example_2_register_components.js b/src/local_examples/example_2_register_components.js
new file mode 100644
index 0000000..fbbfc6e
--- /dev/null
+++ b/src/local_examples/example_2_register_components.js
@@ -0,0 +1,323 @@
+import R from 'ramda';
+
+import * as components from './example_components.react';
+
+export const appLayout = {
+ 'type': 'Div',
+ 'namespace': 'dash_html_components',
+ 'props': {
+ 'content': [
+
+ {
+ 'type': 'DataStore',
+ 'namespace': 'dash_functional_components',
+ 'props': {
+ 'id': 'my-data',
+ 'columns': {
+ 'Column 1': [1, 2, 3, 4, 5],
+ 'Column 2': [4, 2, 1, 4, 5],
+ 'Column 3': ['A', 'B', 'C' ,'D', 'E'],
+ }
+ }
+ },
+
+ {
+ 'type': 'Filter',
+ 'namespace': 'dash_functional_components',
+ 'props': {
+ 'id': 'my-filter',
+ /*
+ * The rest of the filter properties
+ * (like filterColumnId, filterValue, operation)
+ * are set dynamically from different controls
+ */
+ }
+ },
+
+ {
+ 'type': 'Dropdown',
+ 'namespace': 'dash_core_components',
+ 'props': {
+ 'id': 'filter-column-dropdown',
+ /*
+ * The rest of these properties like `options` and `value`
+ * are set dynamically from the DataStore component
+ */
+ }
+ },
+
+ {
+ 'type': 'Dropdown',
+ 'namespace': 'dash_core_components',
+ 'props': {
+ 'id': 'filter-operation-dropdown',
+ 'options': [
+ {'label': 'Less than', 'value': '<'},
+ {'label': 'Greater than', 'value': '>'},
+ {'label': 'Equals', 'value': '=='}
+ ],
+ 'value': '<'
+ }
+ },
+
+ {
+ 'type': 'Input',
+ 'namespace': 'dash_core_components',
+ 'props': {
+ 'id': 'my-input',
+ 'value': 3
+ }
+ },
+
+ {
+ 'type': 'Div',
+ 'namespace': 'dash_html_components',
+ 'props': {
+ 'className': 'row',
+ 'content': [
+ {
+ 'type': 'Div',
+ 'namespace': 'dash_html_components',
+ 'props': {
+ 'className': 'six columns',
+ 'content': {
+ 'type': 'Table',
+ 'namespace': 'dash_functional_components',
+ 'props': {
+ 'id': 'my-original-table'
+ }
+ }
+ }
+ },
+ {
+ 'type': 'Div',
+ 'namespace': 'dash_html_components',
+ 'props': {
+ 'className': 'six columns',
+ 'content': {
+ 'type': 'Table',
+ 'namespace': 'dash_functional_components',
+ 'props': {
+ 'id': 'my-filtered-table'
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+
+ {
+ 'type': 'Graph',
+ 'namespace': 'dash_core_components',
+ 'props': {
+ 'id': 'my-graph',
+ 'animate': true
+ }
+ }
+
+ ]
+ }
+};
+
+export const mapInputsToOutputs = [
+
+ // Display the columns as the dropdown
+ {
+ inputs: [
+ {
+ id: 'my-data',
+ property: 'columns'
+ },
+ ],
+ output: {
+ id: 'filter-column-dropdown',
+ property: 'options',
+
+ /*
+ * The properties of the components would need to be designed
+ * in a way that makes composing one components output into
+ * the next component's input declarative.
+ * In this case, we're setting the dropdown to be the columns
+ * through `keys` and `map` functions, which isn't declarative
+ * nor does it have a direct declarative analogue.
+ */
+ transform: inputArguments => (
+ R.keys(inputArguments['my-data.columns']).map(c => ({
+ label: c, value: c
+ }))
+ )
+ }
+ },
+
+ // Select a default column
+ {
+ inputs: [
+ {
+ id: 'my-data',
+ property: 'columns'
+ },
+ ],
+ output: {
+ id: 'filter-column-dropdown',
+ property: 'value',
+
+ /*
+ * The properties of the components would need to be designed
+ * in a way that makes composing one components output into
+ * the next component's input declarative.
+ * In this case, we're setting the dropdown to be the columns
+ * through `keys` and `map` functions, which isn't declarative
+ * nor does it have a direct declarative analogue.
+ */
+ transform: inputArguments => (
+ R.keys(inputArguments['my-data.columns'])[0]
+ )
+ }
+ },
+
+ // The `filterColumnId` of the filter is the dropdown
+ {
+ inputs: [
+ {
+ id: 'filter-operation-dropdown',
+ property: 'value'
+ },
+ ],
+ output: {
+ id: 'my-filter',
+ property: 'operation',
+
+ transform: inputArguments => inputArguments['filter-operation-dropdown.value']
+ }
+ },
+
+ // The input data (`columns`) for the filter is the data store
+ {
+ inputs: [
+ {
+ id: 'my-data',
+ property: 'columns'
+ },
+ ],
+ output: {
+ id: 'my-filter',
+ property: 'columns',
+
+ transform: inputArguments => inputArguments['my-data.columns']
+ }
+ },
+
+ // The `filterValue` of the filter is the input's value
+ {
+ inputs: [
+ {
+ id: 'my-input',
+ property: 'value'
+ },
+ ],
+ output: {
+ id: 'my-filter',
+ property: 'filterValue',
+
+ transform: inputArguments => inputArguments['my-input.value']
+ }
+ },
+
+ // The `filterColumnId` of the filter is the dropdown
+ {
+ inputs: [
+ {
+ id: 'filter-column-dropdown',
+ property: 'value'
+ },
+ ],
+ output: {
+ id: 'my-filter',
+ property: 'filterColumnId',
+
+ transform: inputArguments => inputArguments['filter-column-dropdown.value']
+ }
+ },
+
+ // Display the original data as a table
+ {
+ inputs: [
+ {
+ id: 'my-data',
+ property: 'columns'
+ },
+ ],
+ output: {
+ id: 'my-original-table',
+ property: 'columns',
+
+ transform: inputArguments => inputArguments['my-data.columns']
+ }
+ },
+
+ // Display the output of the filter as a table
+ {
+ inputs: [
+ {
+ id: 'my-filter',
+ property: 'filteredColumns'
+ },
+ ],
+ output: {
+ id: 'my-filtered-table',
+ property: 'columns',
+
+ transform: inputArguments => inputArguments['my-filter.filteredColumns']
+ }
+ },
+
+ // Combine the filtered data and the original data on a graph
+ {
+ inputs: [
+ {
+ id: 'my-filter',
+ property: 'filteredColumns'
+ },
+ {
+ id: 'my-data',
+ property: 'columns'
+ }
+ ],
+
+ output: {
+ id: 'my-graph',
+ property: 'figure',
+
+ transform: inputArguments => ({
+ 'layout': {'showlegend': false, 'margin': {'l': 10, 'r': 10, 't': 10}},
+ 'data': [
+ {
+ 'x': inputArguments['my-data.columns']['Column 1'],
+ 'y': inputArguments['my-data.columns']['Column 2'],
+ 'text': inputArguments['my-data.columns']['Column 3'],
+ 'mode': 'markers',
+ 'opacity': 0.2,
+ 'marker': {
+ 'size': 24
+ }
+ },
+ {
+ 'x': inputArguments['my-filter.filteredColumns']['Column 1'],
+ 'y': inputArguments['my-filter.filteredColumns']['Column 2'],
+ 'text': inputArguments['my-filter.filteredColumns']['Column 3'],
+ 'mode': 'markers',
+ 'marker': {
+ 'size': 14,
+ 'line': {
+ 'width': 0.5,
+ 'color': 'lightgrey'
+ }
+ }
+ }
+ ]
+ })
+ }
+ }
+
+];
diff --git a/src/local_examples/example_3_crossfilter.js b/src/local_examples/example_3_crossfilter.js
new file mode 100644
index 0000000..58408ee
--- /dev/null
+++ b/src/local_examples/example_3_crossfilter.js
@@ -0,0 +1,299 @@
+import R from 'ramda';
+
+import * as components from './example_components.react';
+
+export const appLayout = {
+ 'type': 'Div',
+ 'namespace': 'dash_html_components',
+ 'props': {
+ 'content': [
+
+ {
+ 'type': 'DataStore',
+ 'namespace': 'dash_functional_components',
+ 'props': {
+ 'id': 'my-data',
+ 'columns': {
+ 'Index': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ 'Column 1': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ 'Column 2': [1, 2, 2, 3, 3, 3, 4, 4, 4, 4],
+ 'Column 3': ['A', 'B', 'C', 'D', 'E',
+ 'A', 'A', 'C', 'C', 'E'],
+ 'Column 4': [2, 1, 4, 5, 1, 3, 2, 5, 6, 9]
+ }
+ }
+ },
+
+ {
+ 'type': 'Filter',
+ 'namespace': 'dash_functional_components',
+ 'props': {
+ 'id': 'filter-graph-1-vs-2',
+ 'operation': 'intersect',
+ 'filterColumnId': 'Index'
+ }
+ },
+
+ {
+ 'type': 'Filter',
+ 'namespace': 'dash_functional_components',
+ 'props': {
+ 'id': 'filter-graph-2-vs-4',
+ 'operation': 'intersect',
+ 'filterColumnId': 'Index'
+ }
+ },
+
+ {
+ 'type': 'Div',
+ 'namespace': 'dash_html_components',
+ 'props': {
+ 'content': 'Hover or drag points on either graph'
+ }
+ },
+
+ {
+ 'type': 'Div',
+ 'namespace': 'dash_html_components',
+ 'props': {
+ 'className': 'row',
+ 'content': [
+ {
+ 'type': 'Div',
+ 'namespace': 'dash_html_components',
+ 'props': {
+ 'className': 'six columns',
+ 'content': {
+ 'type': 'Graph',
+ 'namespace': 'dash_core_components',
+ 'props': {
+ 'id': 'graph-1-vs-2'
+ }
+ }
+ }
+ },
+ {
+ 'type': 'Div',
+ 'namespace': 'dash_html_components',
+ 'props': {
+ 'className': 'six columns',
+ 'content': {
+ 'type': 'Graph',
+ 'namespace': 'dash_core_components',
+ 'props': {
+ 'id': 'graph-2-vs-4'
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+
+ {
+ 'type': 'Table',
+ 'namespace': 'dash_functional_components',
+ 'props': {
+ 'id': 'table-all-data'
+ }
+ }
+
+ ]
+ }
+};
+
+
+export const mapInputsToOutputs = [
+
+ {
+ inputs: [
+ {
+ 'id': 'my-data',
+ 'property': 'columns'
+ }
+ ],
+ output: {
+ 'id': 'table-all-data',
+ 'property': 'columns',
+ 'transform': inputArguments => inputArguments['my-data.columns']
+ }
+ },
+
+ {
+ inputs: [
+ {
+ 'id': 'my-data',
+ 'property': 'columns'
+ }
+ ],
+ output: {
+ 'id': 'filter-graph-1-vs-2',
+ 'property': 'columns',
+ 'transform': inputArguments => inputArguments['my-data.columns']
+ }
+ },
+
+ {
+ inputs: [
+ {
+ 'id': 'my-data',
+ 'property': 'columns'
+ }
+ ],
+ output: {
+ 'id': 'filter-graph-2-vs-4',
+ 'property': 'columns',
+ 'transform': inputArguments => inputArguments['my-data.columns']
+ }
+ },
+
+ {
+ inputs: [
+ {
+ 'id': 'graph-1-vs-2',
+ 'property': 'hoverData',
+ },
+ {
+ 'id': 'graph-1-vs-2',
+ 'property': 'selectedData',
+ }
+ ],
+ output: {
+ 'id': 'filter-graph-1-vs-2',
+ 'property': 'filterValue',
+ 'transform': inputArguments => {
+ let hoverData = [];
+ let selectedData = [];
+ if (inputArguments['graph-1-vs-2.hoverData']) {
+ hoverData = R.pluck('customdata', inputArguments['graph-1-vs-2.hoverData']['points']);
+ }
+ if (inputArguments['graph-1-vs-2.selectedData']) {
+ selectedData = R.pluck('customdata', inputArguments['graph-1-vs-2.selectedData']['points']);
+ }
+ return R.concat(hoverData, selectedData);
+ }
+ }
+ },
+
+ {
+ inputs: [
+ {
+ 'id': 'graph-2-vs-4',
+ 'property': 'hoverData',
+ },
+ {
+ 'id': 'graph-2-vs-4',
+ 'property': 'selectedData',
+ }
+ ],
+ output: {
+ 'id': 'filter-graph-2-vs-4',
+ 'property': 'filterValue',
+ 'transform': inputArguments => {
+ let hoverData = [];
+ let selectedData = [];
+ if (inputArguments['graph-2-vs-4.hoverData']) {
+ hoverData = R.pluck('customdata', inputArguments['graph-2-vs-4.hoverData']['points']);
+ }
+ if (inputArguments['graph-2-vs-4.selectedData']) {
+ selectedData = R.pluck('customdata', inputArguments['graph-2-vs-4.selectedData']['points']);
+ }
+ return R.concat(hoverData, selectedData);
+ }
+ }
+ },
+
+ {
+ inputs: [
+ {
+ 'id': 'my-data',
+ 'property': 'columns'
+ },
+ {
+ 'id': 'filter-graph-2-vs-4',
+ 'property': 'filteredColumns'
+ }
+ ],
+ output: {
+ 'id': 'graph-1-vs-2',
+ 'property': 'figure',
+
+ 'transform': inputArguments => ({
+ 'layout': {
+ 'margin': {'l': 20, 'r': 10, 't': 10, 'b': 20},
+ 'hovermode': 'closest',
+ 'dragmode': 'select',
+ 'showlegend': false
+ },
+ 'data': [{
+ 'x': inputArguments['my-data.columns']['Column 1'],
+ 'y': inputArguments['my-data.columns']['Column 2'],
+ 'text': inputArguments['my-data.columns']['Column 3'],
+ 'customdata': inputArguments['my-data.columns']['Index'],
+ 'mode': 'markers',
+ 'opacity': 0.2,
+ 'marker': {
+ 'size': 20
+ }
+ }, {
+ 'x': inputArguments['filter-graph-2-vs-4.filteredColumns']['Column 1'],
+ 'y': inputArguments['filter-graph-2-vs-4.filteredColumns']['Column 2'],
+ 'text': inputArguments['filter-graph-2-vs-4.filteredColumns']['Column 3'],
+ 'customdata': inputArguments['filter-graph-2-vs-4.filteredColumns']['Index'],
+ 'mode': 'markers',
+ 'marker': {
+ 'size': 10
+ }
+ }]
+ })
+ }
+ },
+
+ {
+ inputs: [
+ {
+ 'id': 'my-data',
+ 'property': 'columns'
+ },
+ {
+ 'id': 'filter-graph-1-vs-2',
+ 'property': 'filteredColumns'
+ }
+
+ ],
+ output: {
+ 'id': 'graph-2-vs-4',
+ 'property': 'figure',
+
+ 'transform': inputArguments => ({
+ 'layout': {
+ 'margin': {'l': 20, 'r': 10, 't': 10, 'b': 20},
+ 'hovermode': 'closest',
+ 'dragmode': 'select',
+ 'showlegend': false
+ },
+ 'data': [{
+ 'x': inputArguments['my-data.columns']['Column 2'],
+ 'y': inputArguments['my-data.columns']['Column 4'],
+ 'text': inputArguments['my-data.columns']['Column 3'],
+ 'customdata': inputArguments['my-data.columns']['Index'],
+ 'mode': 'markers',
+ 'opacity': 0.2,
+ 'marker': {
+ 'size': 20
+ }
+ }, {
+ 'x': inputArguments['filter-graph-1-vs-2.filteredColumns']['Column 2'],
+ 'y': inputArguments['filter-graph-1-vs-2.filteredColumns']['Column 4'],
+ 'text': inputArguments['filter-graph-1-vs-2.filteredColumns']['Column 3'],
+ 'customdata': inputArguments['filter-graph-1-vs-2.filteredColumns']['Index'],
+ 'mode': 'markers',
+ 'marker': {
+ 'size': 10
+ }
+ }]
+ })
+ }
+ }
+
+]
diff --git a/src/local_examples/example_components.react.js b/src/local_examples/example_components.react.js
new file mode 100644
index 0000000..c9d8af5
--- /dev/null
+++ b/src/local_examples/example_components.react.js
@@ -0,0 +1,99 @@
+import R from 'ramda';
+import React, {PropTypes, Component} from 'react';
+
+export class Filter extends Component {
+ componentWillReceiveProps(nextProps) {
+ if (['columns', 'filterColumnId', 'filterValue', 'operation'].find(
+ prop => nextProps[prop] !== this.props[prop]
+ )) {
+ const {
+ columns, filterColumnId, operation, filterValue, setProps
+ } = nextProps;
+
+ console.warn('nextProps: ', nextProps);
+
+ let filteredColumns;
+ if (columns) {
+ filteredColumns = R.map(() => [], columns);
+ }
+
+ if (!(filterColumnId && operation && filterValue)) {
+ if (columns) {
+ setProps({filteredColumns});
+ }
+ return;
+ }
+
+ const pushRow = i => R.keys(filteredColumns).forEach(colId =>
+ filteredColumns[colId].push(columns[colId][i])
+ );
+
+ columns[filterColumnId].forEach((cell, i) => {
+ if ((operation === '<' && cell < parseFloat(filterValue, 10)) ||
+ (operation === '>' && cell > parseFloat(filterValue, 10)) ||
+ (operation === '==' && cell == filterValue) ||
+ (operation === 'intersect' && R.contains(cell, filterValue))
+ ) {
+ pushRow(i);
+ }
+ });
+ console.warn('filteredColumns: ', filteredColumns);
+ setProps({filteredColumns});
+ }
+ }
+
+ render() {
+ return null;
+ }
+}
+Filter.propTypes = {
+ /*
+ * `id` and `setProps` are required by dash
+ */
+ id: PropTypes.string.isRequired,
+ setProps: PropTypes.func.isRequired,
+
+ /*
+ * The rest of the properties are the
+ * declarative properties of the filter
+ * transform
+ */
+ columns: PropTypes.object,
+ filterColumnId: PropTypes.string,
+ operation: PropTypes.oneOf(['<', '>', '==', 'intersect']),
+ filterValue: PropTypes.oneOfType(
+ PropTypes.array,
+ PropTypes.number,
+ PropTypes.string
+ ),
+
+ /*
+ * This set of "props" are output
+ * props and are computed by the set of
+ * input props
+ */
+ filteredColumns: PropTypes.object
+}
+
+export const DataStore = props => null;
+
+export const Table = props => {
+ if (!props.columns) return null;
+ const firstColumn = props.columns[R.keys(props.columns)[0]];
+ const rows = [];
+ firstColumn.forEach((_, i) => (rows.push(
+
+ {R.values(props.columns).map(col => | {col[i]} | )}
+
+ )));
+ return (
+
+ {R.keys(props.columns).map(colId => | {colId} | )}
+ {rows}
+
+ );
+}
+
+window.dash_functional_components = {
+ DataStore, Filter, Table
+};