LOESS (locally estimated scatterplot smoothing) is a method for producing a smooth curve through a set of scatterplot points. The implementation here is by Jason Davies from his library Science.js.
Adjust the slider to see how the curve changes depending upon its bandwidth. Higher bandwidths produce smoother lines, and vice versa. According to Wikipedia:
A user-specified input to the procedure called the "bandwidth" or "smoothing parameter" determines how much of the data is used to fit each local polynomial. The smoothing parameter, α, is the fraction of the total number n of data points that are used in each local fit.
The chart displays the average annual temperature in California from 1900 to 2017. Source: NOAA.
A prettier version of this chart was published in Axios on Nov. 15, 2018, under the headline "Humans are a wildfire threat multiplier".
xxxxxxxxxx
<html>
<head>
<style>
body {
margin: 0;
font-family: "Helvetica Neue", sans-serif;
}
#bandwidth {
position: absolute;
left: 100px;
}
.loess-line {
fill: none;
stroke-width: 2px;
stroke: steelblue;
}
</style>
</head>
<body>
<div id="bandwidth">
<div>Bandwidth: <span class="bandwidth"></span></div>
<input type="range" min="0" max="100" value="25" />
</div>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/science@1.9.3/science.v1.min.js"></script>
<script>
var x_property = "year", y_property = "value";
var margin = {left: 30, right: 5, top: 5, bottom: 20},
aspect_ratio = .68,
width,
height;
var x_scale = d3.scaleLinear();
var y_scale = d3.scaleLinear();
var x_axis_generator = d3.axisBottom().tickFormat(d => d)
var y_axis_generator = d3.axisLeft();
var loess_generator = science.stats.loess(), loess_values, loess_data;
var line_generator = d3.line()
.x(d => x_scale(d[x_property]))
.y(d => y_scale(d[y_property]));
var input = d3.select("input");
var svg = d3.select("body").append("svg");
var g = svg.append("g");
var x_axis = g.append("g");
var y_axis = g.append("g");
d3.json("data.json").then(data => {
var x_values = data.map(d => d[x_property]);
var y_values = data.map(d => d[y_property]);
x_scale.domain(d3.extent(x_values));
y_scale.domain(d3.extent(y_values));
var points = g.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", 3);
function draw(resizing, adjusting){
if (resizing){
width = window.innerWidth - margin.left - margin.right;
height = d3.min([window.innerHeight, (width * aspect_ratio)]) - margin.top - margin.bottom;
x_scale.range([0, width]);
y_scale.range([height, 0]);
x_axis_generator.scale(x_scale);
y_axis_generator.scale(y_scale);
svg
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
g.attr("transform", "translate(" + [margin.left, margin.top] + ")");
x_axis
.attr("transform", "translate(0, " + height + ")")
.call(x_axis_generator);
y_axis.call(y_axis_generator);
points.attr("transform", d => "translate(" + [x_scale(d[x_property]), y_scale(d[y_property])] + ")");
}
if (adjusting){
var new_bandwidth = input.property("value") / 100;
d3.select(".bandwidth").html(new_bandwidth.toFixed(2));
loess_generator.bandwidth(new_bandwidth);
loess_values = loess_generator(x_values, y_values);
loess_data = data.map((d, i) => ({year: d[x_property], value: loess_values[i]}));
}
var loess_line = g.selectAll(".loess-line")
.data([loess_data]);
loess_line.enter().append("path")
.attr("class", "loess-line")
.merge(loess_line)
.attr("d", line_generator);
}
draw(1, 1);
window.addEventListener("resize", _ => draw(1, 0));
input.on("input", _ => draw(0, 1));
});
</script>
</body>
</html>
https://d3js.org/d3.v5.min.js
https://unpkg.com/science@1.9.3/science.v1.min.js