Skip to content

Instantly share code, notes, and snippets.

@mukhtyar
Last active June 1, 2018 19:45
Show Gist options
  • Save mukhtyar/9767e71195e624a8ee559a9556b1de89 to your computer and use it in GitHub Desktop.
Save mukhtyar/9767e71195e624a8ee559a9556b1de89 to your computer and use it in GitHub Desktop.
fresh block
license: mit
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
</style>
</head>
<body>
<div id="vis-container"></div>
<script>
// generate random data
const data = d3.range(50).map((d, i) => ({
x: Math.random(),
y: Math.random(),
id: i,
label: `Point ${i}`,
}));
const data2 = [
{
"year": 1945,
"count": 5
},
{
name: 'HadGEM2-ES',
year: 1945,
count: 2
},
{
name: 'HadGEM2-ES',
year: 1946,
count: 4
},
{
name: 'HadGEM2-ES',
year: 1947,
count: 4
},
{
name: 'Observed',
year: 1945,
count: 1
},
{
name: 'Observed',
year: 1946,
count: 2,
},
{
name: 'Observed',
year: 1947,
count: 3
}
]
// ----------------------------------------------------
// Build a basic scatterplot
// ----------------------------------------------------
// outer svg dimensions
const width = 600;
const height = 400;
// padding around the chart where axes will go
const padding = {
top: 20,
right: 20,
bottom: 40,
left: 50,
};
// inner chart dimensions, where the dots are plotted
const plotAreaWidth = width - padding.left - padding.right;
const plotAreaHeight = height - padding.top - padding.bottom;
// radius of points in the scatterplot
const pointRadius = 3;
// initialize scales
const xScale = d3.scaleTime().domain([new Date(1944, 0, 1), new Date(1948, 11, 31)]).range([0, plotAreaWidth]);
const yScale = d3.scaleLinear().domain([1, 5]).range([plotAreaHeight, 0]);
const colorScale = d3.scaleOrdinal().domain(['HadGEM2-ES', 'Observed']).range(['red', 'orange']);
// select the root container where the chart will be added
const container = d3.select('#vis-container');
// initialize main SVG
const svg = container.append('svg')
.attr('width', width)
.attr('height', height);
// the main <g> where all the chart content goes inside
const g = svg.append('g')
.attr('transform', `translate(${padding.left} ${padding.top})`);
// add in axis groups
const xAxisG = g.append('g').classed('x-axis', true)
.attr('transform', `translate(0 ${plotAreaHeight + pointRadius})`);
// x-axis label
g.append('text')
.attr('transform', `translate(${plotAreaWidth / 2} ${plotAreaHeight + (padding.bottom)})`)
.attr('dy', -4) // adjust distance from the bottom edge
.attr('class', 'axis-label')
.attr('text-anchor', 'middle')
.text('X Axis');
const yAxisG = g.append('g').classed('y-axis', true)
.attr('transform', `translate(${-pointRadius} 0)`);
// y-axis label
g.append('text')
.attr('transform', `rotate(270) translate(${-plotAreaHeight / 2} ${-padding.left})`)
.attr('dy', 12) // adjust distance from the left edge
.attr('class', 'axis-label')
.attr('text-anchor', 'middle')
.text('Y Axis');
// set up axis generating functions
const xTicks = Math.round(plotAreaWidth / 50);
const yTicks = Math.round(plotAreaHeight / 50);
const xAxis = d3.axisBottom(xScale)
.ticks(xTicks)
.tickSizeOuter(0);
const yAxis = d3.axisLeft(yScale)
.ticks(yTicks)
.tickSizeOuter(0);
// draw the axes
yAxisG.call(yAxis);
xAxisG.call(xAxis);
// add in circles
const circles = g.append('g').attr('class', 'circles');
const binding = circles.selectAll('.data-point').data(data2, d => d.name);
binding.enter().append('circle')
.classed('data-point', true)
.attr('r', pointRadius)
.attr('cx', d => xScale(new Date(d.year, 0, 1)))
.attr('cy', d => yScale(d.count))
.attr('fill', d => colorScale(d.name));
// ----------------------------------------------------
// Add in Voronoi interaction
// ----------------------------------------------------
// add in interaction via voronoi
// initialize text output for highlighted points
const highlightOutput = container.append('div')
.attr('class', 'highlight-output')
.style('padding-left', `${padding.left}px`)
.style('min-height', '100px');
// create a voronoi diagram based on the data and the scales
const voronoiDiagram = d3.voronoi()
.x(d => xScale(new Date(d.year, 0, 1)))
.y(d => yScale(d.count))
.size([plotAreaWidth, plotAreaHeight])(data2);
// limit how far away the mouse can be from finding a voronoi site
const voronoiRadius = plotAreaWidth / 10;
// add a circle for indicating the highlighted point
g.append('circle')
.attr('class', 'highlight-circle')
.attr('r', pointRadius + 2) // slightly larger than our points
.style('fill', 'none')
.style('display', 'none');
// callback to highlight a point
function highlight(d) {
// no point to highlight - hide the circle and clear the text
if (!d) {
d3.select('.highlight-circle').style('display', 'none');
highlightOutput.text('');
// otherwise, show the highlight circle at the correct position
} else {
d3.select('.highlight-circle')
.style('display', '')
.style('stroke', colorScale(d.y))
.attr('cx', xScale(new Date(d.year, 0, 1)))
.attr('cy', yScale(d.count));
// format the highlighted data point for inspection
highlightOutput.html(JSON.stringify(d)
.replace(/([{}])/g, '')
.replace(/"(.+?)":/g, '<strong style="width: 40px; display: inline-block">$1:</strong> ')
.replace(/,/g, '<br>'));
}
}
// callback for when the mouse moves across the overlay
function mouseMoveHandler() {
// get the current mouse position
const [mx, my] = d3.mouse(this);
// use the new diagram.find() function to find the voronoi site closest to
// the mouse, limited by max distance defined by voronoiRadius
const site = voronoiDiagram.find(mx, my, voronoiRadius);
// highlight the point if we found one, otherwise hide the highlight circle
highlight(site && site.data);
}
// add the overlay on top of everything to take the mouse events
g.append('rect')
.attr('class', 'overlay')
.attr('width', plotAreaWidth)
.attr('height', plotAreaHeight)
.style('fill', 'red')
.style('opacity', 0)
.on('mousemove', mouseMoveHandler)
.on('mouseleave', () => {
// hide the highlight circle when the mouse leaves the chart
highlight(null);
});
// ----------------------------------------------------
// Add a fun click handler to reveal the details of what is happening
// ----------------------------------------------------
/**
* Add/remove a visible voronoi diagram and a circle indicating the radius used
* in the voronoi find function
*/
function toggleVoronoiDebug() {
// remove if there
if (!g.select('.voronoi-polygons').empty()) {
g.select('.voronoi-polygons').remove();
g.select('.voronoi-radius-circle').remove();
g.select('.overlay').on('mousemove.voronoi', null).on('mouseleave.voronoi', null);
// otherwise, add the polygons in
} else {
// add a circle to follow the mouse to draw the voronoi radius
g.append('circle')
.attr('class', 'voronoi-radius-circle')
.attr('r', voronoiRadius)
.style('fill', 'none')
.style('stroke', 'tomato')
.style('stroke-dasharray', '3,2')
.style('display', 'none');
// move the voronoi radius mouse circle with the mouse
g.select('.overlay')
.on('mousemove.voronoi', function mouseMoveVoronoiHandler() {
const [mx, my] = d3.mouse(this);
d3.select('.voronoi-radius-circle')
.style('display', '')
.attr('cx', mx)
.attr('cy', my);
})
.on('mouseleave.voronoi', () => {
d3.select('.voronoi-radius-circle').style('display', 'none');
});
// draw the polygons
const voronoiPolygons = g.append('g')
.attr('class', 'voronoi-polygons')
.style('pointer-events', 'none');
const binding = voronoiPolygons.selectAll('path').data(voronoiDiagram.polygons());
binding.enter().append('path')
.style('stroke', 'tomato')
.style('fill', 'none')
.style('opacity', 0.15)
.attr('d', d => `M${d.join('L')}Z`);
}
}
// turn on and off voronoi debugging with click
svg.on('click', toggleVoronoiDebug);
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment