@chriddyp, @albsantosdel, @popohoma, an update:
So I’ve got my Dash wrapper around Clustergrammer working. I’m still hazy on why things didn’t work before, but I think that’s because of Clustergrammer, not Dash or webpack. Disclaimer: this pretty specific to what I was trying to do, might not help others trying for other JS packages.
Summary of issue:
- Wanted to wrap Clustergrammer in a Dash component (let’s call it “Component” from here on out)
- Used boilerplate, only changes are that I did
$ npm install clustergrammer
(which adds it to Component’s package.json
) and then added import Clustergrammer from 'clustergrammer';
to the top of the Component’s source component.react.js
file
- But this would result in
d3 not defined
errors; d3 wasn’t making it into the bundle.
Solving d3 undefined error:
Upon inspection of of Clustergrammer’s webpack.config.js, you can see that they place it under externals
which means that webpack won’t bundle d3, the app expects it to be defined and available already by some other “external” means!
Ok, great. So then I consulted Clustergrammer’s package.json to see which version of d3 it needs, and added it to Component’s package.json
(instead of doing $ npm install d3
which would have installed the latest d3 instead of the specific version the Clustergrammer expects). However, testing the Component would again say “d3 not defined”.
My guess was because when webpack starts scanning the javascript of your package from the entrypoint (component.react.js
), it never sees any d3 calls in Component; yeah there are d3 calls in Clustergrammer but remember that in Clustergrammer, d3 is external, so that doesn’t count. Thus, webpack wouldn’t bundle d3.
To force it to bundle d3 (note 1), I explicitly “touch” it by importing it via import * as d3 from "d3";
This took care of the issue, finally no more “d3 undefined” errors!
lodash/underscore undefined error (??something fishy here):
Then I was met with another “undefined” error, this time for _
, which I learned is either underscore
or lodash
, popular JS packages.
This one stumped me: if you check Clustergrammer’s webpack.config.js
, you can see that the author originally had it under externals
, but commented it out. So that means that webpack should put underscore
into the bundle, right?
But for some reason, it wasn’t in the bundle (note 2). I still don’t know why…Can someone shed light on this?
Hacking a solution for lodash/underscore
I just did the same thing as what I did for d3, and turned out that both of the below were necessary to get it working:
- Add
lodash
to package.json
- Explicitly “touch” lodash in Component’s javascript via an import at the top:
import _ from "lodash";
Clustergrammer has hard-copies of dependencies in a folder?
If you look at the Clustergrammer repo, you can see a folder lib/js. I’m super confused about the purpose of this folder:
- is it there just for the example use pages of Clustergrammer they have?
- or is it there to package these libraries with Clustergrammer for end-users?
You can see it has underscore, d3, and even jquery in there!
Note 1
Maybe I shouldn’t force it to bundle d3? It makes your bundle large. Maybe it’s best to indeed do what Clustergrammer did and have it external
and link it from a fast CDN?
Note 2
I found this tool useful for visualizing the contents of your bundle. To get the json you need to upload, I added the following to my package.json
under scripts
:
"build:js-dev-stats": "webpack --mode development --json > stats.json"
Final working code
cgrammer.react.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import * as d3 from "d3";
import _ from "lodash";
/*debugger;*/
import Clustergrammer from 'clustergrammer';
/**
* ExampleComponent is an example component.
* It takes a property, `label`, and
* displays it.
* It renders an input with the property `value`
* which is editable by the user.
*/
export default class cgrammer extends Component {
render() {
const { id, label } = this.props;
return (
<div className="theme" id={id}>
<h1>{label}</h1>
<div id='cgm-container'>
<h1 className='wait_message'>Please wait ...</h1>
</div>
</div>
);
}
componentDidMount() {
var network_data = this.props.network_data;
var args = {
'root': '#cgm-container',
'network_data': network_data
}
resize_container(args);
d3.select(window).on('resize', function () {
resize_container(args);
cgm.resize_viz();
});
// Clustergrammer returns a Clustergrammer object in addition to making
// the visualization
var cgm = Clustergrammer(args);
d3.select(cgm.params.root + ' .wait_message').remove();
}
}
cgrammer.defaultProps = {};
cgrammer.propTypes = {
/**
* The ID used to identify this component in Dash callbacks
*/
id: PropTypes.string,
/**
* A label that will be printed when this component is rendered.
*/
label: PropTypes.string.isRequired,
/**
* The JSON data that Clustergrammer takes in as input
*/
network_data: PropTypes.object.isRequired,
/**
* Dash-assigned callback that should be called whenever any of the
* properties change
*/
setProps: PropTypes.func
};
function resize_container(args) {
var screen_width = window.innerWidth;
var screen_height = window.innerHeight - 20;
d3.select(args.root)
.style('width', screen_width + 'px')
.style('height', screen_height + 'px');
}
usage.py
import cgrammer
import dash
from dash.dependencies import Input, Output
import dash_html_components as html
import json
external_scripts = [
{'src': 'https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js'},
{'src': 'https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js'}
]
app = dash.Dash(__name__,
external_scripts=external_scripts)
app.scripts.config.serve_locally = True
app.css.config.serve_locally = True
print('loading JSON Clustergrammer data...')
with open('mult_view.json', 'r') as f:
network_data = json.load(f)
print('done')
app.layout = html.Div([
cgrammer.cgrammer(
id='cgram-component',
label='Clustergrammer Dash Component',
network_data=network_data,
)
])
if __name__ == '__main__':
app.run_server(debug=True)
package.json
{
"name": "cgrammer",
"version": "0.0.1",
"description": "Project Description",
"main": "build/index.js",
"scripts": {
"start": "webpack-serve ./webpack.serve.config.js --open",
"validate-init": "python _validate_init.py",
"prepublish": "npm run validate-init",
"build:js-dev": "webpack --mode development",
"build:js-dev-stats": "webpack --mode development --json > stats.json",
"build:js": "webpack --mode production",
"build:py": "dash-generate-components ./src/lib/components cgrammer",
"build:py-activated": "(. venv/bin/activate || venv\\scripts\\activate && npm run build:py)",
"build:all": "npm run build:js && npm run build:js-dev && npm run build:py",
"build:all-activated": "(. venv/bin/activate || venv\\scripts\\activate && npm run build:all)"
},
"author": "Amir Alavi s.amir.alavi@gmail.com",
"license": "GPL-3.0",
"dependencies": {
"clustergrammer": "^1.19.5",
"ramda": "^0.25.0",
"d3": "^3.5.15",
"lodash": "^4.17.11"
},
"devDependencies": {
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.3",
"babel-loader": "^7.1.4",
"copyfiles": "^2.0.0",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"css-loader": "^0.28.11",
"eslint": "^4.19.1",
"eslint-config-prettier": "^2.9.0",
"eslint-plugin-import": "^2.12.0",
"eslint-plugin-react": "^7.9.1",
"npm": "^6.1.0",
"react-docgen": "^2.20.1",
"style-loader": "^0.21.0",
"webpack": "^4.20.2",
"webpack-cli": "^3.1.1",
"webpack-serve": "^1.0.2",
"react": ">=0.14",
"react-dom": ">=0.14"
},
"engines": {
"node": ">=8.11.0",
"npm": ">=6.1.0"
}
}