Skip to content

Instantly share code, notes, and snippets.

@EarlGeorge
Forked from cjhin/README.md
Created February 13, 2021 12:21
Show Gist options
  • Save EarlGeorge/3f467c998ded5c7d5cd98b48a0f997cb to your computer and use it in GitHub Desktop.
Save EarlGeorge/3f467c998ded5c7d5cd98b48a0f997cb to your computer and use it in GitHub Desktop.
D3 Day/Week/Month Line Chart

D3 Time Series Day/Week/Month

Wanted to create a (daily) time series chart with automatic rollups to weekly/monthly lines AND nice transitions

Solution

The solution I ended up going with is a bit of an optical illusion.

All three views are technically backed by "daily" data.

The weekly and monthly views though have had their data modified to rollup to the first of the week/month, and then all in between values are interpolated to create even slopes.

For example, given this daily data:

date, value
2014-01-01, 10
2014-01-02, 10
2014-01-03, 10
2014-01-04, 10
2014-01-05, 10
2014-01-06, 10
2014-01-07, 10

2014-01-08, 1
2014-01-09, 1
2014-01-10, 1
2014-01-11, 1
2014-01-12, 1
2014-01-13, 1
2014-01-14, 1

we first rollup to the first of the week

2014-01-01, 70
2014-01-02, 0
2014-01-03, 0
...
2014-01-08, 7
2014-01-09, 0
2014-01-10, 0

and then interpolate the remaining values

2014-01-01, 70
2014-01-02, 61
2014-01-03, 52
...
2014-01-08, 7

Issues

