chart = function(data) {
let height = data.height || width / 2;
let margin = data.margin || { top: 20, right: 0, bottom: 40, left: 40 };
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("overflow", "visible");
let series = data.series || [
{
points: data.points,
n: data.n
}
];
var series_max_title_width = 0;
series.forEach(series => {
if (series.title && series.titlePosition == "right")
series_max_title_width = Math.max(
series_max_title_width,
series.title.length * 10
);
});
margin.right += series_max_title_width;
function range(axisindex, axisobj) {
if (!Array.isArray(data.points)) return [axisobj.min, axisobj.max];
else
return [
d3.min(data.points, _ => _[axisindex]),
d3.max(data.points, _ => _[axisindex])
];
}
const x_scale = d3
.scaleLinear()
.domain(range(0, data.xAxis))
.nice()
.range([margin.left, width - margin.right]);
const y_scale = d3
.scaleLinear()
.domain(range(1, data.yAxis))
.nice()
.range([height - margin.bottom, margin.top]);
// For series whose points are given as functions, evaluate the functions
// and store the computed points.
series.forEach(series => {
if (typeof series.points != "function") return;
let n = series.n || width / 6; /* evaluate at about every 6th pixel */
let points = [];
for (let i = 0; i < n; i++) {
let xval = data.xAxis.min + (i / n) * (data.xAxis.max - data.xAxis.min);
let yval = series.points(xval);
points.push([xval, yval]);
}
series.points = points;
});
// Convert all of the data points to canvas points.
series.forEach(series => {
if (!series.points) return; // missing data?
// Compute canvas coordinates.
let canvas_points = series.points.map(p => [x_scale(p[0]), y_scale(p[1])]);
// If any point is NaN, stop.
for (let i = 0; i < canvas_points.length; i++)
if (isNaN(canvas_points[i][0]) || isNaN(canvas_points[i][1])) return;
// Store.
series.canvas_points = canvas_points;
});
// Solve for the coordinates of labels for series titles.
SolveSeriesLabelCoordinates(series);
// Create the lines from series.
let default_colors = make_scale(
'#2c6ba6',
'#99cc99',
series.length > 2 ? series.length : 2
);
series.forEach(series => {
if (!series.canvas_points) return;
let datum = series.canvas_points;
// Add the line.
var series_color = series.color || default_colors.shift().css;
svg
.append("path")
.datum(datum)
.attr("fill", "none")
.attr("stroke", series_color)
.attr("stroke-width", series.width || 1.5)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr(
"d",
d3
.line()
.defined(_ => !isNaN(_[0]))
.x(_ => _[0])
.y(_ => _[1])
);
// Add the series label.
if (series.title) {
var pt = [width - margin.right, datum[datum.length - 1][1]];
var pa = 0;
if (series.titlePosition != "right" && series.titleLabelCoordinate) {
pt = series.titleLabelCoordinate;
// Add an offset from the line perpendicular to the line.
// First compute a perpendicular vector (in series coordinates)
// from the tangent vector.
var v = [
-series.titleLabelLineVector[1],
series.titleLabelLineVector[0]
];
// Compute the angle of this vector.
pa = Math.round((Math.atan2(v[1], v[0]) * 180) / 3.14159) - 90;
// Add the offset above the curve.
pt[0] -= v[0] * 9 * series.titleLabelSide;
pt[1] -= v[1] * 9 * series.titleLabelSide;
}
svg
.append("text")
.attr(
"transform",
"translate(" + pt[0] + "," + pt[1] + ") rotate(" + pa + ")"
)
.attr("dy", ".35em")
.attr("text-anchor", !series.titleLabelCoordinate ? "start" : "middle")
.style("fill", series_color)
.text(series.title);
}
});
// Add x-axis and its title.
svg
.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(
d3
.axisBottom(x_scale)
.ticks(width / 80)
.tickSizeOuter(0)
)
.call(g =>
g
.select(".tick:last-of-type text")
.clone()
.attr("text-anchor", "middle")
.attr("x", -(width - margin.left - margin.right) / 2)
.attr("y", margin.bottom - 10)
.attr("font-weight", "bold")
.text(data.xAxis ? data.xAxis.title : "")
);
// Add y-axis and its title.
svg
.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y_scale))
.call(g => g.select(".domain").remove())
.call(g =>
g
.select(".tick:last-of-type text")
.clone()
.attr("transform", `rotate(-90)`)
.attr("text-anchor", "middle")
.attr("x", -(height - margin.top - margin.bottom) / 2)
.attr("y", -margin.left)
.attr("font-weight", "bold")
.text(data.yAxis ? data.yAxis.title : "")
);
return svg.node();
}