Skip to content
This repository was archived by the owner on Mar 5, 2020. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions src/BubbleChart/BubbleChart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { Component, PropTypes } from 'react';
import BubbleContainer from './components/BubbleContainer';
import Bubbles from './components/Bubbles';
import GroupingPicker from './components/GroupingPicker';
import CategoryTitles from './components/CategoryTitles';

import { createNodes, width, height, center, category } from './utils';

export default class BubleAreaChart extends Component {

state = {
data: [],
grouping: 'all',
}

componentWillMount() {
this.setState({
data: createNodes(this.props.data),
});
}

onGroupingChanged = (newGrouping) => {
this.setState({
grouping: newGrouping,
});
};

render() {
const { data, grouping } = this.state;
return (
<div>
<GroupingPicker onChanged={this.onGroupingChanged} active={grouping} />
<div style={{ display: 'flex', justifyContent: 'space-around', margin: 'auto' }} >
<BubbleContainer width={width} height={height}>
<Bubbles
data={data}
forceStrength={0.03}
center={center}
categoryCenters1={category[1]}
groupByCategory1={grouping === 'category1'}
categoryCenters2={category[2]}
groupByCategory2={grouping === 'category2'}
color={this.props.colors}
/>
{
grouping === 'category1' &&
<CategoryTitles width={width} categoryCenters={category[1]} />
}
{
grouping === 'category2' &&
<CategoryTitles width={width} categoryCenters={category[2]} />
}
</BubbleContainer>
</div>
</div>
);
}
}

BubleAreaChart.propTypes = {
data: PropTypes.arrayOf(PropTypes.object.isRequired),
colors: PropTypes.arrayOf(PropTypes.string.isRequired),
};
16 changes: 16 additions & 0 deletions src/BubbleChart/components/BubbleContainer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React, { PropTypes } from 'react';

const BubbleContainer = ({ width, height, children }) =>
<svg className="bubbleChart" width={width} height={height}>
{
children
}
</svg>;

BubbleContainer.propTypes = {
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
children: PropTypes.node,
};

export default BubbleContainer;
146 changes: 146 additions & 0 deletions src/BubbleChart/components/Bubbles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import React, { PropTypes } from 'react';
import * as d3 from 'd3';
import tooltip from './Tooltip';
import styles from './Tooltip.css';
import { checkProps } from '../utils';

export default class Bubbles extends React.Component {
constructor(props) {
super(props);
const { forceStrength, center } = props;
this.simulation = d3.forceSimulation()
.velocityDecay(0.2)
.force('x', d3.forceX().strength(forceStrength).x(center.x))
.force('y', d3.forceY().strength(forceStrength).y(center.y))
.force('charge', d3.forceManyBody().strength(this.charge.bind(this)))
.on('tick', this.ticked.bind(this))
.stop();
}

state = {
g: null,
}

componentWillReceiveProps(nextProps) {
if (nextProps.data !== this.props.data) {
this.renderBubbles(nextProps.data);
} else {
checkProps(nextProps, this.props, this.simulation, this.resetBubbles);
}
}

shouldComponentUpdate() {
// we will handle moving the nodes on our own with d3.js
// make React ignore this component
return false;
}

onRef = (ref) => {
this.setState({ g: d3.select(ref) }, () => this.renderBubbles(this.props.data));
}

ticked() {
this.state.g.selectAll('.bubble')
.attr('cx', d => d.x)
.attr('cy', d => d.y);
}

charge(d) {
return -this.props.forceStrength * (d.radius ** 2.0);
}

resetBubbles = () => {
const { forceStrength, center } = this.props;
this.simulation.force('x', d3.forceX().strength(forceStrength).x(center.x))
.force('y', d3.forceY().strength(forceStrength).y(center.y));
this.simulation.alpha(1).restart();
}

renderBubbles(data) {
const bubbles = this.state.g.selectAll('.bubble').data(data, d => d.id);

// Exit
bubbles.exit().remove();

// Enter
const bubblesE = bubbles.enter()
.append('circle')
.classed('bubble', true)
.attr('r', 0)
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('fill', d => d.color)
.attr('stroke', d => d3.rgb(d.color).darker())
.attr('stroke-width', 2)
.on('mouseover', showDetail) // eslint-disable-line
.on('mouseout', hideDetail) // eslint-disable-line

bubblesE.transition().duration(2000).attr('r', d => d.radius).on('end', () => {
this.simulation.nodes(data)
.alpha(1)
.restart();
});
}

render() {
return (
<g ref={this.onRef} className={styles.tooltip} />
);
}
}

