Skip to content

Instantly share code, notes, and snippets.

@jackfiallos
Last active August 20, 2018 20:48
Show Gist options
  • Save jackfiallos/aa32c9d30e5ec8f572cf35e8049ed965 to your computer and use it in GitHub Desktop.
Save jackfiallos/aa32c9d30e5ec8f572cf35e8049ed965 to your computer and use it in GitHub Desktop.
React + D3 Bubble Chart
<div id="charts"></div>
class BubbleChart extends React.Component {
static propTypes = {
data: React.PropTypes.array,
width: React.PropTypes.number,
height: React.PropTypes.number,
useLabels: React.PropTypes.bool
};
static defaultProps = {
data: [],
useLabels: false,
width: 600,
height: 400
};
constructor(props) {
super(props);
this.minValue = 1;
this.maxValue = 100;
this.mounted = false;
this.state = {
data: []
};
this.radiusScale = this.radiusScale.bind(this);
this.simulatePositions = this.simulatePositions.bind(this);
this.renderBubbles = this.renderBubbles.bind(this);
}
componentWillMount() {
this.mounted = true;
}
componentDidMount() {
if (this.props.data.length > 0) {
this.minValue =
0.95 *
d3.min(this.props.data, item => {
return item.v;
});
this.maxValue =
1.05 *
d3.max(this.props.data, item => {
return item.v;
});
this.simulatePositions(this.props.data);
}
}
componentWillUnmount() {
this.mounted = false;
}
radiusScale = value => {
const fx = d3
.scaleSqrt()
.range([1, 50])
.domain([this.minValue, this.maxValue]);
return fx(value);
};
simulatePositions = data => {
this.simulation = d3
.forceSimulation()
.nodes(data)
.velocityDecay(0.5)
.force("x", d3.forceX().strength(0.05))
.force("y", d3.forceY().strength(0.05))
.force(
"collide",
d3.forceCollide(d => {
return this.radiusScale(d.v) + 2;
})
)
.on("tick", () => {
if (this.mounted) {
this.setState({ data });
}
});
};
renderBubbles = data => {
const minValue =
0.95 *
d3.min(data, item => {
return item.v;
});
const maxValue =
1.05 *
d3.max(data, item => {
return item.v;
});
const color = d3
.scaleLinear()
.domain([minValue, maxValue])
.interpolate(d3.interpolateHcl)
.range(["#eb001b", "#f79e1b"]);
// render simple circle element
if (!this.props.useLabels) {
const circles = _.map(data, (item, index) => {
return (
<circle
key={index}
r={this.radiusScale(item.v)}
cx={item.x}
cy={item.y}
fill={color(item.v)}
stroke={d3.rgb(color(item.v)).brighter(2)}
strokeWidth="2"
/>
);
});
return (
<g
transform={`translate(${this.props.width / 2}, ${this.props
.height / 2})`}
>
{circles}
</g>
);
}
// render circle and text elements inside a group
const texts = _.map(data, (item, index) => {
const props = this.props;
const fontSize = this.radiusScale(item.v) / 2;
return (
<g
key={index}
transform={`translate(${props.width / 2 +
item.x}, ${props.height / 2 + item.y})`}
>
<circle
r={this.radiusScale(item.v)}
fill={color(item.v)}
stroke={d3.rgb(color(item.v)).brighter(2)}
strokeWidth="2"
/>
<text
dy="6"
fill="#fff"
textAnchor="middle"
fontSize={`${fontSize}px`}
fontWeight="bold"
>
{item.v}
</text>
</g>
);
});
return texts;
};
render() {
if (this.state.data.length) {
return (
<svg width={this.props.width} height={this.props.height}>
{this.renderBubbles(this.state.data)}
</svg>
);
}
return <div>Loading</div>;
}
}
const rawdata = _.map(_.range(24), () => {
return {
v: _.random(10, 100)
};
});
ReactDOM.render(
<BubbleChart useLabels data={rawdata} />,
document.getElementById("charts")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.9.1/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react-dom.min.js"></script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment