Created
March 17, 2020 03:46
-
-
Save mrdon/83d9e81c50667a66b89cb6b3d797811a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var TimeKnots = { | |
draw: function(id, events, options){ | |
var cfg = { | |
width: 600, | |
height: 200, | |
radius: 10, | |
lineWidth: 2, | |
color: "#999", | |
background: "#FFF", | |
dateFormat: "%Y/%m/%d %H:%M:%S", | |
horizontalLayout: true, | |
showLabels: false, | |
labelFormat: "%Y/%m/%d %H:%M:%S", | |
addNow: false, | |
seriesColor: d3.scale.category20(), | |
dateDimension: true | |
}; | |
//default configuration overrid | |
if(options != undefined){ | |
for(var i in options){ | |
cfg[i] = options[i]; | |
} | |
} | |
if(cfg.addNow != false){ | |
events.push({date: new Date(), name: cfg.addNowLabel || "Today"}); | |
} | |
var tip = d3.select(id) | |
.append('div') | |
.style("opacity", 0) | |
.style("position", "absolute") | |
// .style("font-family", "Helvetica Neue") | |
.style("font-weight", "400") | |
.style("background","rgba(0,0,0,0.9)") | |
.style("color", "white") | |
.style("padding", "5px 10px 5px 10px") | |
.style("-moz-border-radius", "8px 8px") | |
.style("border-radius", "8px 8px"); | |
var svg = d3.select(id).append('svg').attr("width", cfg.width).attr("height", cfg.height); | |
//Calculate times in terms of timestamps | |
if(!cfg.dateDimension){ | |
var timestamps = events.map(function(d){return d.value});//new Date(d.date).getTime()}); | |
var maxValue = d3.max(timestamps); | |
var minValue = d3.min(timestamps); | |
}else{ | |
var timestamps = events.map(function(d){return Date.parse(d.date);});//new Date(d.date).getTime()}); | |
var maxValue = d3.max(timestamps); | |
var minValue = d3.min(timestamps); | |
} | |
var margin = (d3.max(events.map(function(d){return d.radius})) || cfg.radius)*1.5+cfg.lineWidth; | |
var step = (cfg.horizontalLayout)?((cfg.width-2*margin)/(maxValue - minValue)):((cfg.height-2*margin)/(maxValue - minValue)); | |
var series = []; | |
if(maxValue === minValue){step = 0;if(cfg.horizontalLayout){margin=cfg.width/2}else{margin=cfg.height/2}} | |
let slot_size = cfg.radius; | |
let num_slots = Math.floor(cfg.width / slot_size) - slot_size*1.5; | |
let events_ordered = new Array(num_slots); | |
for (idx=0; idx<events.length; idx++) { | |
let e = events[idx]; | |
let datum = (cfg.dateDimension)?new Date(e.date).getTime():e.value; | |
let x = Math.floor(step*(datum - minValue) + margin); | |
let slotidx = Math.floor((x - margin) / slot_size); | |
if (events_ordered[slotidx] === undefined) { | |
events_ordered[slotidx] = []; | |
} | |
events_ordered[slotidx].push(e); | |
} | |
// Smooth out slots left to right | |
for (idx=0; idx<events_ordered.length - 1; idx++) { | |
let stack = events_ordered[idx]; | |
if (stack !== undefined && stack.length > 1) { | |
for (sidx=stack.length - 1; sidx >= 1; sidx--) { | |
item = stack.pop(); | |
next_stack = events_ordered[idx+1]; | |
if (next_stack === undefined) { | |
next_stack = []; | |
events_ordered[idx+1] = next_stack; | |
} | |
next_stack.unshift(item); | |
} | |
} | |
} | |
// Smooth out slots right to left | |
for (idx=events_ordered.length; idx >= 1; idx--) { | |
let stack = events_ordered[idx]; | |
if (stack !== undefined && stack.length > 1) { | |
for (sidx=stack.length - 1; sidx >= 1; sidx--) { | |
item = stack.shift(); | |
next_stack = events_ordered[idx-1]; | |
if (next_stack === undefined) { | |
next_stack = []; | |
events_ordered[idx-1] = next_stack; | |
} | |
next_stack.push(item); | |
} | |
} | |
} | |
// Assign to events | |
for (idx=0; idx<events_ordered.length; idx++) { | |
let stack = events_ordered[idx]; | |
if (stack !== undefined) { | |
stack[0].slot_id = idx; | |
} | |
} | |
linePrevious = { | |
x1 : null, | |
x2 : null, | |
y1 : null, | |
y2 : null | |
}; | |
svg.selectAll("line") | |
.data(events).enter().append("line") | |
.attr("class", "timeline-line") | |
.attr("x1", function(d){ | |
var ret; | |
if(cfg.horizontalLayout){ | |
var datum = (cfg.dateDimension)?new Date(d.date).getTime():d.value; | |
ret = Math.floor(step*(datum - minValue) + margin) | |
} | |
else{ | |
ret = Math.floor(cfg.width/2) | |
} | |
linePrevious.x1 = ret | |
return ret | |
}) | |
.attr("x2", function(d){ | |
if (linePrevious.x1 != null){ | |
return linePrevious.x1 | |
} | |
if(cfg.horizontalLayout){ | |
var datum = (cfg.dateDimension)?new Date(d.date).getTime():d.value; | |
ret = Math.floor(step*(datum - minValue )) | |
} | |
return Math.floor(cfg.width/2) | |
}) | |
.attr("y1", function(d){ | |
var ret; | |
if(cfg.horizontalLayout){ | |
ret = Math.floor(cfg.height/2) | |
} | |
else{ | |
var datum = (cfg.dateDimension)?new Date(d.date).getTime():d.value; | |
ret = Math.floor(step*(datum - minValue)) + margin | |
} | |
linePrevious.y1 = ret | |
return ret | |
}) | |
.attr("y2", function(d){ | |
if (linePrevious.y1 != null){ | |
return linePrevious.y1 | |
} | |
if(cfg.horizontalLayout){ | |
return Math.floor(cfg.height/2) | |
} | |
var datum = (cfg.dateDimension)?new Date(d.date).getTime():d.value; | |
return Math.floor(step*(datum - minValue)) | |
}) | |
.style("stroke", function(d){ | |
if(d.color != undefined){ | |
return d.color | |
} | |
if(d.series != undefined){ | |
if(series.indexOf(d.series) < 0){ | |
series.push(d.series); | |
} | |
return cfg.seriesColor(series.indexOf(d.series)); | |
} | |
return cfg.color}) | |
.style("stroke-width", cfg.lineWidth); | |
svg.selectAll("circle") | |
.data(events).enter() | |
.append("circle") | |
.attr("class", "timeline-event") | |
.attr("r", function(d){if(d.radius != undefined){return d.radius} return cfg.radius}) | |
.style("stroke", function(d){ | |
if(d.color != undefined){ | |
return d.color | |
} | |
if(d.series != undefined){ | |
if(series.indexOf(d.series) < 0){ | |
series.push(d.series); | |
} | |
console.log(d.series, series, series.indexOf(d.series)); | |
return cfg.seriesColor(series.indexOf(d.series)); | |
} | |
return cfg.color} | |
) | |
.style("stroke-width", function(d){if(d.lineWidth != undefined){return d.lineWidth} return cfg.lineWidth}) | |
.style("fill", function(d){if(d.background != undefined){return d.background} return cfg.background}) | |
.attr("cy", function(d){ | |
if(cfg.horizontalLayout){ | |
return Math.floor(cfg.height/2) | |
} | |
var datum = (cfg.dateDimension)?new Date(d.date).getTime():d.value; | |
return Math.floor(step*(datum - minValue) + margin) | |
}) | |
.attr("cx", function(d,i){ | |
if(cfg.horizontalLayout){ | |
return d.slot_id * slot_size + margin; | |
} | |
return Math.floor(cfg.width/2) | |
}).on("mouseover", function(d){ | |
if(cfg.dateDimension){ | |
var format = d3.time.format(cfg.dateFormat); | |
var datetime = format(new Date(d.date)); | |
var dateValue = (datetime != "")?(d.name +" <small>("+datetime+")</small>"):d.name; | |
}else{ | |
var format = function(d){return d}; // TODO | |
var datetime = d.value; | |
var dateValue = d.name +" <small>("+d.value+")</small>"; | |
} | |
d3.select(this) | |
.style("fill", function(d){if(d.color != undefined){return d.color} return cfg.color}).transition() | |
.duration(100).attr("r", function(d){if(d.radius != undefined){return Math.floor(d.radius*1.5)} return Math.floor(cfg.radius*1.5)}); | |
tip.html(""); | |
if(d.img != undefined){ | |
tip.append("img").style("float", "left").style("margin-right", "4px").attr("src", d.img).attr("width", "64px"); | |
} | |
tip.append("div").style("float", "left").html(dateValue ); | |
tip.transition() | |
.duration(100) | |
.style("opacity", .9); | |
}) | |
.on("mouseout", function(){ | |
d3.select(this) | |
.style("fill", function(d){if(d.background != undefined){return d.background} return cfg.background}).transition() | |
.duration(100).attr("r", function(d){if(d.radius != undefined){return d.radius} return cfg.radius}); | |
tip.transition() | |
.duration(100) | |
.style("opacity", 0)}); | |
//Adding start and end labels | |
if(cfg.showLabels != false){ | |
if(cfg.dateDimension){ | |
var format = d3.time.format(cfg.labelFormat); | |
var startString = format(new Date(minValue)); | |
var endString = format(new Date(maxValue)); | |
}else{ | |
var format = function(d){return d}; //Should I do something else? | |
var startString = minValue; | |
var endString = maxValue; | |
} | |
svg.append("text") | |
.text(startString).style("font-size", "70%") | |
.attr("x", function(d){if(cfg.horizontalLayout){return d3.max([0, (margin-this.getBBox().width/2)])} return Math.floor(this.getBBox().width/2)}) | |
.attr("y", function(d){if(cfg.horizontalLayout){return Math.floor(cfg.height/2+(margin+this.getBBox().height))}return margin+this.getBBox().height/2}); | |
svg.append("text") | |
.text(endString).style("font-size", "70%") | |
.attr("x", function(d){if(cfg.horizontalLayout){return cfg.width - d3.max([this.getBBox().width, (margin+this.getBBox().width/2)])} return Math.floor(this.getBBox().width/2)}) | |
.attr("y", function(d){if(cfg.horizontalLayout){return Math.floor(cfg.height/2+(margin+this.getBBox().height))}return cfg.height-margin+this.getBBox().height/2}) | |
} | |
svg.on("mousemove", function(){ | |
tipPixels = parseInt(tip.style("height").replace("px", "")); | |
pos = d3.mouse(d3.select(id)[0][0].parentNode.parentNode.parentNode) ; | |
startX = pos[0]; | |
startY = 0; | |
posX = startX+20; | |
posY = startY-tipPixels; //-margin; | |
console.log("startY: " + startY + " tipPixes: " + tipPixels + " margin: " + margin); | |
return tip.style("top", posY+"px").style("left",posX+"px");}) | |
.on("mouseout", function(){return tip.style("opacity", 0).style("top","0px").style("left","0px").style("z-index", "-1;")}); | |
} | |
}; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment