The visualization demonstrates the use of an embedded Canvas in an SVG element. While this example includes only a single SVG element, an embedded canvas may make sense when a number of SVG elements needs to be generated, with each requiring to render large datasets that would othwewise overwhelm the DOM.
In this example, up to 50,000 elements can be generated and visualized in the embedded canvas with no impact to the DOM (other than creating the canvas). Mouse hit detection of data elements is performed with a 4x4 pixel zone under the current mouse position. While the example is admittedly a bit of a Rube Goldberg kind of project, it nevertheless attempts to demonstrate the following concepts:
forked from boeric's block: Embedded Canvas in SVG
xxxxxxxxxx
<html lang="en">
<head>
<meta charset="utf-8">
<title>Canvas in SVG</title>
<!-- Author: Bo Ericsson, bo@boe.net -->
<link rel=stylesheet type=text/css href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.3.2/css/bootstrap.min.css" media="all">
<!--<link rel=stylesheet type=text/css href="../_lib/bootstrap.min.css" media="all">-->
<style>
body {
margin: 0px;
padding: 10px;
width: 920px;
height: 400px;
}
.well {
margin-bottom: 10px;
padding: 8px 12px;
}
h4 {
margin: 0px
}
svg {
border: 1px solid #E6AAAA; /*#e3e3e3 = bootstrap .well look*/;
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0,0,0,0.05);
display: inline;
vertical-align: top;
}
svg .bg {
fill: #F5F5F5
}
button.btn {
width: 100%;
margin-bottom: 10px;
}
pre {
background-color: white;
border: none;
font-size: 10px;
overflow: scroll;
}
.control {
width: 295px;
overflow-x: scroll;
padding-left: 20px;
}
#container {
overflow: hidden;
}
label {
font-size: 12px;
margin-top: 15px;
}
input[type=range] {
display: block;
width: 100%;
}
input[type=checkbox],
input[type=radio] {
vertical-align: top;
margin-left: 5px;
}
text {
font-size: 12px;
font-family: "Arial"
}
.axis text {
font-family: "Arial";
/*font-size: 12px;*/
}
.axis path,
.axis line {
fill: none;
stroke: gray;
shape-rendering: crispEdges;
}
.sampleOption label {
display: inline-block;
}
.dataBar {
fill: lightgray;
}
</style>
<body>
<div class="well">
<h4>Canvas in SVG (with up to 50K data points)</h4>
</div>
<div id="container"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
<!--<script src="../_lib/d3.min.js"></script>-->
<script>
'use strict';
// dimensions
var svgDim = { width: 600, height: 350 };
var margin = { x: 10, y: 10 };
var canvasDim = { width: svgDim.height - margin.x * 2, height: svgDim.height - margin.y * 2 };
var groupWidth = 200;
var barHeight = 18;
var barPadding = 3;
// samples
var samplesOptions = [
{ value: 0, samples: 5, display: "5", pointScaleDomain: [0, 3], opacity: 1.0 },
{ value: 1, samples: 50, display: "50", pointScaleDomain: [0, 5], opacity: 0.8 },
{ value: 2, samples: 500, display: "500", pointScaleDomain: [0, 10], opacity: 0.5 },
{ value: 3, samples: 5000, display: "5K", pointScaleDomain: [0, 100], opacity: 0.2 },
{ value: 4, samples: 50000, display: "50K", pointScaleDomain: [0, 1000], opacity: 0.1 }
];
var samplesIdx = 2;
var samplesDefault = samplesOptions[samplesIdx].samples;
var samplesCurrent = samplesDefault;
// other defaults
var opacityDefault = 0.7;
var opacityCurrent = opacityDefault;
var correlationDefault = 0.6;
var correlationCurrent = correlationDefault;
// svg variables
var xScale;
var yScale;
var pearsonCorrelation;
var pointBarGroup;
var pointAxis;
var pointAxisGroup;
var pointScale;
// other
var container = d3.select("#container");
var opacityRangeControl;
var opacityLabel;
var canvasColor = false;
var format = d3.format(",d");
var posY = 0;
var d3randomNormal = d3.random.normal(0, 1);
var useD3randomNormal = false;
var data = [];
// create svg and group
var svg = container.append("svg")
.attr("width", svgDim.width + "px")
.attr("height", svgDim.height + "px")
.append("g");
// background
svg.append("rect")
.attr("x", svgDim.x)
.attr("y", svgDim.y)
.attr("width", svgDim.width)
.attr("height", svgDim.height)
.attr("class", "bg");
// add foreign object to svg
// https://gist.github.com/mbostock/1424037
var foreignObject = svg.append("foreignObject")
.attr("x", margin.x)
.attr("y", margin.y)
.attr("width", canvasDim.width)
.attr("height", canvasDim.height);
// add embedded body to foreign object
var foBody = foreignObject.append("xhtml:body")
.style("margin", "0px")
.style("padding", "0px")
.style("background-color", "none")
.style("width", canvasDim.width + "px")
.style("height", canvasDim.height + "px")
.style("border", "1px solid lightgray");
// add embedded canvas to embedded body
var canvas = foBody.append("canvas")
.attr("x", 0)
.attr("y", 0)
.attr("width", canvasDim.width)
.attr("height", canvasDim.height)
.style("cursor", "crosshair")
.on("mousemove", function() {
var pos = {
x: d3.mouse(this)[0],
y: d3.mouse(this)[1]
};
pos.minX = pos.x - 2;
pos.maxX = pos.x + 2;
pos.minY = pos.y - 2;
pos.maxY = pos.y + 2;
// hit detection
var matches = data.filter(function(d) {
if (d.x >= pos.minX && d.x <= pos.maxX &&
d.y >= pos.minY && d.y <= pos.maxY) {
return d;
}
});
var out = "Points under mouse: " + matches.length + "\n";
matches.forEach(function(d) {
out += "Id: " + d.id + ", x: " + d.x + ", y: " + d.y + ", color: " + d.color + "\n";
});
// output mouse hit details
d3.select("pre").text(out);
// cap point bar to current domain + 5% (which will slightly overflow pointBar scale)
var hits = matches.length;
var max = samplesOptions[samplesIdx].pointScaleDomain[1];
if (hits > max) {
hits = max * 1.05;
};
// update svg
pointBarGroup.selectAll("rect")
.data([hits])
.attr("width", function(d) { return pointScale(d) });
});
// get drawing context of canvas
var ctx = canvas.node().getContext("2d");
// add svg elements
// add svg identifier
svg.append("text")
.attr({ "x": svgDim.width - 180, "y": 20 })
.text("SVG with embedded Canvas");
// create group to hold viz elements
var group = svg.append("g")
.attr("transform", "translate(370, 50)");
// add data extent bars
group.append("text")
.attr({ x: 0, y: posY })
.text("Data extent");
posY += 15;
var extentScale = d3.scale.linear()
.domain([-5, 5])
.range([0, groupWidth])
var extentAxis = d3.svg.axis()
.scale(extentScale)
.orient("bottom")
.outerTickSize([-(barHeight + barPadding) * 2])
var initialData = [
{ name: "x", min: -1, max: 3 },
{ name: "y", min: -4, max: 4 }
];
var extentBarGroup = group.append("g")
.attr("transform", "translate(0," + posY + ")");
var extentBarUpdateSel = extentBarGroup.selectAll("rect")
.data(initialData);
extentBarUpdateSel
.enter().append("rect")
.attr("class", "dataBar")
.attr("x", function(d) { return extentScale(d.min) })
.attr("y", function(d, i) { return i * (barHeight + 3) })
.attr("width", function(d) { return extentScale(d.max) - extentScale(d.min) })
.attr("height", barHeight);
extentBarUpdateSel
.enter().append("text")
.attr("x", -13)
.attr("y", function(d, i) { return i * (barHeight +3) + 12 })
.text(function(d) { return d.name });
extentBarGroup.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + 2 * (barHeight + barPadding) + ")")
.call(extentAxis);
posY += 2 * (barHeight + barPadding) + 50;
// add computed correlation bar
group.append("text")
.attr({ x: 0, y: posY })
.text("Computed correlation");
posY += 15;
var correlScale = d3.scale.linear()
.domain([0, 1])
.range([0, groupWidth]);
var correlAxis = d3.svg.axis()
.scale(correlScale)
.orient("bottom")
.outerTickSize([-(barHeight + 3)])
.tickValues([0, 0.2, 0.4, 0.6, 0.8, 1]);
var correlBarGroup = group.append("g")
.attr("transform", "translate(0," + posY + ")");
var correlBar = correlBarGroup.selectAll("rect")
.data([1])
.enter().append("rect")
.attr("class", "dataBar")
.attr("x", 0)
.attr("y", 0)
.attr("width", function(d) { return correlScale(d) })
.attr("height", barHeight);
correlBarGroup.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (barHeight + barPadding) + ")")
.call(correlAxis);
posY += (barHeight + barPadding) + 50;
// add mouse hit bar
group.append("text")
.attr({ x: 0, y: posY })
.text("Current points under mouse");
posY += 15;
pointScale = d3.scale.linear()
.domain(samplesOptions[samplesIdx].pointScaleDomain)
.range([0, groupWidth]);
pointAxis = d3.svg.axis()
.scale(pointScale)
.orient("bottom")
.outerTickSize([-(barHeight + 3)])
.ticks([5]);
pointBarGroup = group.append("g")
.attr("transform", "translate(0," + posY + ")");
var pointBar = pointBarGroup.selectAll("rect")
.data([0])
.enter().append("rect")
.attr("class", "dataBar")
.attr("x", 0)
.attr("y", 0)
.attr("width", function(d) { return pointScale(d) })
.attr("height", barHeight);
pointAxisGroup = pointBarGroup.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (barHeight + barPadding) + ")")
.call(pointAxis);
function updatePointScale() {
pointScale.domain(samplesOptions[samplesIdx].pointScaleDomain);
pointAxisGroup.call(pointAxis);
}
// add ui controls
// add update button
var controlPanel = container.append("div")
.style("display", "inline-block")
.attr("class", "control");
controlPanel.append("button")
.attr("class", "btn btn-primary btn-small")
.text("Generate New Data")
.on("click", function() {
this.blur();
generateData();
updateViz();
});
// add samples radio buttons
var newSamplesLabel = controlPanel.append("label")
.text("Number of samples");
controlPanel.append("div")
.style("margin-top", "-20px")
.selectAll(".sampleOption")
.data(samplesOptions)
.enter().append("span")
.style("margin-right", "25px")
.append("label")
.attr("class", "sampleOption")
.style("display", "inline-block")
.text(function(d) { return d.display })
.append("input")
.attr({ type: "radio", class: "radio", name: "samples" })
.attr("value", function(d, i) { return d.value })
.property("checked", function(d) {
return d.samples === samplesCurrent ? true : false;
})
.on("change", function() {
// get index to samplesOptions
samplesIdx = +d3.select('input[name="samples"]:checked').node().value;
// update samples count
samplesCurrent = samplesOptions[samplesIdx].samples;
// update opacity variable and opacity range control
opacityCurrent = samplesOptions[samplesIdx].opacity;
opacityRangeControl.property("value", Math.round(opacityCurrent * 100));
opacityLabel.text("Opacity (currently " + Math.round((opacityCurrent * 100)) + "% )");
// update viz
updatePointScale();
generateData();
updateViz();
});
// add opacity range control
opacityLabel = controlPanel.append("label")
.text("Opacity (currently " + (opacityCurrent * 100) + "% )");
opacityRangeControl = controlPanel.append("input")
.attr({ "type": "range", "min": 0, "max": 100, "step": 1 })
.attr("value", Math.round(opacityCurrent * 100))
.on("input", function() {
var value = d3.select(this).property("value");
opacityCurrent = value / 100;
opacityLabel.text("Opacity (currently " + Math.round((opacityCurrent * 100)) + "% )");
updateViz();
});
// add correlation range control
var correlationLabel = controlPanel.append("label")
.text("Target correlation (currently " + correlationCurrent + ")");
controlPanel.append("input")
.attr({ "type": "range", "min": 0, "max": 100, "step": 1 })
.attr("value", Math.round(correlationDefault * 100))
.on("input", function() {
var value = d3.select(this).property("value");
correlationCurrent = value / 100;
correlationLabel.text("Target correlation (currently " + correlationCurrent + ")");
generateData();
updateViz();
});
// add color checkbox
controlPanel.append("label")
.text("Use color")
.attr("for", "checkbox")
.append("input")
.attr("type", "checkbox")
.attr("name", "checkbox")
.property("checked", false)
.on("change", function() {
canvasColor = d3.select(this).property("checked");
generateData();
updateViz();
});
// add container for mouseover hit detection list
container.append("pre");
// data generator
function generateData() {
// clear data array
data = [];
xScale = d3.scale.linear()
.domain([-4, 4])
.rangeRound([0, canvasDim.width]);
yScale = d3.scale.linear()
.domain([-4, 4])
.rangeRound([canvasDim.height, 0]);
var color = d3.scale.category20b();
// generate correlated data points
// source: https://www.sitmo.com/article/generating-correlated-random-numbers/
d3.range(samplesCurrent).forEach(function(d) {
var x1 = normDist();
var x2 = normDist();
var p = correlationCurrent;
var y1 = p * x1 + Math.sqrt(1 - p * p) * x2
var xValue = x1;
var yValue = y1;
var colorValue = canvasColor ? color(d) : "black";
var point = {
x: xScale(xValue),
y: yScale(yValue),
id: d,
color: colorValue
};
data.push(point);
})
var xArr = data.map(function(d) { return d.x });
var yArr = data.map(function(d) { return d.y });
pearsonCorrelation = getPearsonCorrelation(xArr, yArr);
}
function updateViz() {
// update canvas
// clear canvas
ctx.clearRect(0, 0, canvasDim.width, canvasDim.height);
// draw identifier
ctx.globalAlpha = 1;
ctx.fillStyle = "black";
ctx.font = "12px Arial";
ctx.fillText("Canvas with " + format(samplesCurrent) + " elements",
canvasDim.width - 170, canvasDim.height - 20);
// set opacity for data elements
ctx.globalAlpha = opacityCurrent;
// draw the data
data.forEach(function(d, i) {
ctx.beginPath();
ctx.arc(d.x, d.y, 2, 0, 2 * Math.PI, true);
ctx.fillStyle = d.color;
ctx.closePath();
ctx.fill();
})
// update svg
// compute extents
var xExtent = d3.extent(data, function(d) { return xScale.invert(d.x); });
var yExtent = d3.extent(data, function(d) { return yScale.invert(d.y); });
var extents = [
{ name: "xDim", min: xExtent[0], max: xExtent[1] },
{ name: "yDim", min: yExtent[0], max: yExtent[1] }
];
// compute mean
//var xMean = d3.mean(data, function(d) { return xScale.invert(d.x) });
//var yMean = d3.mean(data, function(d) { return yScale.invert(d.y) });
// update extent bars
extentBarGroup.selectAll("rect")
.data(extents)
.attr("x", function(d) { return extentScale(d.min) })
.attr("width", function(d) { return extentScale(d.max) - extentScale(d.min) });
// update correlation bar
correlBar = correlBarGroup.selectAll("rect")
.data([Math.abs(-pearsonCorrelation)])
.attr("width", function(d) { return correlScale(d) });
}
// initial update
generateData();
updateViz();
// generate normal distribution
// https://www.design.caltech.edu/erik/Misc/Gaussian.html
function normDist() {
if (useD3randomNormal) { return d3randomNormal() };
var x1, x2, w, y1, y2;
do {
x1 = 2.0 * Math.random() - 1.0;
x2 = 2.0 * Math.random() - 1.0;
w = x1 * x1 + x2 * x2;
} while ( w >= 1.0 );
w = Math.sqrt( (-2.0 * Math.log( w ) ) / w );
y1 = x1 * w;
y2 = x2 * w;
return y1;
}
// compute correlation coefficient
// Source: https://stevegardner.net/2012/06/11/javascript-code-to-calculate-the-pearson-correlation-coefficient/
// Source: https://memory.psych.mun.ca/tech/js/correlation.shtml
function getPearsonCorrelation(x, y) {
var shortestArrayLength = 0;
if (x.length == y.length) {
shortestArrayLength = x.length;
} else if(x.length > y.length) {
shortestArrayLength = y.length;
console.error('x has more items in it, the last ' + (x.length - shortestArrayLength) + ' item(s) will be ignored');
} else {
shortestArrayLength = x.length;
console.error('y has more items in it, the last ' + (y.length - shortestArrayLength) + ' item(s) will be ignored');
}
var xy = [];
var x2 = [];
var y2 = [];
for (var i = 0; i < shortestArrayLength; i++) {
xy.push(x[i] * y[i]);
x2.push(x[i] * x[i]);
y2.push(y[i] * y[i]);
};
var sum_x = 0;
var sum_y = 0;
var sum_xy = 0;
var sum_x2 = 0;
var sum_y2 = 0;
for (var i = 0; i < shortestArrayLength; i++) {
sum_x += x[i];
sum_y += y[i];
sum_xy += xy[i];
sum_x2 += x2[i];
sum_y2 += y2[i];
};
var step1 = (shortestArrayLength * sum_xy) - (sum_x * sum_y);
var step2 = (shortestArrayLength * sum_x2) - (sum_x * sum_x);
var step3 = (shortestArrayLength * sum_y2) - (sum_y * sum_y);
var step4 = Math.sqrt(step2 * step3);
var answer = step1 / step4;
return answer;
}
</script>
https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js