Published
Edited
Sep 7, 2019
Importers
5 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const colors = {
domain: ['setosa','versicolor','virginica'],
range: ['rgb(60,200,200)','rgb(150,100,200)','rgb(200,150,60)']
}
const dots = vl.markCircle()
.data(data)
.encode(
vl.x().fieldQ("sepalLength").scale({zero: false}).title("sepalLength"),
vl.y().fieldQ("sepalWidth").scale({zero: false}).title("sepalWidth"),
// vl.size().count(),
vl.color().fieldN("species").scale(colors),
vl.opacity().value(0.5),
)
const leastSquares = vl.markLine()
.encode(
vl.x().fieldQ('x').title(null),
vl.y().fieldQ('y').title(null)
)
const colorZip = colors['domain'].map((d, i) => [d, colors['range'][i]])
return vl.layer(
dots,
colorZip.map(([domain, range]) => (
leastSquares.data(leastSquaresData(data.filter(d => d.species === domain), 'sepalLength', 'sepalWidth')[0])
.markLine({ color: range}))),
leastSquares.data(leastSquaresData(data, 'sepalLength', 'sepalWidth')[0])
.markLine({ color: 'black', strokeDash: [5,4]}),
).render()
}
Insert cell
Insert cell
Insert cell
chart = {
const margin = { top: 50, bottom: 50, left: 50, right: 100},
height = 450,
width = 550,
colors = {
domain: ['setosa','versicolor','virginica'],
range: ['rgb(60,200,200)','rgb(150,100,200)','rgb(200,150,60)']
}
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0,0,width,height])
const x = d3.scaleLinear()
.domain(d3.extent(data, d => d.sepalLength))
.range([margin.left, width - margin.right])
.nice()
const y = d3.scaleLinear()
.domain(d3.extent(data, d => d.sepalWidth))
.range([height - margin.bottom, margin.top])
.nice()
const color = d3.scaleOrdinal()
.domain(colors.domain)
.range(colors.range)
const xAxis = g => g.attr("transform", `translate(0, ${height - margin.bottom})`).call(d3.axisBottom(x))
const yAxis = g => g.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y))
.call(g => g.select(".domain").remove())
svg.append("g")
.call(xAxis)
.selectAll(".tick line").clone()
.attr("y1", margin.top + margin.bottom - height)
.attr("stroke", "lightgrey")
svg.append("g")
.call(yAxis)
.selectAll(".tick line").clone()
.attr("x1", width - margin.left - margin.right)
.attr("stroke", "lightgrey")
// Create the legend
const legend = svg.append("g").attr("transform", `translate(${width - margin.right + 20}, ${margin.top + 10})`)
// legend title
legend.append("text")
.text("species")
.attr("font-size", "0.7em")
.attr("font-weight", "bold")
.attr("font-family", "sans-serif")
// legend circles
legend.selectAll(".legend-mark")
.data(colors.domain)
.join("circle")
.attr("class", "legend-mark")
.attr("cx", 5)
.attr("cy", (d, i) => (i + 1) * 15)
.attr("fill", d => color(d))
.attr("r", 5)
.attr("opacity", 0.5)
// legend names
legend.selectAll(".legend-name")
.data(colors.domain)
.join("text")
.attr("class", "legend-name")
.attr("x", 15)
.attr("y", (d, i) => (i + 1) * 15)
.attr("text-anchor", "start")
.attr("alignment-baseline", "middle")
.attr("font-size", "0.7em")
.attr("font-family", "sans-serif")
.text(d => d)
const points = svg.selectAll(".scatter")
.data(data)
.join("circle")
.attr("class", "scatter")
.attr("cx", d => x(d.sepalLength))
.attr("cy", d => y(d.sepalWidth))
.attr("fill", d => color(d.species))
.attr("r", 3)
.attr("opacity", 0.5)
const lines = svg.selectAll(".line")
.data(colors.domain
.map(d => (
[
...leastSquaresData(data.filter(f => f.species === d), 'sepalLength', 'sepalWidth')[0], {species: d}
])))
.join("line")
.attr("class", "line")
.attr("x1", d => x(d[0].x))
.attr("x2", d => x(d[1].x))
.attr("y1", d => y(d[0].y))
.attr("y2", d => y(d[1].y))
.attr("stroke", d => color(d[2].species))
.attr("stroke-width", 2)
const overall_line = svg.selectAll(".total-line")
.data([leastSquaresData(data, 'sepalLength', 'sepalWidth')[0]])
.join("line")
.attr("class", "total-line")
.attr("x1", d => x(d[0].x))
.attr("x2", d => x(d[1].x))
.attr("y1", d => y(d[0].y))
.attr("y2", d => y(d[1].y))
.attr("stroke", "black")
.attr("stroke-width", 2)
.attr("stroke-dasharray", "5 4")
svg.append("text") // add a y-axis label
.attr("transform", "rotate(-90)")
.attr("alignment-baseline", "center")
.attr("text-anchor", "middle")
.attr("x", -height/2.0) // mind-bendingly x/y swapped because of rotate(-90)
.attr("y", margin.left - 30)
.attr("font-size", "0.7em")
.attr("font-weight", "bold")
.attr("font-family", "sans-serif")
.text("sepalWidth")
svg.append("text") // add an x-axis label
.attr("alignment-baseline", "center")
.attr("text-anchor", "middle")
.attr("x", (width - margin.left)/2.0)
.attr("y", height - margin.bottom + 30)
.attr("font-size", "0.7em")
.attr("font-weight", "bold")
.attr("font-family", "sans-serif")
.text("sepalLength")
return svg.node()
}
Insert cell
Insert cell
Insert cell
// returns slope, intercept and r-square of the line
// from: http://bl.ocks.org/benvandyke/8459843
function leastSquares(xSeries, ySeries) {
var reduceSumFunc = function(prev, cur) { return prev + cur; };

var xBar = xSeries.reduce(reduceSumFunc) * 1.0 / xSeries.length;
var yBar = ySeries.reduce(reduceSumFunc) * 1.0 / ySeries.length;

var ssXX = xSeries.map(function(d) { return Math.pow(d - xBar, 2); })
.reduce(reduceSumFunc);

var ssYY = ySeries.map(function(d) { return Math.pow(d - yBar, 2); })
.reduce(reduceSumFunc);

var ssXY = xSeries.map(function(d, i) { return (d - xBar) * (ySeries[i] - yBar); })
.reduce(reduceSumFunc);

var slope = ssXY / ssXX;
var intercept = yBar - (xBar * slope);
var rSquare = Math.pow(ssXY, 2) / (ssXX * ssYY);

return [slope, intercept, rSquare];
}
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