Published
Edited
Oct 27, 2020
1 fork
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const height = 500;
const p_h = 50;
const width = 800;
const p_w = 100;
const svg = d3.select(DOM.svg(width, height));
const n_bins = 20;
const r = 3; //set radius

//////get x and y metrics data via input, combine it into single list of points
//x data and scale
let x_data = input_to_data.filter(function(d) { return d['input'] == x_metric})[0]['data']
const x_max = d3.max(x_data)
const x_min = d3.min(x_data)
//y data and scale
let y_data = input_to_data.filter(function(d) { return d['input'] == y_metric})[0]['data']
//handle one-metric case
if (y_metric == "Number of stories") {
const new_x_data = []
const new_y_data = []
//interpolate the x-metric range
let x_interpolated = d3.interpolate(x_min, x_max)
//calculate left and right bound of histogram bins
const bin_bounds = d3.range(n_bins).map(b => [x_interpolated(b/n_bins), x_interpolated((b+1)/n_bins)])
for (let i=0; i<n_bins; i++) {
//count all x data points within the bin
let this_bin = bin_bounds[i]
let this_bin_size = x_data.filter(function(d) {
return d >= this_bin[0] && d <= this_bin[1] }).length
//locate mid-point of bin
let this_bin_point = d3.interpolate(this_bin[0], this_bin[1])(0.5)
//push points into x and y datasets
d3.range(this_bin_size).map(p => new_x_data.push(this_bin_point))
d3.range(this_bin_size).map(p => new_y_data.push(r*p))
}
x_data = new_x_data;
y_data = new_y_data;
}
//combined data points
let combined_data = d3.range(n_points).map(j => [x_data[j], y_data[j]])
/////scales
//x-scale w/already calculated min and max
var x_scale = d3.scaleLinear()
.domain([x_min, x_max])
.range([p_w, width-p_w]);
//y-scale
const y_max = d3.max(y_data)
const y_min = d3.min(y_data)
var y_scale = d3.scaleLinear()
.domain([y_min, y_max])
.range([height-p_h, p_h]);
////////////axes
////y-axis
var y_axis = d3.axisLeft(y_scale)
.ticks(5);
svg.append('g')
.attr('class', 'y-axis')
.call(y_axis)
.attr('transform', 'translate(' + p_w + ',0)');
//add y-axis label
svg.append("text")
.attr("class", "axis-label")
.attr("transform", "rotate(-90)")
.attr("y", p_w-30)
.attr("x", 0 - (height / 2))
.text("Y: " + y_metric);
////x-axis
var x_axis = d3.axisBottom(x_scale)
.ticks(5);
svg.append('g')
.attr('class', 'x-axis')
.call(x_axis)
.attr('transform', 'translate(0,' + (height-p_h) +')');
//add y-axis label
svg.append("text")
.attr("class", "axis-label")
.attr("y", height - (p_h - 40))
.attr("x", width/2)
.text("X: " + x_metric);
//draw scatterplot points
svg.selectAll('circle')
.data(combined_data)
.join('circle')
.attr('class', 'point')
.attr('cx', (d) => x_scale(d[0]))
.attr('cy', (d) => y_scale(d[1]))
.attr('r', r)
.style('fill', "blue")
.style('opacity', 0.5);
svg.selectAll('.circle-stroke')
.data(combined_data)
.join('circle')
.attr('class', 'point')
.attr('cx', (d) => x_scale(d[0]))
.attr('cy', (d) => y_scale(d[1]))
.attr('r', r)
.style('fill', "none")
.style('stroke', 'purple');
///////////interactivity - tooltips on hover
//create tooltip
svg.append('rect') //questionable rectangle background because I don't understand observable
.attr('id', 'tooltip-background')
.attr('class', 'tooltip')
.attr('width', 80)
.attr('height', 20)
.style('fill', 'white')
.style('display', 'none')
.attr('rx', 5);
svg.append('text')
.attr('id', 'tooltip-text')
.attr('class', 'tooltip')
.style('display', 'none');
//set mouse events for the points
svg.selectAll('.point')
.on('mouseover', function() {
//make bigger
d3.select(this).attr('r', r*2);
//show tooltip
let d = d3.select(this).datum()
d3.selectAll('.tooltip')
.style('display', 'inline');
d3.select('#tooltip-text')
.attr('x', x_scale(d[0]) + r*2)
.attr('y', y_scale(d[1]) - r*2)
.attr('width', 100)
.text('Headline: XXXXX ' + x_metric + ': ' + d[0] + ' ' + y_metric + ': ' + d[1]);
d3.select('#tooltip-background')
.attr('x', x_scale(d[0]) + r)
.attr('y', y_scale(d[1]) - 20);
})
.on('mouseout', function() {
//reset radius
d3.select(this).attr('r', r);
//hide tooltip
d3.selectAll('.tooltip')
.style('display', 'none');
});
//draw median lines
//draw x-median every time
const x_median = x_scale(d3.quantile(x_data, 0.5))
svg.append('path')
.attr('d', d3.line()([[x_median, p_h], [x_median, (height-p_h)]]))
.attr('stroke', "black")
.style('fill', 'none')
.style('stroke-dasharray', '5,5');
//draw y-median if it represents a metric
if (y_metric != "Number of stories") {
const y_median = y_scale(d3.quantile(y_data, 0.5))
svg.append('path')
.attr('d', d3.line()([[p_w, y_median], [(width-p_w), y_median]]))
.attr('stroke', "black")
.style('fill', 'none')
.style('stroke-dasharray', '5,5');
}
return svg.node();
}
Insert cell
input_to_data = ([{'input': "Number of stories", 'data': 0}, {'input': "Views per story", 'data': random_views}, {'input': "Avg read time (m)", 'data': random_read_time}, {'input': "Social interactions per story", 'data': random_interactions}])
Insert cell
random_views = (d3.range(n_points).map(x => Math.floor(Math.random()*10000)))
Insert cell
random_read_time = (d3.range(n_points).map(x => Math.random()*10))
Insert cell
random_interactions = (d3.range(n_points).map(x => Math.floor(Math.random()*1000)))
Insert cell
Insert cell
percentiles = d3.range(100)
Insert cell
html `
<style>
.axis-label {
font-family: sans-serif;
text-anchor: middle;
}
#tooltip-text {
position: absolute;
font-family: sans-serif;
font-size: 12px;
pointer-events: none;
}
</style>
`
Insert cell
import {select} from "@jashkenas/inputs"

Insert cell
import {slider} from "@jashkenas/inputs"
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more