Skip to content

Instantly share code, notes, and snippets.

@mrdon
Created March 17, 2020 03:46
Show Gist options
  • Save mrdon/83d9e81c50667a66b89cb6b3d797811a to your computer and use it in GitHub Desktop.
Save mrdon/83d9e81c50667a66b89cb6b3d797811a to your computer and use it in GitHub Desktop.
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