Charting sleep periods over the course of a number of days.
- vertical axis day
- horizontal time of day
milestones
- each day gets unique track
forked from zeffii's block: sleep_tkd - nested v3 - comments
Charting sleep periods over the course of a number of days.
milestones
forked from zeffii's block: sleep_tkd - nested v3 - comments
/* | |
MIT License 2016 - Dealga McArdle. | |
================================= | |
For now this is moderately light usage of d3.js - maybe to the chagrin of more weathered | |
d3.js masters - simply to illustrate a problem i'm facing. | |
typical json to parse: | |
.... | |
"YYYY/MM/DD": { | |
"sleeptimes": "00:00->07:00,15:20->18:30,23:00->24:00", | |
"comments": "AMC visit, Cystoscopy - all clear", | |
"glucose": [ | |
{"time": "06:35", "value": 6.94}, | |
{"time": "15:18", "value": 14.93}, | |
{"time": "20:28", "value": 11.77}, | |
{"time": "22:13", "value": 12.71} | |
] | |
}, | |
.... | |
- could color grade using 0-5 5-10 10-15 15-20 20-25 25-up | |
intentionally left blank. | |
*/ | |
function get_ratio_from_time(time_str){ | |
// this function converts a time_str into how far into the day it is | |
// f.ex 12:00 => 0.5 06:00 => 0.25 | |
var time_parts = time_str.split(':'); | |
var a = +time_parts[0]; | |
var b = +time_parts[1]; | |
return (1/1440*((a*60) + b)) | |
} | |
function get_color(tval){ | |
if (tval < 5.0){return {bg:"#18dff5", tx: "#111"}} | |
else if (tval >= 5.0 && tval < 10.0){return {bg:"#27f518", tx: "#111"}} | |
else if (tval >= 10.0 && tval < 15.0){return {bg:"#c8f97f", tx: "#111"}} | |
else if (tval >= 15.0 && tval < 20.0){return {bg:"#ffb76b", tx: "#111"}} | |
else if (tval >= 20.0){return {bg:"#fe70bc", tx: "#ffffff"}} | |
} | |
var svg = d3.select("svg") | |
var format_day = d3.time.format("%Y/%m/%d"); | |
var format_hours = d3.time.format("%H:%M"); | |
var formatTime = d3.time.format("%m / %d"); | |
d3.json("times.json", function(error, times) { | |
if (error) throw error; | |
times = json_preprocessor(times); | |
draw_graph(times); | |
}); | |
function times_preprocessor(t){ | |
if (!t){return []} | |
var ts = t.split(','); | |
var emb = []; | |
for (var k of ts){ | |
var abl = k.split('->'); | |
if (abl.length === 2){ emb.push(abl); } | |
} | |
return emb; | |
} | |
function json_preprocessor(p){ | |
var new_object_array = []; | |
for (var key in p) { | |
if (p.hasOwnProperty(key)) { | |
var day_datum = format_day.parse(key); | |
var time_object = p[key]; | |
var processed_times = times_preprocessor(time_object.sleeptimes); | |
new_object_array.push({ | |
day: day_datum, | |
times: processed_times, | |
comments: time_object.comments, | |
glucose: time_object.glucose | |
}); | |
} | |
} | |
return new_object_array; | |
} | |
function draw_graph(times){ | |
var margin = {top: 20, right: 80, bottom: 30, left: 50}, | |
width = 960 - margin.left - margin.right, | |
height = 500 - margin.top - margin.bottom; | |
svg | |
.attr("width", width + margin.left + margin.right) | |
.attr("height", height + margin.top + margin.bottom); | |
var main_group = svg.append("g") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
var tracks = main_group.append('g').classed('tracks', true) | |
var yindex = 0; | |
var begin_time, end_time; | |
var tab_height = 12; | |
var bar_height = 15; | |
var tab_ear = 4; | |
var vertical_skip = 17; | |
var time_offset_down = 6; | |
for (var item of times){ | |
var mg = tracks.append('g'); | |
for (var time_slot of item.times){ | |
begin_time = get_ratio_from_time(time_slot[0]); | |
end_time = get_ratio_from_time(time_slot[1]); | |
var rec = mg.append('rect'); | |
rec.attr("width", (end_time - begin_time) * width) | |
.attr("height", bar_height) | |
.style({fill: "#badcfc"}) | |
.attr("transform", function(d){ | |
return "translate(" + [ | |
begin_time * width, | |
yindex * vertical_skip | |
] + ")" | |
}) | |
} | |
var gg = tracks.append('g'); | |
for (var reading of item.glucose){ | |
var gtime = get_ratio_from_time(reading.time); | |
var gval = reading.value; | |
var ggroup = gg.append('g'); | |
ggroup.attr({ | |
transform: "translate(" + [0, (yindex * vertical_skip + 8)] + ")"}) | |
var cl = ggroup.append('rect'); | |
cl.attr({transform: "translate(" + [(gtime * width) + tab_ear , -6] +")"}) | |
.attr({'height': tab_height}) | |
.style({fill: get_color(gval).bg}) | |
var cl2 = ggroup.append('path'); | |
cl2.attr({transform: "translate(" + [(gtime * width) + tab_ear , -6] +")"}) | |
.attr({'d': 'M' + [0,0,-tab_ear,tab_height/2,0,tab_height] + 'z'}) | |
.style({fill: get_color(gval).bg}) | |
var cltext = ggroup.append('text'); | |
cltext.text(gval) | |
.attr({transform: "translate(" + [(gtime * width) + tab_ear , 4] +")"}) | |
.attr({ | |
'text-anchor': "start", | |
"font-size": 11, | |
"font-family": "sans-serif" | |
}) | |
cltext.style({'fill': get_color(gval).tx }) | |
var textwidth = cltext.node().getComputedTextLength(); | |
cl.attr({'width': textwidth}) | |
} | |
// draw comments | |
if (item.comments.length > 0){ | |
var comment_group = mg.append('g'); | |
comment_group.attr({ | |
transform: "translate(" + [width + 20, yindex * vertical_skip] + ")" | |
}) | |
var newrec = comment_group.append('rect') | |
newrec.attr('width', 20).attr('height', 13) | |
newrec.style({fill: "#fce7ba"}) | |
} | |
yindex += 1; | |
mg.append('text') | |
.text(formatTime(item.day)) | |
.attr("transform", "translate(-21," + (yindex * vertical_skip - 7) + ")") | |
.attr({'text-anchor': "middle", "font-size": 10, "font-family": "sans-serif"}) | |
} | |
var indicat = main_group.append('g').classed('indications', true); | |
var ditimes = "00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24".split(" "); | |
var circle_push_vertical = 1.593856; // <---- use to push hourly circles up / down | |
var time_index = 0; | |
for (var tick of ditimes){ | |
tick = tick + ":00"; | |
var tgl = indicat.append('g'); | |
var tl = tgl.append('line'); | |
var xpostime = Math.floor(get_ratio_from_time(tick) * width); | |
tl.attr('x1', xpostime) | |
.attr('x2', xpostime) | |
.attr('y1', 0) | |
.attr('y2', height) | |
.style({"stroke-width": 1, stroke: "#cfdbe7"}) | |
if (time_index % 3 === 0){ | |
tl.style({"stroke-width": 1, stroke: "#aabfd4"}) | |
var tcl = tgl.append('circle'); | |
tcl.attr('cx', Math.floor(get_ratio_from_time(tick) * width)) | |
.attr('cy', height/circle_push_vertical) | |
.attr('r', 16) | |
.style({fill: "#e0eeff"}) | |
var txl = tgl.append('text'); | |
txl.attr({ | |
'text-anchor': "middle", | |
"font-size": 17, | |
"font-family": "sans-serif" | |
}) | |
.attr( | |
'transform', | |
'translate(' + [ | |
Math.floor(get_ratio_from_time(tick) * width), | |
(height/circle_push_vertical)+time_offset_down ] + | |
')') | |
.style({'fill': "#7c7c7c"}) | |
.text(tick.slice(0,2)) | |
} | |
time_index += 1; | |
} | |
} |
<!DOCTYPE html> | |
<head> | |
<meta charset="utf-8"> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script> | |
<script src="https://d3js.org/d3-time.v0.2.min.js"></script> | |
<link rel="stylesheet" type="text/css" href="style.css" /> | |
<style> | |
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; } | |
svg { width:100%; height: 100% } | |
</style> | |
</head> | |
<body> | |
<svg></svg> | |
<script src="dillitant.js"></script> | |
</body> |
.axis path, | |
.axis line { | |
fill: none; | |
stroke: #000; | |
shape-rendering: crispEdges; | |
} | |
.axis text { | |
font: 10px sans-serif; | |
} | |
{ | |
"2016/04/13": { | |
"comments": "", | |
"glucose": [ | |
{ | |
"time": "07:46", | |
"value": 8.77 | |
}, | |
{ | |
"time": "11:18", | |
"value": 12.88 | |
}, | |
{ | |
"time": "17:25", | |
"value": 15.65 | |
}, | |
{ | |
"time": "21:24", | |
"value": 23.81 | |
} | |
], | |
"times": "" | |
}, | |
"2016/04/14": { | |
"comments": "", | |
"glucose": [ | |
{ | |
"time": "08:18", | |
"value": 7.49 | |
}, | |
{ | |
"time": "12:56", | |
"value": 9.1 | |
}, | |
{ | |
"time": "15:27", | |
"value": 15.38 | |
}, | |
{ | |
"time": "15:29", | |
"value": 19.7 | |
}, | |
{ | |
"time": "20:59", | |
"value": 17.15 | |
} | |
], | |
"times": "" | |
}, | |
"2016/04/15": { | |
"comments": "", | |
"glucose": [ | |
{ | |
"time": "08:08", | |
"value": 7.55 | |
}, | |
{ | |
"time": "12:26", | |
"value": 14.82 | |
}, | |
{ | |
"time": "17:27", | |
"value": 19.21 | |
}, | |
{ | |
"time": "21:43", | |
"value": 14.6 | |
} | |
], | |
"times": "" | |
}, | |
"2016/04/16": { | |
"comments": "", | |
"glucose": [ | |
{ | |
"time": "07:33", | |
"value": 8.38 | |
}, | |
{ | |
"time": "11:54", | |
"value": 12.49 | |
}, | |
{ | |
"time": "17:17", | |
"value": 12.1 | |
}, | |
{ | |
"time": "21:27", | |
"value": 12.6 | |
} | |
], | |
"times": "" | |
}, | |
"2016/04/17": { | |
"comments": "", | |
"glucose": [ | |
{ | |
"time": "08:03", | |
"value": 8.1 | |
}, | |
{ | |
"time": "13:12", | |
"value": 16.6 | |
}, | |
{ | |
"time": "17:55", | |
"value": 15.71 | |
}, | |
{ | |
"time": "22:18", | |
"value": 10.44 | |
} | |
], | |
"times": "" | |
}, | |
"2016/04/18": { | |
"comments": "", | |
"glucose": [ | |
{ | |
"time": "08:10", | |
"value": 7.77 | |
}, | |
{ | |
"time": "12:49", | |
"value": 12.77 | |
}, | |
{ | |
"time": "18:17", | |
"value": 10.55 | |
}, | |
{ | |
"time": "22:03", | |
"value": 14.49 | |
} | |
], | |
"times": "" | |
}, | |
"2016/04/19": { | |
"comments": "", | |
"glucose": [ | |
{ | |
"time": "08:20", | |
"value": 6.88 | |
}, | |
{ | |
"time": "12:41", | |
"value": 13.65 | |
}, | |
{ | |
"time": "17:06", | |
"value": 16.49 | |
}, | |
{ | |
"time": "22:04", | |
"value": 11.6 | |
} | |
], | |
"times": "" | |
}, | |
"2016/04/20": { | |
"comments": "", | |
"glucose": [ | |
{ | |
"time": "08:28", | |
"value": 5.66 | |
}, | |
{ | |
"time": "13:03", | |
"value": 11.93 | |
}, | |
{ | |
"time": "18:01", | |
"value": 13.04 | |
}, | |
{ | |
"time": "22:25", | |
"value": 12.49 | |
} | |
], | |
"times": "" | |
}, | |
"2016/04/21": { | |
"comments": "", | |
"glucose": [ | |
{ | |
"time": "07:54", | |
"value": 7.38 | |
}, | |
{ | |
"time": "11:47", | |
"value": 14.82 | |
}, | |
{ | |
"time": "17:18", | |
"value": 13.99 | |
}, | |
{ | |
"time": "22:21", | |
"value": 8.44 | |
} | |
], | |
"times": "" | |
}, | |
"2016/04/22": { | |
"comments": "", | |
"glucose": [ | |
{ | |
"time": "08:02", | |
"value": 7.77 | |
}, | |
{ | |
"time": "12:47", | |
"value": 11.93 | |
}, | |
{ | |
"time": "18:46", | |
"value": 10.44 | |
}, | |
{ | |
"time": "22:19", | |
"value": 11.55 | |
} | |
], | |
"times": "" | |
}, | |
"2016/04/23": { | |
"comments": "", | |
"glucose": [ | |
{ | |
"time": "08:22", | |
"value": 8.49 | |
}, | |
{ | |
"time": "12:34", | |
"value": 10.1 | |
}, | |
{ | |
"time": "18:12", | |
"value": 14.99 | |
}, | |
{ | |
"time": "21:37", | |
"value": 13.88 | |
} | |
], | |
"times": "" | |
}, | |
"2016/04/24": { | |
"comments": "", | |
"glucose": [ | |
{ | |
"time": "07:52", | |
"value": 6.55 | |
}, | |
{ | |
"time": "11:55", | |
"value": 10.88 | |
}, | |
{ | |
"time": "17:18", | |
"value": 14.6 | |
}, | |
{ | |
"time": "21:41", | |
"value": 17.87 | |
} | |
], | |
"times": "" | |
}, | |
"2016/04/25": { | |
"comments": "", | |
"glucose": [ | |
{ | |
"time": "08:04", | |
"value": 7.83 | |
}, | |
{ | |
"time": "12:32", | |
"value": 9.6 | |
}, | |
{ | |
"time": "17:25", | |
"value": 17.98 | |
}, | |
{ | |
"time": "21:31", | |
"value": 13.82 | |
} | |
], | |
"times": "" | |
}, | |
"2016/04/26": { | |
"comments": "", | |
"glucose": [ | |
{ | |
"time": "08:08", | |
"value": 5.83 | |
}, | |
{ | |
"time": "12:10", | |
"value": 11.21 | |
}, | |
{ | |
"time": "17:56", | |
"value": 21.37 | |
}, | |
{ | |
"time": "22:04", | |
"value": 10.82 | |
} | |
], | |
"times": "" | |
}, | |
"2016/04/27": { | |
"comments": "", | |
"glucose": [ | |
{ | |
"time": "08:09", | |
"value": 6.11 | |
} | |
], | |
"times": "" | |
} | |
} |