/* 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} ] }, .... [x] color determined by sugar value - [x] option for BW and full color [x] time bubbles (x axis) auto position in y depending on number of days in record. intentionally left blank. */ var multicolor = false; 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 (multicolor){ 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 && tval < 25.0){return {bg:"#fe70bc", tx: "#ffffff"}} else if (tval >= 25.0){return {bg:"#AA70bc", tx: "#ffffff"}} } else { if (tval < 5.0){return {bg:"#FFFFFF", tx: "#050505"}} else if (tval >= 5.0 && tval < 10.0){return {bg:"#FAFAFA", tx: "#050505"}} else if (tval >= 10.0 && tval < 15.0){return {bg:"#F0F0F0", tx: "#050505"}} else if (tval >= 15.0 && tval < 20.0){return {bg:"#EAEAEA", tx: "#050505"}} else if (tval >= 20.0 && tval < 25.0){return {bg:"#E0E0E0", tx: "#050505"}} else if (tval >= 25.0){return {bg:"#DADADA", tx: "#050505"}} } } 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 +3}) } // 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"}) } // instead of appending, I want this group to be added before all existing groups. // this will ensure that the lines it draws are beneath the rest. var indicat = main_group.insert('g', ':first-child').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 time_index = 0; function set_time_bubbles(){ return yindex * vertical_skip + 20; } 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+50) .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', set_time_bubbles()) .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), set_time_bubbles() + time_offset_down ] + ')') .style({'fill': "#7c7c7c"}) .text(tick.slice(0,2)) } time_index += 1; } }