|
<!doctype html> |
|
|
|
<html lang="en"> |
|
<head> |
|
<meta charset="utf-8"> |
|
<title>Canvas in SVG</title> |
|
<!-- Author: Bo Ericsson, https://www.linkedin.com/in/boeric00/ --> |
|
<link rel=stylesheet type=text/css href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.3.2/css/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> |
|
</head> |
|
<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="https://cdn.jsdelivr.net/npm/[email protected]/src/index.js"></script> |
|
<script> |
|
'use strict'; |
|
const { generate, inspect, version } = this.arrayCorrel; |
|
console.log('version', version); |
|
|
|
// 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 |
|
const array = generate(samplesCurrent, correlationCurrent); |
|
array.forEach((d, id) => { |
|
const { x: xValue, y: yValue } = d; |
|
|
|
var colorValue = canvasColor ? color(d) : "black"; |
|
var point = { |
|
x: xScale(xValue), |
|
y: yScale(yValue), |
|
id, |
|
color: colorValue |
|
}; |
|
data.push(point); |
|
}); |
|
|
|
var xArr = data.map(function(d) { return d.x }); |
|
var yArr = data.map(function(d) { return d.y }); |
|
|
|
pearsonCorrelation = inspect(array).r; |
|
} |
|
|
|
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(); |
|
|
|
</script> |
|
</body> |
|
</html> |