Bubbles.propTypes = {
center: PropTypes.shape({
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
}),
forceStrength: PropTypes.number.isRequired,
data: PropTypes.arrayOf(PropTypes.shape({
x: PropTypes.number.isRequired,
id: PropTypes.number.isRequired,
radius: PropTypes.number.isRequired,
value: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
})),
};

/*
* Function called on mouseover to display the
* details of a bubble in the tooltip.
*/
export function showDetail(d) {
// change outline to indicate hover state.
d3.select(this).attr('stroke', 'black');

const content = `<span class=${styles.name}>Title: </span>
<span class="value">
${d.name}
</span><br/>`
+
`<span class=${styles.name}>Amount: </span>
<span class="value">
${d.value}
</span><br/>`
+
`<span class=${styles.name}>Category: </span>
<span class="value">
${d.category}
</span><br/>`
+
`<span class=${styles.name}>Spending: </span>
<span class="value">
${d.spending}
</span><br/>`;
tooltip.showTooltip(content, d3.event);
}

/*
* Hides tooltip
*/
export function hideDetail() {
// reset outline
d3.select(this)
.attr('stroke', d => d3.rgb(d.color).darker());

tooltip.hideTooltip();
}
29 changes: 29 additions & 0 deletions src/BubbleChart/components/CategoryTitles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, { PropTypes } from 'react';

const CategoryTitles = ({ categoryCenters }) =>
<g className="categoryTitles">
{
Object.keys(categoryCenters).map(category =>
<text
key={category}
x={categoryCenters[category].x}
y={50}
fontSize="35"
textAnchor="middle"
alignmentBaseline="middle"
>
{
category
}
</text>)
}
</g>;

CategoryTitles.propTypes = {
categoryCenters: PropTypes.objectOf(PropTypes.shape({
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
}).isRequired).isRequired,
};

export default CategoryTitles;
22 changes: 22 additions & 0 deletions src/BubbleChart/components/GroupingPicker.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.GroupingPicker {
display: flex;
justify-content: flex-start;
padding: 10px;
}

.button {
min-width: 130px;
padding: 4px 5px;
cursor: pointer;
text-align: center;
font-size: 13px;
background: white;
border: 1px solid #e0e0e0;
text-decoration: none;
margin: 0 5px;
}

.active {
background: black;
color: white;
}
24 changes: 24 additions & 0 deletions src/BubbleChart/components/GroupingPicker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React, { PropTypes } from 'react';
import styles from './GroupingPicker.css';

export default class GroupingPicker extends React.Component {
onBtnClick = (event) => {
this.props.onChanged(event.target.name);
}
render() {
const { active } = this.props;

return (
<div className={styles.GroupingPicker}>
<button className={`${active === 'all' && styles.active} ${styles.button}`} name="all" onClick={this.onBtnClick}>Group All</button>
<button className={`${active === 'category1' && styles.active} ${styles.button}`} name="category1" onClick={this.onBtnClick}>Group by Category</button>
<button className={`${active === 'category2' && styles.active} ${styles.button}`} name="category2" onClick={this.onBtnClick}>Group by Spending</button>
</div>
);
}
}

GroupingPicker.propTypes = {
onChanged: PropTypes.func.isRequired,
active: PropTypes.oneOf(['all', 'category1', 'category2']).isRequired,
};
21 changes: 21 additions & 0 deletions src/BubbleChart/components/Tooltip.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.tooltip {
position: absolute;
-moz-border-radius:5px;
border-radius: 5px;
border: 2px solid #000;
background: #fff;
opacity: .9;
color: black;
padding: 10px;
width: 300px;
font-size: 12px;
z-index: 10;
}

.tooltip .title {
font-size: 13px;
}

.tooltip .name {
font-weight:bold;
}
Loading