Skip to content

Instantly share code, notes, and snippets.

@rturk
Forked from gilbarbara/react-d3.jsx
Created October 5, 2016 00:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rturk/5c5fa6bcad119f773ac8658494ac093e to your computer and use it in GitHub Desktop.
Save rturk/5c5fa6bcad119f773ac8658494ac093e to your computer and use it in GitHub Desktop.
React + D3 example
import React from 'react';
import { autobind } from 'core-decorators';
import d3 from 'd3';
import moment from 'moment';
import classNames from 'classnames';
import { getFirstDate } from 'utils/Consolidator';
import { shouldComponentUpdate } from 'utils/shouldUpdate';
import { classes, sort } from 'utils/Presentation';
import Money from 'utils/Money';
import Loader from '../components/Loader';
export default class EvolutionChart extends React.Component {
constructor(props) {
super(props);
this.months = 0;
this.state = {
exclude: [],
ready: false
};
}
static propTypes = {
investment: React.PropTypes.object.isRequired,
size: React.PropTypes.array,
wallet: React.PropTypes.object.isRequired
};
static defaultProps = {
size: [0, 0]
};
shouldComponentUpdate = shouldComponentUpdate;
componentDidMount() {
if (this.props.investment.position.done) {
this.prepareData();
}
window.addEventListener('resize', this.prepareSize);
window.addEventListener('scroll', this.onScroll);
}
componentDidUpdate(prevProps) {
if (this.props.wallet.requests.data.length && (!prevProps.investment.position.done && this.props.investment.position.done)) {
this.prepareData();
}
if (this.state.ready) {
this.plotChart();
}
}
componentWillUnmount() {
window.removeEventListener('resize', this.prepareSize);
window.removeEventListener('resize', this.onScroll);
}
@autobind
onScroll() {
d3.select('.evolution-chart__hover').classed('active', false);
d3.select('.evolution-chart__tooltip').classed('in', false);
}
@autobind
onClickLegend(e) {
e.preventDefault();
const $el = $(e.currentTarget);
let exclude = [...this.state.exclude];
if ($('.evolution-chart__legend a.active').length === 1 && $el.hasClass('active')) {
return;
}
if ($el.hasClass('active') && !exclude.includes($el.data('class'))) {
exclude.push($el.data('class'));
}
else {
exclude = exclude.filter(d => d !== $el.data('class'));
}
this.setState({ exclude });
}
prepareData() {
const investment = this.props.investment;
const wallet = this.props.wallet;
const assetsByClass = {};
const assets = [];
const firstDate = moment(getFirstDate(wallet.requests.data)).utc();
const lastDate = moment(investment.position.data[investment.position.data.length - 1].date).utc();
Object.keys(investment.assets.data).forEach(d => {
const asset = investment.assets.data[d];
if (!assetsByClass[asset.marketingType]) {
assetsByClass[asset.marketingType] = {};
}
});
assetsByClass.cash = {};
investment.position.data.forEach(d => {
Object.keys(assetsByClass).forEach(a => {
assetsByClass[a][d.date] = 0;
});
assetsByClass.cash[d.date] += d.cash;
Object.keys(d.positions).forEach(e => {
const asset = d.positions[e];
assetsByClass[investment.assets.data[e].marketingType][d.date] += asset.value;
});
});
this.interval = d3.time.months;
this.difference = lastDate.diff(firstDate, 'months', true);
if (this.difference < 0.5) {
this.interval = d3.time.days;
}
else if (this.difference < 2) {
this.interval = d3.time.weeks;
}
this.xTicks = this.interval(firstDate.toDate(), lastDate.toDate()).length;
this.xClass = '';
if (this.xTicks < 7) {
this.xClass = 'small';
}
else if (this.xTicks > 16) {
this.xClass = 'large';
}
Object.keys(assetsByClass).forEach(d => {
const marketingType = assetsByClass[d];
const typeData = [];
investment.position.data.forEach(p => {
if (moment(p.date).utc().isSameOrAfter(firstDate)) {
typeData.push({ date: moment(p.date).utc().format(), value: marketingType[p.date] || 0 });
}
});
assets.push({
name: d,
values: typeData
});
});
this.setState({
data: sort(assets, 'name')
}, this.prepareSize);
}
filterData() {
const state = this.state;
return state.data.filter(d => !state.exclude.includes(d.name));
}
@autobind
prepareSize(e) {
const props = this.props;
this.width = props.size[0] || this.container.getBoundingClientRect().width;
this.height = props.size[1] || this.container.getBoundingClientRect().height;
this.margin = { top: 15, right: 20, bottom: 30, left: 100 };
if (window.innerWidth < 600) {
this.margin.left = 55;
}
this.innerWidth = this.width - this.margin.left - this.margin.right;
this.innerHeight = this.height - this.margin.top - this.margin.bottom;
this.x = d3.time.scale()
.range([0, this.innerWidth]);
this.y = d3.scale.linear()
.range([this.innerHeight, 0]);
this.stack = d3.layout.stack()
.values(d => d.values);
this.colors = d3.scale.ordinal()
.range(this.filterData().map(d => classes[d.name].color));
this.xAxis = d3.svg.axis()
.scale(this.x)
.orient('bottom')
.ticks(this.interval)
.tickSize(-(this.height - this.margin.bottom))
.tickPadding(15)
.tickFormat(d => moment(d, 'YYYY-MM').utc().format(this.difference < 0.5 ? 'DD/MM' : 'MM/YY'));
this.yAxis = d3.svg.axis()
.scale(this.y)
.orient('left')
.tickSize(-this.innerWidth)
.tickPadding(window.innerWidth < 600 ? 5 : 10)
.tickFormat(d => {
let money = Money.shorten(d);
if (window.innerWidth < 600) {
money = money.replace(/ mil(hão|hões)/, 'M').replace(' mil', 'k');
}
return `R$ ${money}`;
});
this.area = d3.svg.area()
// .interpolate('basis')
.x(d => this.x(d.date))
.y0(d => this.y(d.y0))
.y1(d => this.y(d.y0 + d.y));
this.bisectDate = d3.bisector(d => d.date).left;
if (e && e.type === 'resize') {
this.forceUpdate();
d3.select('.evolution-chart__hover').classed('active', false);
d3.select('.evolution-chart__tooltip').classed('in', false);
}
if (!this.state.ready) {
this.setState({
ready: true
});
}
}
tooltipData(data, index) {
const dailies = [];
const tooltip = d3.select('.evolution-chart__tooltip');
const tooltipInner = tooltip.select('.tooltip-inner');
data.forEach((d, i) => {
if (d.values[index].y > 0) {
dailies.push({
name: d.name,
amount: d.values[index].y
});
}
d3.select(`.hover__circle:nth-of-type(${i + 1})`)
.attr('transform', `translate(${this.x(d.values[index].date)},${this.y(d.values[index].y + d.values[index].y0)})`);
});
dailies.push({
title: 'Total',
amount: dailies.reduce((prev, curr) => prev + curr.amount, 0)
});
d3.select('.hover__line').attr({
x1: this.x(data[0].values[index].date),
x2: this.x(data[0].values[index].date)
});
const $tooltip = $('.evolution-chart__tooltip');
const offset = $(this.container).offset();
let top = (this.innerHeight - $tooltip.outerHeight()) / 2;
let left = (this.x(data[0].values[index].date) + this.margin.left + 15);
if (left + offset.left + $tooltip.outerWidth() + 30 > window.innerWidth) {
left -= $tooltip.outerWidth() + 30;
}
if (window.innerWidth < 600) {
top = (this.height - this.margin.bottom) + 10;
left = (window.innerWidth - $tooltip.outerWidth()) / 2;
}
tooltip
.style({
left: `${left}px`,
top: `${top}px`,
});
tooltipInner
.select('h3').text(moment(data[0].values[index].date).utc().format('DD/MM/YY'));
const tooltipSelection = tooltipInner
.selectAll('.tooltip-asset')
.data(dailies);
tooltipSelection.enter()
.append('div')
.attr('class', 'tooltip-asset');
tooltipSelection.html(d => `
<div>
<i style="background-color: ${d.name ? classes[d.name].color : '#fff'}"></i>
<span>${d.name ? classes[d.name].name : d.title}</span>
</div>
<div>${Money.format(d.amount)}</div>
`);
tooltipSelection.exit().remove();
d3.select('.evolution-chart__tooltip').classed('in', true);
d3.select('.evolution-chart__hover').classed('active', true);
}
plotChart() {
d3.select(this.container)
.datum(this.filterData())
.call(this.update());
}
update() {
const chart = (selection) => {
selection.each(data => {
const self = this;
const assetsByClass = this.stack(data.map(d => ({
name: d.name,
values: d.values.map(v => ({
date: new Date(v.date), y: v.value * 1
}))
})));
this.colors
.range(this.filterData().map(d => classes[d.name].color))
.domain(data.map(d => d.name));
this.x.domain(d3.extent(assetsByClass[0].values, d => d.date));
this.y.domain([0, d3.sum(assetsByClass, d => d3.max(d.values.map(dd => (dd.y || 0))))]);
const svg = d3.select(this.container).selectAll('svg').data([assetsByClass]);
const svgEnter = svg.enter().append('svg');
const gEnter = svgEnter.append('g');
gEnter.append('g').attr('class', `axis x ${this.xClass}`);
gEnter.append('g').attr('class', 'axis y');
gEnter.append('line').attr('class', 'y__line start');
gEnter.append('line').attr('class', 'y__line end');
gEnter.append('g').attr('class', 'assets');
const hover = gEnter.append('g').attr('class', 'evolution-chart__hover');
hover.append('line').attr('class', 'hover__line');
gEnter.append('rect').attr('class', 'evolution-chart__overlay');
// Update the outer dimensions.
svg.attr('width', this.width)
.attr('height', this.height);
// Update the inner dimensions.
const g = svg.select('g')
.attr('transform', `translate(${this.margin.left}, ${this.margin.top})`);
const areas = g.select('.assets').selectAll('.area').data(d => d);
areas.enter()
.append('path')
.attr('class', 'area');
areas.exit().remove();
const circles = g.select('.evolution-chart__hover').selectAll('.hover__circle').data(d => d);
circles.enter()
.append('circle')
.attr('class', 'hover__circle')
.attr('r', 4);
circles.exit().remove();
g.selectAll('.area')
.attr('d', d => this.area(d.values))
.style('fill', d => this.colors(d.name));
g.select('.axis.x')
.attr('transform', `translate(0, ${this.innerHeight})`)
.call(this.xAxis);
g.select('.axis.y')
.call(this.yAxis);
g.selectAll('.y__line.start')
.attr({
x1: 0,
y1: -this.margin.top,
x2: 0,
y2: this.innerHeight
});
g.selectAll('.y__line.end')
.attr({
x1: this.innerWidth,
y1: -this.margin.top,
x2: this.innerWidth,
y2: this.innerHeight
});
g.selectAll('.hover__line')
.attr({
x1: 0,
y1: -this.margin.top,
x2: 0,
y2: this.innerHeight
});
function mouseMove() { //eslint-disable-line
const asset = assetsByClass[0].values;
const x0 = self.x.invert(d3.mouse(this)[0]);
const i = self.bisectDate(asset, x0, 1);
const d0 = asset[i - 1];
const d1 = asset[i];
if (d0 && d1) {
const index = (i === asset.length - 1 || (x0 - d0[0] > d1[0] - x0)) ? i : i - 1;
self.tooltipData(assetsByClass, index);
}
}
g.selectAll('.evolution-chart__overlay')
.attr('width', this.innerWidth)
.attr('height', this.innerHeight)
.style('fill', 'none')
.on('mouseenter', () => {
d3.select('.evolution-chart__hover').classed('active', true);
d3.select('.evolution-chart__tooltip').classed('in', true);
})
.on('mouseleave', () => {
d3.select('.evolution-chart__hover').classed('active', false);
d3.select('.evolution-chart__tooltip').classed('in', false);
})
.on('touchmove', mouseMove)
.on('mousemove', mouseMove);
});
};
chart.margin = arg => {
if (!arg) {
return this.margin;
}
this.margin = arg;
return chart;
};
chart.width = arg => {
if (!arg) {
return this.width;
}
this.width = arg;
return chart;
};
chart.height = arg => {
if (!arg) {
return this.height;
}
this.height = arg;
return chart;
};
chart.x = arg => {
if (!arg) {
return this.xValue;
}
this.xValue = arg;
return chart;
};
chart.y = arg => {
if (!arg) {
return this.yValue;
}
this.yValue = arg;
return chart;
};
return chart;
}
render() {
const state = this.state;
const output = {};
if (state.ready) {
output.legend = (
<div className="evolution-chart__legend">
{state.data.map((d, i) =>
(<a
key={i}
href="#exclude"
className={classNames(d.name, { active: !state.exclude.includes(d.name) })}
data-class={d.name}
onClick={this.onClickLegend}>
<span className="rect" style={{ backgroundColor: classes[d.name].color, color: classes[d.name].color }} />
<span className="text">
{classes[d.name].name}
</span>
</a>)
)}
</div>
);
output.tooltip = (
<div
className="evolution-chart__tooltip tooltip"
ref={c => (this.tooltip = c)}
role="tooltip">
<div className="tooltip-arrow" />
<div className="tooltip-inner">
<h3>Date</h3>
</div>
</div>
);
}
else {
output.main = (<Loader chart={true} />);
}
return (
<div key="EvolutionChart" className="evolution-chart__wrapper">
<div className="evolution-chart" ref={c => (this.container = c)}>
{output.main}
</div>
{output.legend}
{output.tooltip}
</div>
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment