document.getElementById("generate")
.addEventListener("click", generateChart);
generateChart();
function generateChart() {
// clear chart
d3.select("#chart")
.selectAll("*")
.remove();
// random data
const data = sampleData();
// chart init
const margin = 30;
const width = 300;
const height = 120;
const svg = d3.select("#chart")
.append("svg")
.attr("width", width + margin + margin)
.attr("height", height + margin + margin);
const gChart = svg.append("g")
.attr("transform", `translate(${margin},${margin})`);
const color = d3.scaleOrdinal(d3.schemeCategory10);
// scales and axes
const xMax = d3.max(data[0].values, d => d.index + 1);
const yMax = d3.max(data, series => {
return d3.max(series.values, d => d.value * 1.2);
});
const xScale = d3.scaleLinear()
.range([0, width])
.domain([1, xMax]);
const yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, yMax]);
gChart.append("g")
.call(d3.axisLeft().scale(yScale));
gChart.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom().scale(xScale));
// draw lines
const line = d3.line()
.curve(d3.curveCardinal)
.x(d => xScale(d.index))
.y(d => yScale(d.value));
const lines = gChart.selectAll(".series")
.data(data)
.enter()
.append("g");
lines.append("path")
.attr("class", "series")
.attr("d", d => line(d.values))
.attr("stroke", d => color(d.id))
// add labels
const labels = gChart.selectAll(".labels")
.data(data)
.enter()
.append("g");
// x and y refer to iqr dependent code in sampleData()
labels.append("text")
.attr("class", "labels")
.attr("x", d => xScale(d.xLabelIndex))
.attr("y", d => yScale(d.xLabelValue.value) + (d.xLabelOrient === "above" ? -15 : 10))
//.attr("dy", ".35em")
.attr("fill", d => color(d.id))
.text(d => d.id)
}
function sampleData() {
const labels = [
"Amsterdam", "Barcelona",
"Copenhagen", "Dublin",
"Edinburgh", "Frankfurt"
]; // 6 example series
const seriesCount = d3.randomInt(2, labels.length + 1)(); // e.g. 2-6 lines
const sampleLabels = labels.slice(0, seriesCount);
const valueCount = d3.randomInt(12, 25)(); // e.g. 1-2 years
const series = sampleLabels.map(label => {
return {
"id": label,
"values": d3.range(valueCount).map((n, i) => {
return {
"index": i + 1,
"value": d3.randomIrwinHall(5)() * (i ** 0.5)
}
})
}
});
// look at points that are max outside box per series
const iqrLows = d3.range(valueCount).map((n, i) => {
const values = sampleLabels.map((label, i2) => series[i2].values[i]).map(o => o.value);
return d3.quantile(values, 0.25);
});
const iqrHighs = d3.range(valueCount).map((n, i) => {
const values = sampleLabels.map((label, i2) => series[i2].values[i]).map(o => o.value);
return d3.quantile(values, 0.75);
});
series.forEach(s => {
const compareLows = s.values.map((k, i) => k.value < iqrLows[i] ? iqrLows[i] - k.value : 0);
const compareHighs = s.values.map((k, i) => k.value > iqrHighs[i] ? k.value - iqrHighs[i] : 0);
if (d3.max(compareHighs) > d3.max(compareLows)) {
s.xLabelIndex = d3.maxIndex(compareHighs) + 1;
s.xLabelValue = s.values[d3.maxIndex(compareHighs)];
s.xLabelOrient = "above";
} else {
s.xLabelIndex = d3.maxIndex(compareLows) + 1;
s.xLabelValue = s.values[d3.maxIndex(compareLows)];
s.xLabelOrient = "below";
}
});
//console.log(series);
return series;
}
.series {
fill: none;
stroke-width: 2px;
}
.labels {
text-anchor: middle;
font: 10px sans-serif;
font-weight: bold;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
<button id="generate">Generate</button>
<div id="chart"></div>