One big down-side to this (besides the fact that the code isn't very simple) is that the various d3 interpolations (line smoothing) won't work anymore.

If anyone knows of an easier way to do this ping me on github or leave a comment on the gist.

TODO:

  • something is off with the end of the month view, need to figure that out
date value
2014-01-01 16
2014-01-02 8
2014-01-03 43
2014-01-04 21
2014-01-05 52
2014-01-06 47
2014-01-07 25
2014-01-08 33
2014-01-09 42
2014-01-10 55
2014-01-11 61
2014-01-12 61
2014-01-13 59
2014-01-14 40
2014-01-15 61
2014-01-16 75
2014-01-17 72
2014-01-18 71
2014-01-19 67
2014-01-20 69
2014-01-21 54
2014-01-22 47
2014-01-23 68
2014-01-24 52
2014-01-25 33
2014-01-26 55
2014-01-27 72
2014-01-28 96
2014-01-29 87
2014-01-30 95
2014-01-31 82
2014-02-01 70
2014-02-02 49
2014-02-03 22
2014-02-04 9
2014-02-05 13
2014-02-06 42
2014-02-07 56
2014-02-08 63
2014-02-09 72
2014-02-10 52
2014-02-11 49
2014-02-12 5
2014-02-13 1
2014-02-14 9
2014-02-15 63
2014-02-16 82
2014-02-17 55
2014-02-18 77
2014-02-19 67
2014-02-20 52
2014-02-21 64
2014-02-22 93
2014-02-23 93
2014-02-24 87
2014-02-25 55
2014-02-26 33
2014-02-27 37
2014-02-28 48
2014-03-01 49
2014-03-02 55
2014-03-03 65
2014-03-04 55
2014-03-05 33
2014-03-06 53
2014-03-07 55
2014-03-08 42
2014-03-09 45
2014-03-10 49
2014-03-11 49
2014-03-12 79
2014-03-13 71
2014-03-14 55
2014-03-15 63
2014-03-16 69
2014-03-17 61
2014-03-18 16
2014-03-19 21
2014-03-20 24
2014-03-21 29
2014-03-22 33
2014-03-23 39
2014-03-24 42
2014-03-25 42
2014-03-26 42
2014-03-27 43
2014-03-28 61
2014-03-29 55
2014-03-30 42
2014-03-31 48
2014-04-01 40
2014-04-02 35
2014-04-03 27
2014-04-04 28
2014-04-05 37
2014-04-06 41
2014-04-07 39
2014-04-08 30
2014-04-09 41
2014-04-10 47
2014-04-11 42
2014-04-12 45
2014-04-13 51
2014-04-14 46
2014-04-15 42
2014-04-16 25
2014-04-17 29
2014-04-18 27
2014-04-19 39
2014-04-20 42
2014-04-21 39
2014-04-22 37
2014-04-23 33
2014-04-24 37
2014-04-25 44
2014-04-26 10
2014-04-27 23
2014-04-28 30
2014-04-29 31
2014-04-30 52
<style>
body {
font-family: Helvetica Neue, Helvetica-Neue, Helvetica;
}
.chart {
float: left;
}
.button-area {
padding: 40px 0 0 30px;
float: left;
}
.app-button {
display: block;
margin-bottom: 3px;
}
.axis text {
font-size: 0.8em;
}
.axis line {
stroke: #000;
}
.axis path {
display: none;
}
.line {
stroke: orange;
fill: none;
stroke-width: 3px;
}
.overlay {
fill: none;
pointer-events: all;
}
.tooltip {
position: absolute;
padding: 10px;
font: 12px sans-serif;
background: #222;
color: #fff;
border: 0px;
border-radius: 8px;
pointer-events: none;
opacity: 0.9;
visibility: hidden;
}
</style>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
d3.csv('data.csv', function(day_data) {
var formatDate = d3.time.format("%Y-%m-%d");
var bisectDate = d3.bisector(function(d) { return formatDate.parse(d['date']); }).left;
// fix those integers
day_data.forEach(function(d, i) {
d.value = parseInt(d.value);
});
///////////////////////
// generate weekly data
var week_data = create_time_unit_data(parse_for_week);
function parse_for_week(date) {
return formatDate.parse(date).getDay();
}
///////////////////////
// generate monthly data
var month_data = create_time_unit_data(parse_for_month);
function parse_for_month(date) {
return formatDate.parse(date).getDate() - 1;
}
///////////////////////
// helper functions
function create_time_unit_data(parse_date) {
var new_data = JSON.parse(JSON.stringify(day_data));
new_data.forEach(function(d, i) {
var offset = parse_date(d.date);
if(offset == 0 || i == 0 || i == day_data.length - 1) { // it's a new date_unit, or this is the first or last date in the array
d['start'] = true;
} else {
d['start'] = false;
// add this value to the start of the week
var tar_idx = i - offset;
if(tar_idx < 0) { tar_idx = 0; }
new_data[tar_idx].value += d.value;
// then nil out
d.value = -1;
}
});
fill_in_gaps(new_data);
return new_data;
}
function fill_in_gaps(data_array) {
// go back in and fill in the missing dates
data_array.forEach(function(d, i) {
if(d.start == false) {
var prev_val, next_val, prev_dist, next_dist;
for(var idx = i; idx>=0; idx--) {
if(data_array[idx].start == true) {
prev_val = data_array[idx].value;
prev_dist = i - idx;
break;
}
}
for(var idx = i; idx < data_array.length; idx++) {
if(data_array[idx].start == true) {
next_val = data_array[idx].value;
next_dist = idx - i;
break;
}
}
d.value = prev_val + ((next_val - prev_val) * (prev_dist/ (prev_dist + next_dist)));
}
});
}
///////////////////////
// Time unit selection buttons
var all_data = [
{ 'name': 'daily', 'data': day_data},
{ 'name': 'weekly', 'data': week_data},
{ 'name': 'monthly', 'data': month_data}
];
d3.select('.button-area').selectAll('.app-button')
.data(all_data)
.enter().append('button')
.attr('class', 'app-button')
.html(function(d) { return d.name; })
.on('click', function(d) {
curr_data = d.data;
updateChart();
});
///////////////////////
// Chart Size Setup
var margin = { top: 40, right: 50, bottom: 20, left: 20 };
var width = 840 - margin.left - margin.right;
var height = 500 - margin.top - margin.bottom;
var chart = d3.select(".chart")
.attr("width", 840)
.attr("height", 500)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
///////////////////////
// Title
chart.append("text")
.text("Time Series")
.attr("text-anchor", "middle")
.attr("class", "graph-title")
.attr("y", -10)
.attr("x", width / 2.0);
chart.append("text")
.attr("text-anchor", "middle")
.attr("style", "font-size: 0.9em")
.attr("y", height + 50)
.attr("x", width / 2.0);
///////////////////////
// Scales
var x = d3.time.scale()
.domain(d3.extent(day_data, function(d) { return formatDate.parse(d.date); }))
.range([0, width]);
var y;
///////////////////////
// Axis
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
///////////////////////
// Tooltips
var overlay = chart.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
.on("mouseover", function() { chart.selectAll('.focus').style("display", null); })
.on("mouseout", function() { chart.selectAll('.focus').style("display", "none"); })
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip");
var focus = chart.append("g")
.attr("class", "focus")
.style("display", "none");
focus.append("circle")
.attr("r", 4.5)
focus.append("text")
.attr("x", 9)
.attr("dy", ".35em");
///////////////////////
// DYNAMIC STUFF GOES HERE
// any data things that need to update (yxis, lines, etc)
function updateChart() {
///////////////////////
// Y axis changes
y = d3.scale.linear()
.domain([0, d3.max(curr_data, function(d) { return d.value; })])
.range([height, 0]);
var yAxis = d3.svg.axis()
.scale(y)
.orient("right");
// remove any old axis
chart.selectAll(".y-axis").remove()
chart.append("g")
.attr("class", "y axis y-axis")
.attr("transform", "translate(" + (width + 5) + ",0)")
.call(yAxis);
///////////////////////
// Line changes
var lineGenerator = d3.svg.line()
.x(function(d) { return x(formatDate.parse(d.date)) })
.y(function(d) { return y(d.value) });
var lines = chart.selectAll(".line")
.data([curr_data])
lines.enter().append("path")
.attr("class", "line")
.attr("d", lineGenerator);
lines.transition()
.duration(1000)
.attr("d", lineGenerator);
overlay.on("mousemove", mousemove);
}
function mousemove() {
var x0 = x.invert(d3.mouse(this)[0]),
i = bisectDate(curr_data, x0, 1),
d0 = curr_data[i - 1],
d1 = curr_data[i];
// search for dates where "start" == true, in other words, only valid dates for the dataset
// start of week, start of month
var i0 = i - 1;
while(d0.start == false && i0 > 0) {
i0 -= 1;
d0 = curr_data[i0];
}
var i1 = i;
while(d1.start == false && i1 < curr_data.length - 1) {
i1 += 1;
d1 = curr_data[i1];
}
var dIdx = x0 - d0.date > d1.date - x0 ? i1 : i0;
var tar_date = curr_data[dIdx]['date'];
var tooltip_string = tar_date;
var tar_value = curr_data[dIdx].value;
focus.attr("transform", "translate(" + x(formatDate.parse(tar_date)) + ","+y(tar_value)+ ")");
tooltip_string += "<br>" + tar_value;
tooltip.html(tooltip_string)
.style("visibility", "visible")
.style("top", d3.mouse(this)[1] - (tooltip[0][0].clientHeight - 30) + "px")
.style("left", d3.mouse(this)[0] - (tooltip[0][0].clientWidth / 2.0) + "px");
}
// default
curr_data = day_data;
updateChart();
});
</script>
<body>
<svg class="chart"></svg>
<div class='button-area'>
</div>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment