Skip to content
This repository has been archived by the owner on Mar 5, 2020. It is now read-only.

Bubbles2 #73

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
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
38 changes: 38 additions & 0 deletions src/BubbleChart/BubbleChart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, { Component, PropTypes } from 'react';
import Bubbles from './Bubbles';

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

export default class BubleAreaChart extends Component {

state = {
data: [],
}

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

render() {
const { data } = this.state;
return (
<div style={{ display: 'flex', justifyContent: 'space-around', margin: 'auto' }} >
<svg className="bubbleChart" width={width} height={height}>
<Bubbles
data={data}
forceStrength={0.03}
center={center}
color={this.props.colors}
/>
</svg>
</div>
);
}
}

BubleAreaChart.propTypes = {
data: PropTypes.arrayOf(PropTypes.object.isRequired),
colors: PropTypes.arrayOf(PropTypes.string.isRequired),
};
125 changes: 125 additions & 0 deletions src/BubbleChart/Bubbles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import React, { PropTypes } from 'react';
import * as d3 from 'd3';
import tooltip from './Tooltip';
import styles from './Tooltip.css';

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,
}

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);
}

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}>Percentage: </span>
<span class="value">
${d.percentage}
</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();
}
21 changes: 21 additions & 0 deletions src/BubbleChart/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;
}
87 changes: 87 additions & 0 deletions src/BubbleChart/Tooltip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* eslint-disable */
import * as d3 from 'd3';
import styles from './Tooltip.css';

/*
* Creates tooltip with provided id that
* floats on top of visualization.
* Most styling is expected to come from CSS
* so check out bubble_chart.css for more details.
*/
const floatingTooltip = (tooltipId, width) => {

// Local variable to hold tooltip div for
// manipulation in other functions.
const tt = d3.select('body')
.append('div')
.attr('class', styles.tooltip)
.attr('id', tooltipId)
.style('pointer-events', 'none');

/*
* Display tooltip with provided content.
* content is expected to be HTML string.
* event is d3.event for positioning.
*/
const showTooltip = (content, event) => {
tt.style('opacity', 1.0)
.html(content);

updatePosition(event);
}

/*
* Hide the tooltip div.
*/
const hideTooltip = () => {
tt.style('opacity', 0.0);
}

/*
* Figure out where to place the tooltip
* based on d3 mouse event.
*/
const updatePosition = (event) => {
const xOffset = 20;
const yOffset = 10;

const ttw = tt.style('width');
const tth = tt.style('height');

var wscrY = window.scrollY;
var wscrX = window.scrollX;

const curX = (document.all) ? event.clientX + wscrX : event.pageX;
const curY = (document.all) ? event.clientY + wscrY : event.pageY;

let ttleft = ((curX - wscrX + xOffset * 2 + ttw) > window.innerWidth) ?
curX - ttw - xOffset * 2 : curX + xOffset;

if (ttleft < wscrX + xOffset) {
ttleft = wscrX + xOffset;
}

let tttop = ((curY - wscrY + yOffset * 2 + tth) > window.innerHeight) ?
curY - tth - yOffset * 2 : curY + yOffset;

if (tttop < wscrY + yOffset) {
tttop = curY + yOffset;
}

tt
.style('top', '100px')
.style('left', '100px')
}

// Initially it is hidden.
hideTooltip();

return {
showTooltip,
hideTooltip,
updatePosition,
};
};

const tooltip = floatingTooltip('myTooltip', 240);
export default tooltip;
37 changes: 37 additions & 0 deletions src/BubbleChart/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as d3 from 'd3';

// constants
export const width = 960;
export const height = 640;
export const center = { x: width / 2, y: height / 2 };

// create nodes
export const createNodes = (rawData) => {
// Use the max total_amount in the data as the max in the scale's domain
// note we have to ensure the total_amount is a number.
const maxAmount = d3.max(rawData, d => +d.total_amount);

// Sizes bubbles based on area.
// @v4: new flattened scale names.
const radiusScale = d3.scalePow()
.exponent(0.5)
.range([2, 85])
.domain([0, maxAmount]);

// Use map() to convert raw data into node data.
const myNodes = rawData.map(d => ({
id: d.id,
radius: radiusScale(+d.total_amount),
value: +d.total_amount,
percentage: d.percentage,
name: d.bureau_title,
color: d.color,
x: Math.random() * 900,
y: Math.random() * 800,
}));

// sort them descending to prevent occlusion of smaller nodes.
myNodes.sort((a, b) => b.value - a.value);

return myNodes;
};
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export AreaChart from './AreaChart/AreaChart';
export BarChart from './BarChart/BarChart';
export BubbleChart from './BubbleChart/BubbleChart';
export Sankey from './Sankey/Sankey';
export Button from './Button/Button';
export StoryCard from './StoryCard/StoryCard';
Expand Down
Loading