|
var svg, background, g, defs, div, data, w = 800, h = 600, dist, offset = 64, vratio = 1.0, radius = 18, zAxis, zValues, ratio = 0.3814, stacksData = [], filter = false, filtered = [true, true, true, true, true]; |
|
|
|
var days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; //could be German or whatever |
|
var states = ["suspended", "pending", "normal", "urgent", "critical"]; |
|
var maxStack = Number.NEGATIVE_INFINITY; |
|
|
|
var colors = ["#101C32", "#18607D", "#F7EDCC", "#F99613", "#CA391B"]; |
|
|
|
var scale = 20.0; |
|
|
|
var zl = { x: -11.070 * scale, y: -11.993 * scale }, |
|
zm = { x: 0.0 * scale, y: -14.392 * scale }, |
|
zr = { x: 12.749 * scale, y: -12.466 * scale }, |
|
tp = { x: 0.0, y: 0.0 }, |
|
rp = { x: 12.749 * scale, y: 5.044 * scale }, |
|
bp = { x: 2.887 * scale, y: 14.904 * scale }, |
|
lp = { x: -11.070 * scale, y: 6.277 * scale }; |
|
|
|
var HUD = function() { |
|
|
|
this.radius = 18; |
|
this.vratio = 1.0; |
|
|
|
}; |
|
|
|
d3.legend = function(){ |
|
|
|
function legend(selection_){ |
|
|
|
selection_.each(function(d_, i_) { |
|
|
|
var g = d3.select(this).attr("transform", "translate(" + d_.x + "," + d_.y + ")") |
|
var back = g.append("rect") |
|
.attr("width", d_.w) |
|
.attr("height", d_.h) |
|
.attr("fill", "#DEDEDE") |
|
.attr("opacity", 0.5); |
|
|
|
var legendlabels = g.selectAll(".legendlabel") |
|
.data(d_.states) |
|
.enter() |
|
.append("text") |
|
.attr("class", function(d2_, j_){ return "legendText_" + j_; }) |
|
.attr("dx", 40) |
|
.attr("dy", function(d2_, j_){ return 134 - j_ * 25; }) |
|
.text(function(d_){ return d_; }) |
|
|
|
var boxes = g.selectAll(".box") |
|
.data(d_.states) |
|
.enter() |
|
.append("rect") |
|
.attr("class", function(d2_, j_){ return "legendRect_" + j_; }) |
|
.attr("x", 20) |
|
.attr("y", function(d2_, j_){ return 125 - j_ * 25; }) |
|
.attr("width", 12) |
|
.attr("height", 12) |
|
.attr("fill", function(d2_, j_){ return colors[j_]; }); |
|
|
|
var placeholder = g.selectAll(".box") |
|
.data(d_.states) |
|
.enter() |
|
.append("rect") |
|
.attr("x", 10) |
|
.attr("y", function(d2_, j_){ return 120 - j_ * 25; }) |
|
.attr("width", 108) |
|
.attr("height", 22) |
|
.attr("fill", "transparent") |
|
.on("click", function(d2_, j_){ |
|
|
|
filtered[j_] = !filtered[j_]; |
|
if(filtered[j_]){ |
|
|
|
d3.select(".legendText_" + j_).attr("fill", "#000000"); |
|
d3.select(".legendRect_" + j_).attr("fill", colors[j_]); |
|
drawStacks(); |
|
|
|
}else{ |
|
|
|
d3.select(".legendText_" + j_).attr("fill", "#808080"); |
|
d3.select(".legendRect_" + j_).attr("fill", "#808080"); |
|
drawStacks(); |
|
|
|
} |
|
|
|
}) |
|
|
|
}); |
|
|
|
} |
|
|
|
return legend; |
|
} |
|
|
|
d3.diagonalAxis = function(){ |
|
|
|
function diagonalAxis(selection_){ |
|
|
|
selection_.each(function(d_, i_) { |
|
|
|
var g = d3.select(this); |
|
g.append("line").attr("x1", d_.start.x).attr("y1", d_.start.y).attr("x2", d_.end.x).attr("y2", d_.end.y).attr("stroke", "#000000"); |
|
|
|
var tline = offsetLine(d_, d_.offset); |
|
var ticksData = setTicks(d_.start, d_.end, tline[0], tline[1], d_.labels.length + 1); |
|
|
|
var ticks = g.selectAll(".tick") |
|
.data(ticksData) |
|
.enter() |
|
.append("line") |
|
.attr("x1", function(d_) { return d_.x1; }) |
|
.attr("y1", function(d_) { return d_.y1; }) |
|
.attr("x2", function(d_) { return d_.x2; }) |
|
.attr("y2", function(d_) { return d_.y2; }) |
|
.attr("stroke", "#000000") |
|
|
|
var tline0 = offsetLine(d_, d_.textOffset); |
|
var tline1 = offsetLine(d_, d_.textRegion); |
|
|
|
var labelsData = setLabels(d_, tline0, tline1); |
|
|
|
var paths = g.selectAll(".labelpath") |
|
.data(labelsData) |
|
.enter() |
|
.append("path") |
|
.attr("id", function(d2_, i_){ return "labelPath" + d_.id + "_" + i_ }) |
|
.attr("d", function(d_) { return d_.path; }) |
|
.attr("stroke", "none"); |
|
|
|
var clickme = g.selectAll(".clickme") |
|
.data(labelsData) |
|
.enter() |
|
.append("circle") |
|
.attr("cx", function(d2_){ return getPathMedian(d2_, d_.placeholders).x; }) |
|
.attr("cy", function(d2_){ return getPathMedian(d2_, d_.placeholders).y; }) |
|
.attr("r", 16) |
|
.attr("fill", "transparent") |
|
.attr("stroke", "none") |
|
.on("click", function(d_){ |
|
|
|
filter = true; |
|
d3.selectAll(".cylinder").attr("opacity", 0.0); |
|
d3.selectAll(".day_" + d_.l).attr("opacity", 1.0); |
|
d3.selectAll(".age_" + d_.l).attr("opacity", 1.0); |
|
|
|
}) |
|
.on("mouseover", function(d_){ |
|
|
|
if(!filter){ |
|
|
|
d3.selectAll(".cylinder").attr("opacity", 0.0); |
|
d3.selectAll(".day_" + d_.l).attr("opacity", 1.0); |
|
d3.selectAll(".age_" + d_.l).attr("opacity", 1.0); |
|
|
|
} |
|
|
|
}) |
|
.on("mouseout", function(d_){ |
|
|
|
if(!filter) { d3.selectAll(".cylinder").attr("opacity", 1.0); } |
|
|
|
}) |
|
|
|
var labels = g.selectAll(".label") |
|
.data(labelsData) |
|
.enter() |
|
.append("text") |
|
.attr("pointer-events", "none") |
|
.append("textPath") |
|
.attr("xlink:href", function(d2_, i_){ return "#labelPath" + d_.id + "_" + i_; }) |
|
.text(function(d_){ return d_.l; }) |
|
|
|
|
|
}); |
|
|
|
} |
|
|
|
function getPathMedian(d_, placeholders_){ |
|
|
|
var coords = d_.path.replace(/M|L/g, "").split(" "); |
|
return {x: lerp1D(coords[0], coords[2], placeholders_.x), y: lerp1D(coords[1], coords[3], placeholders_.y) }; |
|
|
|
} |
|
|
|
function offsetLine(d_, offset_){ |
|
|
|
var l = Math.sqrt(Math.pow(d_.end.x - d_.start.x, 2) + Math.pow(d_.end.y - d_.start.y, 2)); |
|
var delta = 1.0 + offset_ / l; |
|
var p0 = lerp2D(d_.start, d_.startTo, delta); |
|
var p1 = lerp2D(d_.end, d_.endTo, delta); |
|
|
|
return [p0, p1]; |
|
} |
|
|
|
|
|
function setTicks(s0_, e0_, s1_, e1_, steps_){ |
|
|
|
var out = []; |
|
|
|
for(var i = 1; i < steps_; i++){ |
|
|
|
var inc = 1.0 / steps_; |
|
|
|
var v0 = lerp2D(s0_, e0_, inc * i); |
|
var v1 = lerp2D(s1_, e1_, inc * i); |
|
|
|
out.push({x1: v0.x, y1: v0.y, x2: v1.x, y2: v1.y }); |
|
|
|
} |
|
|
|
return out; |
|
|
|
} |
|
|
|
function setLabels(d_, v0_, v1_, ){ |
|
|
|
steps = d_.labels.length + 1; |
|
|
|
var out = []; |
|
|
|
for(var i = 1; i < steps; i++){ |
|
|
|
var inc = 1.0 / steps; |
|
|
|
if(d_.id == "X"){ |
|
var dv0 = lerp2D(v1_[0], v1_[1], inc * i); |
|
var dv1 = lerp2D(v0_[0], v0_[1], inc * i); |
|
} |
|
else{ |
|
|
|
var dv1 = lerp2D(v1_[0], v1_[1], inc * i); |
|
var dv0 = lerp2D(v0_[0], v0_[1], inc * i); |
|
|
|
} |
|
|
|
out.push({l: d_.labels[i - 1], path: "M" + dv0.x + " " + dv0.y + " L" + dv1.x + " " + dv1.y }); |
|
|
|
} |
|
|
|
return out; |
|
|
|
} |
|
|
|
return diagonalAxis; |
|
|
|
} |
|
|
|
d3.stack = function(){ |
|
|
|
function stack(selection_){ |
|
|
|
selection_.each(function(d_, i_) { |
|
|
|
var xy = getGridPosition(d_.x, d_.y); |
|
|
|
var g = d3.select(this) |
|
.attr("transform", "translate(" + xy.x + "," + xy.y + ")"); |
|
|
|
var floor = 0; |
|
|
|
for(var j = 0; j < d_.stack.length; j++){ |
|
|
|
if(filtered[j] && d_.stack[j] > 0){ |
|
|
|
var vertices = generate_cylinder(0, floor, radius * d_.srx * d_.sry, d_.stack[j] * vratio); |
|
|
|
var cylinder = g.append("g").attr("class", "cylinder day_" + d_.d + " age_" + d_.a); |
|
|
|
cylinder.append("path") |
|
.attr("class", "top") |
|
.attr("id", d_.stack[j] + " " + states[j] + " tickets made on " + d_.d + ", " + d_.a + " days old") |
|
.attr("d", function(d_) { return base_generator(vertices); }) |
|
.attr("stroke", "#FFFFFF") |
|
.attr("stroke-opacity", 0.5) |
|
.attr("fill", "url(#gradient_" + j + ")") |
|
.on("mouseover", function(d2_) { |
|
|
|
div.transition() |
|
.duration(500) |
|
.style("opacity", .9); |
|
div.html(d3.select(this).attr("id")) |
|
.style("left", (d3.event.pageX) + "px") |
|
.style("top", (d3.event.pageY - 28) + "px"); |
|
|
|
}) |
|
.on("mouseout", function(d2_) { |
|
|
|
div.transition() |
|
.duration(500) |
|
.style("opacity", 0); |
|
|
|
}); |
|
|
|
cylinder.append("path") |
|
.attr("class", "top") |
|
.attr("id", d_.stack[j] + " " + states[j] + " tickets made on " + d_.d + ", " + d_.a + " days old") |
|
.attr("d", function(d_) { return cap_generator(vertices); }) |
|
.attr("stroke", "#FFFFFF") |
|
.attr("stroke-opacity", 0.5) |
|
.attr("fill", colors[j]) |
|
.on("mouseover", function(d2_) { |
|
|
|
div.transition() |
|
.duration(500) |
|
.style("opacity", .9); |
|
div.html("value: " + d3.select(this).attr("id")) |
|
.style("left", (d3.event.pageX) + "px") |
|
.style("top", (d3.event.pageY - 28) + "px"); |
|
|
|
}) |
|
.on("mouseout", function(d2_) { |
|
|
|
div.transition() |
|
.duration(500) |
|
.style("opacity", 0); |
|
|
|
}); |
|
|
|
floor -= d_.stack[j] * vratio; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
} |
|
|
|
function getGridPosition(x_, y_){ |
|
|
|
var gx = d3.select("#gridX_" + x_); |
|
var gy = d3.select("#gridY_" + y_); |
|
|
|
return twoLineIntersection([{x: Number(gx.attr("x1")), y: Number(gx.attr("y1"))}, |
|
{x: Number(gx.attr("x2")), y: Number(gx.attr("y2"))}], |
|
[{x: Number(gy.attr("x1")), y: Number(gy.attr("y1"))}, |
|
{x: Number(gy.attr("x2")), y: Number(gy.attr("y2"))}]); |
|
|
|
} |
|
|
|
function twoLineIntersection(a_, b_){ |
|
|
|
ma = (a_[0].y - a_[1].y) / (a_[0].x - a_[1].x); |
|
mb = (b_[0].y - b_[1].y) / (b_[0].x - b_[1].x); |
|
|
|
return { x: (ma * a_[0].x - mb * b_[0].x + b_[0].y - a_[0].y) / (ma - mb), y: (ma * mb * (b_[0].x - a_[0].x) + mb * a_[0].y - ma * b_[0].y) / (mb - ma)}; |
|
|
|
} |
|
|
|
function generate_cylinder(x_, y_, width_, height_){ |
|
|
|
var out; |
|
|
|
var base = [[x_, y_ + width_ / 2 * ratio], [x_ + width_ / 2, y_], [x_, y_ - width_ / 2 * ratio], [x_ - width_ / 2, y_]]; |
|
var top = [[x_, y_ + width_ / 2 * ratio - height_], [x_ + width_ / 2, y_ - height_], [x_, y_ - width_ / 2 * ratio - height_], [x_ - width_ / 2, y_ - height_]]; |
|
|
|
return [base, top]; |
|
|
|
} |
|
|
|
function cap_generator(d_) { |
|
|
|
var rx = (d_[1][1][0] - d_[1][3][0]) / 2; |
|
var ry = (d_[1][2][1] - d_[1][0][1]) / 2; |
|
str = "M" + d_[1][3].join(" ") + "A" + rx + " " + ry + ", 0, 0 1," + d_[1][1].join(" "); |
|
str += "A" + rx + " " + ry + ", 0, 0 1," + d_[1][3].join(" ") |
|
return str; |
|
|
|
|
|
}; |
|
|
|
function base_generator(d_) { |
|
|
|
var rx = (d_[0][1][0] - d_[0][3][0]) / 2; |
|
var ry = (d_[0][2][1] - d_[0][0][1]) / 2; |
|
str = "M" + d_[0][1].join(" ") + "A" + rx + " " + ry + ", 0, 0 1," + d_[0][3].join(" "); |
|
str += " L" + d_[1][3].join(" "); |
|
str += " L" + d_[1][1].join(" "); |
|
str += " L" + d_[0][1].join(" "); |
|
return str; |
|
|
|
|
|
}; |
|
|
|
|
|
return stack; |
|
|
|
} |
|
|
|
d3.json("mockup.json", function(error_, data_) { |
|
|
|
if (error_) throw error_; |
|
data = data_; |
|
parseData(data_); |
|
|
|
inits(); |
|
|
|
}); |
|
|
|
function inits(){ |
|
|
|
console.log("%cWeekly Ticket Stacks β demo", "color: #494949; font-size: 18px; font-family: sans-serif;"); |
|
console.log("%cby Vladimir V KUCHINOV", "color: #494949; font-size: 12px; font-style: italic;font-family: sans-serif;"); |
|
|
|
var menu = new HUD(); |
|
var gui = new dat.GUI(); |
|
gui.add(menu, "radius", 5, 24, 0.5).onChange(function(value_) { radius = value_, drawStacks(); }); |
|
gui.add(menu, "vratio", 0.1, 1.0, 0.05).onChange(function(value_) { zValues.domain([0, 350 / value_]); zAxis.call(d3.axisRight(zValues)); vratio = value_, drawStacks() }); |
|
|
|
svg = d3.select("body").append("svg") |
|
.attr("preserveAspectRatio", "xMinYMin meet") |
|
.attr("viewBox", "0 0 " + w + " " + h) |
|
.classed("svg-content", true); |
|
|
|
|
|
background = svg.append("rect").attr("width", w).attr("height", h).attr("fill", "transparent").on("click", function(){ filter = false, d3.selectAll(".cylinder").attr("opacity", 1.0); }); |
|
defs = svg.append("defs"); |
|
|
|
div = d3.select("body").append("div") |
|
.attr("class", "tooltip") |
|
.style("opacity", 0); |
|
|
|
generateGradients(5); //while current data has 5 stacked values |
|
|
|
g = svg.append("g").attr("transform", "translate(" + w/2 + "," + h/2 + ")"); |
|
//dist = distance(w/2 - rx, h/2, w/2, h/2 - ry); |
|
//ceilDist = 350; |
|
|
|
var xdays = d3.scaleLinear().range([0, dist]).domain([0, 6]); |
|
|
|
//x min |
|
g.append("line").attr("x1", lp.x).attr("y1", lp.y).attr("x2", zl.x).attr("y2", zl.y).attr("stroke", "#DEDEDE"); |
|
|
|
//y min |
|
g.append("line").attr("x1", lp.x).attr("y1", lp.y).attr("x2", tp.x).attr("y2", tp.y).attr("stroke", "#DEDEDE"); |
|
|
|
drawGrid(tp, rp, bp, lp, days.length + 1, days.length + 1, "#DEDEDE"); |
|
drawGrid(zm, zr, rp, tp, 8, 7, "#DEDEDE"); |
|
drawGrid(zl, zm, tp, lp, 8, 7, "#DEDEDE"); |
|
|
|
zValues = d3.scaleLinear().range([0, -(rp.y - zr.y)]).domain([0, 350]); |
|
|
|
zAxis = g.append("g") |
|
.attr("transform", "translate(" + (rp.x - 0.5) + "," + rp.y +")") |
|
.call(d3.axisRight(zValues)); |
|
|
|
var xLabels = g.selectAll(".xLabels") |
|
.data([{ |
|
|
|
id: "X", |
|
labels: days.reverse(), |
|
start: lp, |
|
startTo: tp, |
|
end: bp, |
|
endTo: rp, |
|
offset: 8, |
|
textOffset: 12, |
|
textRegion: 40, |
|
textRotation : 0, |
|
placeholders : { x: 0.7, y: 0.35 } |
|
|
|
}, { |
|
|
|
id: "Y", |
|
labels: [1, 2, 3, 4, 5, 6, 7], |
|
start: rp, |
|
startTo: tp, |
|
end: bp, |
|
endTo: lp, |
|
offset: 8, |
|
textOffset: 20, |
|
textRegion: 48, |
|
textRotation : 30, |
|
placeholders: {x: 0.9, y: 1.15} |
|
|
|
}]) |
|
.enter().append("g") |
|
.attr("id", "xLabels") |
|
.call(d3.diagonalAxis()); |
|
|
|
drawStacks(); |
|
|
|
var legend = svg.selectAll(".legendBox") |
|
.data([{states: states, x: 32, y: 32, w: 128, h: 160 }]) |
|
.enter().append("g") |
|
.attr("class", "legendBox") |
|
.call(d3.legend()); |
|
|
|
|
|
} |
|
|
|
function drawStacks(){ |
|
|
|
d3.selectAll(".stack").remove(); |
|
|
|
var stacks = g.selectAll(".stack") |
|
.data(JSON.parse(JSON.stringify(stacksData))) |
|
.enter().append("g") |
|
.attr("class", "stack") |
|
.call(d3.stack()); |
|
|
|
} |
|
|
|
function parseData(data_){ |
|
|
|
var days = Object.keys(data_); |
|
|
|
days.forEach(function(d_, i_){ |
|
|
|
data_[d_].forEach(function(a_, j_){ |
|
|
|
stacksData.push({ x: 7 - i_ , y: 7 - j_, d: d_, a: a_.age, stack: a_.stack, vrx: getRatio(7 - i_, 9, zm, zr, tp, rp), vry: getRatio(7 - i_, 9, zl, zm, lp, tp), srx : getRatio(7 - i_, 9, tp, rp, lp, bp), sry : getRatio(7 - i_, 9, lp, tp, bp, rp) }) |
|
maxStack = Math.max(maxStack, a_.stack.reduce((a, b) => a + b, 0)); |
|
|
|
}) |
|
|
|
}); |
|
|
|
} |
|
|
|
function getRatio(pos_, n_, f0_, f1_, n0_, n1_){ |
|
|
|
var fd = distance2D(f0_, f1_); //far distance |
|
var nd = distance2D(n0_, n1_); //near distance |
|
|
|
var axisRatio = fd / nd / 1.05; |
|
var step = 1.0 / (n_ + 1); |
|
|
|
return lerp1D(axisRatio, 1.0, step * pos_); |
|
|
|
} |
|
|
|
function drawGrid(t_, r_, b_, l_, stepsX_, stepsY_, color_){ |
|
|
|
for(var i = 0; i < stepsX_; i++){ |
|
|
|
var inc = 1.0 / stepsX_; |
|
|
|
g.append("line") |
|
.attr("id", "gridX_" + i) |
|
.attr("x1", lerp2D(t_, r_, inc * i).x) |
|
.attr("y1", lerp2D(t_, r_, inc * i).y) |
|
.attr("x2", lerp2D(l_, b_, inc * i).x) |
|
.attr("y2", lerp2D(l_, b_, inc * i).y) |
|
.attr("stroke", color_) |
|
|
|
} |
|
|
|
for(var i = 0; i < stepsY_; i++){ |
|
|
|
var inc = 1.0 / stepsY_; |
|
|
|
g.append("line") |
|
.attr("id", "gridY_" + i) |
|
.attr("x1", lerp2D(l_, t_, inc * i).x) |
|
.attr("y1", lerp2D(l_, t_, inc * i).y) |
|
.attr("x2", lerp2D(b_, r_, inc * i).x) |
|
.attr("y2", lerp2D(b_, r_, inc * i).y) |
|
.attr("stroke", color_); |
|
|
|
|
|
} |
|
|
|
} |
|
|
|
function generateGradients(n_){ |
|
|
|
for(var i = 0; i < n_; i++){ |
|
|
|
var gradient = defs.append("linearGradient") |
|
.attr("gradientTransform", "rotate(0)") |
|
.attr("id", "gradient_" + i); |
|
|
|
gradient.append("stop") |
|
.attr("offset", "0%") |
|
.attr("stop-color", d3.rgb(colors[i]).darker(0.5)); |
|
|
|
gradient.append("stop") |
|
.attr("offset", "12%") |
|
.attr("stop-color", d3.rgb(colors[i]).darker(1.5)); |
|
|
|
gradient.append("stop") |
|
.attr("offset", "65%") |
|
.attr("stop-color", d3.rgb(colors[i])); |
|
|
|
gradient.append("stop") |
|
.attr("offset", "100%") |
|
.attr("stop-color", d3.rgb(colors[i]).darker(0.25)); |
|
|
|
} |
|
|
|
} |
|
|
|
function average1D(v0_, v1_){ return (Number(v0_) + Number(v1_)) / 2.0; } |
|
|
|
function remapFloat(v_, min0_, max0_, min1_, max1_) { |
|
|
|
return min1_ + (v_ - min0_) / (max0_ - min0_) * (max1_ - min1_); |
|
|
|
} |
|
|
|
function distance(x0_, y0_, x1_, y1_){ |
|
|
|
return Math.sqrt(Math.pow(x1_ - x0_, 2) + Math.pow(y1_ - y0_, 2)); |
|
|
|
} |
|
|
|
function distance2D(v0_, v1_){ |
|
|
|
return Math.sqrt(Math.pow(v1_.x - v0_.x, 2) + Math.pow(v1_.y - v0_.y, 2)); |
|
|
|
} |
|
|
|
function lerp2D(v0_, v1_, t_){ |
|
|
|
return { x: v0_.x * t_ + (1.0 - t_) * v1_.x, y: v0_.y * t_ + (1.0 - t_) * v1_.y }; |
|
} |
|
|
|
function lerp1D(f0_, f1_, t_){ return f0_ * t_ + (1.0 - t_) * f1_; } |