chart = (data, options) => {
const unit = width / 954
const defaults = {
helpText: "data is an array of arrays: [[0, 0], [1, 5], ...]",
margin: {
left: 50 * unit + 2 * 14 * unit / Math.sqrt(unit),
top: 25 * unit,
right: 35 * unit,
bottom: 50 * unit + 14 * unit / Math.sqrt(unit),
},
width: width,
height: width / (Math.sqrt(2) + 1),
pointRadius: 15 * unit / Math.log(data.length + 2),
pointFill: "black",
pointOpacity: 1,
lineStroke: "black",
lineStrokeWidth: 2,
curve: "curveLinear",
ticklabelSize: 14 * unit / Math.sqrt(unit),
tickFontFamily: "-apple-system,system-ui,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Helvetica,Arial,sans-serif"
}
const p = Object.assign(defaults, options)
const x = d3.scaleLinear().domain(d3.extent(data, d => d[0])).nice().range([0, width])
const y = d3.scaleLinear().domain(d3.extent(data, d => d[1])).nice().range([p.height, 0])
const linePath = d3.line()
.defined(d => (!isNaN(d[0]) && !isNaN(d[1])))
.curve(d3[p.curve])
.x(d => x(d[0]))
.y(d => y(d[1]))
let svg = d3.create("svg")
.attr("viewBox", `-${p.margin.left} -${p.margin.top} ${p.width + p.margin.left + p.margin.right} ${p.height + p.margin.top + p.margin.bottom}`)
svg.append("g").attr("class", "line").append("path")
.attr("d", linePath(data))
.attr("stroke", p.lineStroke)
.attr("stroke-width", p.lineStrokeWidth)
.attr("fill", "none")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
svg.append("g").attr("class", "points").selectAll("circle").data(data).join("circle")
.attr("cx", d => x(d[0]))
.attr("cy", d => y(d[1]))
.attr("r", p.pointRadius)
.attr("fill", p.pointFill)
.attr("fill-opacity", p.pointOpacity)
.style("display", function (d, i) {
if (isNaN(d[0]) || isNaN(d[1])) {
return "none"
}
})
let xAxis = svg.append("g").attr("id", "xAxis").call(d3.axisBottom(x))
.attr("transform", `translate(0, ${p.height + p.pointRadius + p.ticklabelSize})`)
.style("font-size", p.ticklabelSize)
.style("font-family", p.tickFontFamily)
svg.append("g").attr("id", "yAxis").call(d3.axisLeft(y))
.attr("transform", `translate(${-p.pointRadius - p.ticklabelSize},0)`)
.style("font-size", p.ticklabelSize)
.style("font-family", p.tickFontFamily)
if (!data.length) {
svg.append("g").append("text").text("data is empty ¯\\_(ツ)_/¯")
.attr("x", p.width / 2)
.attr("y", p.height / 2)
.attr("text-anchor", "middle")
.style("font-family", "monospace")
.style("font-size", 2 * p.ticklabelSize)
}
const result = svg.node()
result.help = defaults
return result
}