Skip to content

Instantly share code, notes, and snippets.

@cjhin
Last active September 29, 2021 02:13
Show Gist options
  • Save cjhin/8872a492d44694ecf5a883642926f19c to your computer and use it in GitHub Desktop.
Save cjhin/8872a492d44694ecf5a883642926f19c 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>
@arpitgupta1214
Copy link

arpitgupta1214 commented Apr 26, 2020

Hi, I read about your project on freelancer and I am interested. I went through the issue you are facing and some of your code. Though I am no expert I believe that a good solution would be to recompute the data for the week and month sections. I could work on it and try to fix it if you want. please email arpitgupta1214@gmail.com if you want to contact me.
I read your proposal so-
hello world

@cjhin
Copy link
Author

cjhin commented Apr 27, 2020

Hey @arpitgupta1214 thanks for the note! I'm not actually sure I quite understand, I think the code I have above does recompute the data for the week and month sections.

Also, apologies, I'm not sure I understand what on freelancer and I read your proposal means? Is there a proposal on a freelance code website that links to this gist?

@arpitgupta1214
Copy link

arpitgupta1214 commented Apr 27, 2020 via email

@cjhin
Copy link
Author

cjhin commented Apr 27, 2020

@arpitgupta1214 do you mind sending me a link to this posting?

@arpitgupta1214
Copy link

arpitgupta1214 commented Apr 27, 2020 via email

@arpitgupta1214
Copy link

arpitgupta1214 commented Apr 27, 2020 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment