<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
body {
font: 14px sans-serif;
}
.axis path, .axis line {
shape-rendering: crispEdges;
}
.line {
fill: none;
stroke-width: 1.5px;
}
svg {
overflow: visible;
}
</style>
</head>
<body>
<script>
var data = CARGO_TALLY_DATA;
var margin = { top: 20, right: 100, bottom: 30, left: 50 };
var width = 950 - margin.left - margin.right;
var height = 500 - margin.top - margin.bottom;
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
var color = d3.scaleOrdinal(d3.schemeCategory10);
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y);
var line = d3.line()
.x(function(d) {
return x(d.time);
})
.y(function(d) {
return y(d.edges);
});
color.domain(data.map(function(dataset) {
return dataset.name;
}));
data.forEach(function(dataset) {
dataset.values.forEach(function(d) {
d.time = new Date(d.time);
});
});
var minDate = d3.min(data, function(dataset) {
return dataset.values[0].time;
});
var maxDate = d3.max(data, function(dataset) {
return dataset.values[dataset.values.length - 1].time;
});
var maxValue = d3.max(data, function(c) {
return d3.max(c.values, function(v) {
return v.edges;
});
});
x.domain([(21 * minDate - maxDate) / 20, maxDate]);
y.domain([0, 1.025 * maxValue]);
#if CARGO_TALLY_RELATIVE
var stepSize = y.ticks()[1] - y.ticks()[0]
var yFormatter = d3.format(`.${Math.max(0, d3.precisionFixed(stepSize) - 2)}%`);
var tooltipFormatter = d3.format(`.${Math.max(1, d3.precisionFixed(stepSize / 10) - 2)}%`);
#else
var yFormatter = d3.format(",");
var tooltipFormatter = d3.format(",");
#endif
yAxis.tickFormat(yFormatter);
var svg = d3.select("body")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left} ${margin.top})`);
var filter = svg.append("defs")
.append("filter")
.attr("x", "0")
.attr("y", "0")
.attr("width", "1")
.attr("height", "1")
.attr("id", "solid");
filter.append("feFlood")
.attr("flood-color", "white");
filter.append("feComposite")
.attr("in", "SourceGraphic");
var legend = svg.selectAll()
.data(data)
.enter()
.append("g");
legend.append("rect")
.attr("x", 50)
.attr("y", function(d, i) {
return i * 20;
})
.attr("width", 10)
.attr("height", 10)
.style("fill", function(d) {
return color(d.name);
});
legend.append("text")
.attr("x", 64)
.attr("y", function(d, i) {
return (i * 20) + 9;
})
.text(function(d) {
return d.name;
});
svg.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0 ${height})`)
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text(CARGO_TALLY_TITLE);
var curve = svg.selectAll()
.data(data)
.enter()
.append("g");
curve.append("path")
.attr("class", "line")
.attr("d", function(d) {
return line(d.values);
})
.style("stroke", function(d) {
return color(d.name);
})
.style("stroke-linejoin", "round");
curve.append("text")
.attr("transform", function(d) {
var last = d.values[d.values.length - 1];
return `translate(${x(last.time)} ${y(last.edges)})`;
})
.attr("x", 3)
.attr("dy", ".35em")
.text(function(d) {
return d.name;
});
var mouseG = svg.append("g")
.style("opacity", "0");
mouseG.append("path") .style("stroke", "black")
.style("stroke-width", "1px")
.attr("d", `M0 ${height + xAxis.tickSize()} 0 0`);
var mouseDate = mouseG.append("text")
.attr("y", height + 9)
.attr("dy", "0.71em")
.attr("text-anchor", "middle")
.attr("filter", "url(#solid)");
var mousePerLine = mouseG.selectAll()
.data(data)
.enter()
.append("g")
.attr("class", "mouse-per-line");
mousePerLine.append("circle")
.attr("r", 7)
.style("stroke", function(d) {
return color(d.name);
})
.style("fill", "none")
.style("stroke-width", "1px");
mousePerLine.append("text")
.attr("x", -6)
.attr("y", -4)
.style("text-anchor", "end");
svg.append("rect") .attr("width", width) .attr("height", height + xAxis.tickSize() + 16)
.attr("fill", "none")
.attr("pointer-events", "all")
.on("mouseout", function() { mouseG.style("opacity", "0");
})
.on("mouseover", function() { mouseG.style("opacity", "1");
})
.on("mousemove", function(event) { var mouse = d3.pointer(event);
mouseG.attr("transform", `translate(${mouse[0]} 0)`);
mousePerLine.attr("transform", function(d, i) {
var xDate = x.invert(mouse[0]);
var bisect = d3.bisector(function(d) { return d.time; }).right;
var idx = bisect(d.values, xDate);
var below = d.values[idx - (idx > 0)];
var above = d.values[idx - (idx == d.values.length)];
var interp = below.time == above.time ? 0 : (xDate - below.time) / (above.time - below.time);
var val = d3.interpolateNumber(below.edges, above.edges)(interp);
d3.select(this)
.style("opacity", below.edges ? "1" : "0")
.select("text")
.text(tooltipFormatter(below.edges));
mouseDate.text(d3.timeFormat("%b %-d")(xDate));
return `translate(0 ${y(val)})`;
});
});
</script>
</body>
</html>