Last active
August 29, 2015 14:26
-
-
Save cool-Blue/caaeb746e7332d5745da to your computer and use it in GitHub Desktop.
Stratega for managing objects to be only visible on the plot surface
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
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title></title> | |
<style> | |
body { | |
margin: 1px; | |
} | |
div { | |
text-align: center; | |
} | |
p { | |
text-align: left; | |
padding-left: 1em; | |
text-indent: -1em; | |
line-height: 1; | |
padding-top: .35em; | |
font-size: 12px; | |
} | |
svg { | |
outline: 1px solid red; | |
overflow: visible; | |
display: inline-block; | |
} | |
.axis path { | |
stroke: #ccc; | |
} | |
.axis line { | |
stroke: #ccc; | |
stroke-opacity: .5; | |
} | |
.axis path { | |
fill: none; | |
} | |
.axis text { | |
font-size: 8px; | |
} | |
.min-nodes .datum { | |
/*fill: url(#filter-bubble-green-white);*/ | |
fill: url(#filter-spherical-shading-black-white); | |
opacity: 1; | |
/*-webkit-filter: drop-shadow(10px 5px 5px blue);*/ | |
} | |
.min-traffic .datum.visible { | |
/*fill: url(#filter-spherical-shading);*/ | |
opacity: 1; | |
} | |
.min-traffic .datum { | |
opacity: 0; | |
} | |
</style> | |
<link rel="stylesheet" type="text/css" href="https://gitcdn.xyz/repo/cool-Blue/d3-lib/1dcef5868b3e774e8303d0c6eeaaef2528867893/inputs/button/style.css"> | |
<!--<link rel="stylesheet" type="text/css" href="inputs/button/style.css">--> | |
</head> | |
<body> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/tinycolor/1.1.2/tinycolor.min.js"></script> | |
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/filters/shadow.js"></script> | |
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/inputs/button/button.js"></script> | |
<script> | |
var size = 500, r = 1.75, n = 1000; | |
var inputDiv = d3.select("body").append("div") | |
.attr({id: "inputs"}) | |
.style({position: "absolute", margin: "20px"}) | |
.call(inputs.number, [{ | |
name: "nodeCount", | |
label: "nodes: ", | |
min:"1000", | |
max: "100,000", | |
step: "1000", | |
value: n, | |
onchange: function() { | |
dataSet = initData(+this.value); | |
quadtree = d3.geom.quadtree(dataSet); | |
minNodesRedraw(); | |
minTrafficRedraw(); | |
this.blur(); | |
} | |
}]); | |
var iFrame = {width: 960, height: 406}, | |
margin = {top: 30, right: 40, bottom: 30, left: 50}, | |
geom = {outerWidth: iFrame.width/ 2, outerHeight: 200, margin: margin}; | |
function initData(n){ | |
return d3.range(n).map(function(d, idx) { | |
return { | |
x: d3.random.normal(100 / 2, 100 / 10)(), | |
y: d3.random.normal(100 / 2, 100 / 10)(), | |
uuid: idx | |
}; | |
}); | |
} | |
var dataSet = initData(n), | |
quadtree = d3.geom.quadtree(dataSet); | |
d3.select(self.frameElement).style({height: "406px", width: [iFrame.width,"px"].join("")}); | |
function graph(selector, Owner, geom){ | |
var margin = geom.margin, | |
width = geom.outerWidth - margin.left - margin.right, | |
height = geom.outerHeight - margin.top - margin.bottom, | |
_dropShadow = true, | |
_debug = true; | |
// Init Scales | |
var xScale = d3.scale.linear() | |
.domain([0, 100]) | |
.range([0, width]) | |
.nice(10); | |
var yScale = d3.scale.linear() | |
.domain([0, 100]) | |
.range([height, 0]) | |
.nice(10); | |
// Init Axes | |
var xAxis = d3.svg.axis() | |
.scale(xScale) | |
.ticks(10) | |
.orient("bottom") | |
.tickSize(-height); | |
var yAxis = d3.svg.axis() | |
.scale(yScale) | |
.ticks(10) | |
.orient("left") | |
.tickSize(-width); | |
// Init Zoom | |
var d3Zoom = d3.behavior.zoom() | |
.x(xScale) | |
.y(yScale) | |
.scaleExtent([0.99, Infinity]) | |
.on("zoom", onZoom); | |
var root = d3.select(selector).selectAll("svg").data([geom]), | |
rootEnter = root.enter().append("svg") | |
.attr("width", geom.outerWidth) | |
.attr("height", geom.outerHeight), | |
title = textBox(root, | |
{width: width, height: margin.top/2, x: margin.left, y: 0}, | |
selector), | |
containerClass = "body-container", | |
svg = root.append("g") | |
.attr("class", ["chart-body", containerClass].join(" ")) | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")") | |
.call(d3Zoom), | |
plotSurface = svg.append("rect") | |
.attr("class", "chart-body overlay") | |
.attr("width", width) | |
.attr("height", height) | |
.style({"fill": "none", opacity: 0.8}) | |
.style("pointer-events", "all"), | |
clipRect = svg.append("clipPath") | |
.attr("id", "clip") | |
.append("rect") | |
.attr("width", width) | |
.attr("height", height), | |
circles = svg.append("g") | |
.attr("class", "chart-body data-points") | |
.style("filter", _dropShadow ? filters.drop(svg) : null) | |
.attr({"clip-path": "url(#clip)"}), | |
owner = Owner({root: root, xScale: xScale, yScale: yScale, title: title }), | |
redraw = function(data, nodes){ | |
owner.redraw(data, nodes); | |
if(_debug) getBBox(".chart-body, .axis, .tick text") | |
}, | |
bubble = Bubble(root); | |
d3Zoom.xAxis = svg.insert("g", function(){return circles.node()}) | |
.attr("class", "x axis") | |
.attr("transform", "translate(0," + height + ")") | |
.call(xAxis); | |
d3Zoom.yAxis = svg.insert("g", function(){return circles.node()}) | |
.attr("class", "y axis") | |
.call(yAxis); | |
var dropShadow = { | |
label : "drop shadow", | |
onclick: function(d) { | |
circles.style("filter", d.value ? filters.drop(svg) : null); | |
onZoom(); | |
this.blur(); | |
}, | |
value : true | |
}, | |
debugMode = { | |
label : "debug mode", | |
onclick: function(d) { | |
if(!(_debug = d.value)) getBBox(); | |
onZoom(); | |
this.blur(); | |
}, | |
value : _debug | |
}, | |
_controls = [dropShadow, debugMode], | |
controls = owner.controls && owner.controls.length ? _controls.concat(owner.controls) | |
: _controls, | |
bodyBB = getBBox("." + containerClass)[0], | |
footer = bodyBB.y + bodyBB.height, | |
controlPanel = htmlPanel(root, { | |
width: width, height: margin.top / 2, x: margin.left, y: footer | |
}) | |
.call(inputs.toggle, controls) | |
.style({"font-size": "8px", padding: "3px 0 0 3px"}); | |
owner.draw(circles); | |
onZoom(); | |
return onZoom; | |
function onZoom() { | |
//TODO fix safari zoom bug: change zoom and zoom focus is not translated | |
// Store zoom extent | |
d3Zoom.extent = [ | |
[xScale.invert(0), yScale.invert(height)], | |
[xScale.invert(width), yScale.invert(0)] | |
]; | |
d3Zoom.scaleFactor = d3.event && d3.event.scale ? d3.event.scale : 1; | |
d3Zoom.translation = d3.event && d3.event.translate ? d3.event.translate : [0, 0]; | |
// Update axes | |
d3Zoom.xAxis.call(xAxis); | |
d3Zoom.yAxis.call(yAxis); | |
// Prune non visible data | |
redraw(search(quadtree, d3Zoom.extent), circles); | |
} | |
function htmlPanel(root, attributes) { | |
return root.append("foreignObject") | |
.attr(attributes) | |
.append("xhtml:div") | |
} | |
function textBox(root, attributes, defaultText) { | |
return htmlPanel(root, attributes) | |
.append("p") | |
.text(defaultText); | |
} | |
function getBBox(types){ | |
// create rectangles in the parent svg coordinate system for | |
// selected classes of chart components | |
//todo convert geometry to svg points and apply the transform to other attributes - eliminate the transform attribute | |
// if(true) return; | |
if(!types) {root.select(".g-chart-component").remove(); return} | |
// parse the types and build an array of bounding box attributes | |
var bBoxes = [], classTag = "chart-component", | |
elements = root.selectAll(types) | |
.each(function(d, i) { | |
var el = d3.select(this), title; | |
el.attr("title", title = (el.attr("class") || el.node().tagName).replace(classTag, "")); | |
var bBox = {indx: i, title: title}; | |
bBox.relBB = d3.select(this).node().getBBox(); | |
bBox.CTM = this.getCTM(); | |
bBoxes.push(bBox) | |
}), | |
components = root.selectAll(".g-" + classTag).data([bBoxes]), | |
componentsEnter = components.enter().append("g") | |
.attr("class", "g-" + classTag), | |
boxes = components.selectAll("." + classTag).data(function(d){return d}); | |
boxes.enter().append("rect") | |
.style("pointer-events", "none"); | |
boxes.exit().remove(); | |
boxes.attr("id", function(d){ | |
return [d.title, d.indx].join("_") | |
}) | |
.classed("chart-component", true) | |
.each(function(d) { | |
d3.select(this).attr(d.relBB) | |
}) | |
.each(function(d) { | |
var svg = this.ownerSVGElement, | |
s = d3.select(this), pt = svg.createSVGPoint(); | |
pt.x = s.attr("x"); pt.y = s.attr("y"); | |
pt = pt.matrixTransform(d.CTM); | |
s.attr({x: pt.x, y: pt.y}) | |
d.bb = this.getBBox(); | |
}) | |
.style({"stroke": "green", fill: "none", opacity: 0.3}); | |
return bBoxes.map(function(d){return d.bb}); | |
} | |
} | |
var minNodes = function(config) { | |
var fill = filters.sphere(config.root, "black"); | |
config.title.text(config.title.text() + ":\tdelete nodes on zoom in, re-insert and sort nodes on zoom out") | |
function drawCircles(circles) { | |
return redraw((fill.map ? dataSet.map(fill.map) : dataSet), circles) | |
} | |
function redraw(subset, circles) { | |
// Attach new data | |
// keep the original order for object constancy | |
var elements = circles.selectAll("circle") | |
.data(subset.sort(function compareNumbers(a, b) { | |
return a.uuid - b.uuid; | |
}), function(d) { | |
return d.uuid | |
}); | |
//enter | |
elements.enter().append("circle") | |
.style("fill", fill) | |
.attr("class", "datum") | |
.attr("r", r); | |
//exit | |
elements.exit().remove(); | |
//update | |
updateSelection(elements); | |
return elements; | |
function updateSelection(elements) { | |
// some not so heavy duty stuff | |
elements.order().attr("transform", scaleData); | |
} | |
function scaleData(d) { | |
return "translate(" + [config.xScale(d.x), config.yScale(d.y)] + ")"; | |
} | |
} | |
return { | |
draw : drawCircles, | |
redraw: redraw, | |
} | |
}; | |
d3.select("body").append("div").attr("class", "min-nodes"); | |
var minNodesRedraw = graph(".min-nodes", minNodes, geom); | |
var minTraffic = function(config) { | |
var fill = filters.sphere(config.root, "black"), //init uri for CSS | |
bubble = Bubble(config.root); | |
config.title.text(config.title.text() | |
+ ":\tnodes are never deleted, excluded nodes classed invisible and disconnected from transform") | |
function drawCircles(circles) { | |
return redraw(dataSet, circles); | |
} | |
function redraw(data, circles) { | |
var circle = circles | |
.selectAll("circle") | |
.data(dataSet, function(d) { | |
return d.uuid; | |
}); | |
circle.enter().append("circle") | |
.attr("r", r) | |
.attr("class", "datum") | |
.attr("transform", ScaleData) | |
.call(bubble.call); | |
//exit | |
circle.exit().remove(); | |
markSubset(data, circle); | |
updateSelection(circle); | |
return circle; | |
function markSubset(data, nodes) { | |
var marked = nodes.data(data, function(d) { | |
return d.uuid; | |
}); | |
marked.enter(); | |
marked.classed("visible", true) | |
marked.exit().classed("visible", false) | |
} | |
function updateSelection(elements) { | |
elements | |
.select(function() { | |
return d3.select(this) | |
.classed("visible") ? this : null; | |
}) | |
.attr("transform", ScaleData); | |
} | |
} | |
function ScaleData(d) { | |
return "translate(" + [config.xScale(d.x), config.yScale(d.y)] + ")"; | |
} | |
return { | |
draw : drawCircles, | |
redraw: redraw, | |
} | |
}; | |
d3.select("body").append("div").attr("class", "min-traffic"); | |
var minTrafficRedraw = graph(".min-traffic", minTraffic, geom); | |
function Bubble(svg){ | |
var colors = d3.range(20).map(d3.scale.category20()).map(function(d){ | |
return filters.sphere(svg, d) | |
}); | |
return { | |
call: function(selection){ | |
selection.style("fill", function(){ | |
return colors[~~(Math.random()*20)] | |
}) | |
}, | |
map: function(d, i, data){ | |
d.fill = colors[~~(Math.random()*20)]; | |
}, | |
fill: function(d){return d.fill} | |
} | |
}; | |
// search quadtree | |
function search(qt, extent) { | |
var pts = [], | |
x0=extent[0][0], y0=extent[0][1], | |
x3=extent[1][0], y3=extent[1][1]; | |
qt.visit(function(node, x1, y1, x2, y2) { | |
var p = node.point; | |
if ((p) && (p.x >= x0) && (p.x <= x3) && (p.y >= y0) && (p.y <= y3)) { | |
pts.push(p); | |
} | |
return x1 >= x3 || y1 >= y3 || x2 < x0 || y2 < y0; | |
}); | |
return pts; | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment