Switching from svg to canvas on d3 force layout - javascript

Basically i'm trying to migrate from svg to canvas based graph to improve fps on large datasets.
I've been using d3 (v3) force layout to represent a network of nodes and links and i noticed a decay of performance as the number of svg elements on screen increased. To prevent this i started looking at canvas as a possible alternative and i saw some interesting results on the web. I completely converted my code to draw the same stuff on canvas instead of svg while maintaining the d3 force layout characteristics.
this._mainCanvas = d3.select(this._container).append('canvas')
.attr('width', this._width)
.attr('height', this._height);
this._mainContext = this._mainCanvas.node().getContext('2d');
this._simulation = d3.layout
.force()
.size([this._width, this._height])
.gravity(0.35)
.charge(-2000)
.theta(0.85)
.alpha(0.05)
.friction(0.7)
.nodes(this.data.nodes)
.links(this.data.relationships)
.linkDistance(1000)
.linkStrength(this._linkStrength)
.on('tick', self._tick())
.start();
this._tick() {
this._mainContext.clearRect(0, 0, this._width, this._height);
// draw links
this.data.relationships.forEach(function (d) {
self._mainContext.strokeStyle = 'rgba(128, 128, 128, 0.5)';
self._mainContext.beginPath();
self._mainContext.moveTo(d.source.x, d.source.y);
self._mainContext.lineTo(d.target.x, d.target.y);
self._mainContext.lineWidth = d.linkWeight;
self._mainContext.stroke();
});
this.data.nodes.forEach(function (d) {
// Node
self._mainContext.fillStyle = d.color;
self._mainContext.beginPath();
self._mainContext.moveTo(d.x, d.y);
self._mainContext.arc(d.x, d.y, d.radius, 0, 2 * Math.PI);
self._mainContext.strokeStyle = d3.rgb(d.color).darker(1);
self._mainContext.lineWidth = 2;
self._mainContext.stroke();
self._mainContext.fill();
self._mainContext.closePath();
// Text
self._mainContext.font = parseInt(Math.round(d.radius * 0.75)) + 'px' + ' Open Sans';
self._mainContext.textBaseline = 'middle';
self._mainContext.textAlign = 'start';
self._mainContext.fillStyle = 'black';
self._mainContext.fillText(d.value, d.x + parseInt(Math.round(d.radius * 1.25)), d.y);
// Icon
self._mainContext.font = parseInt(Math.round(d.radius * 0.75)) + 'px' + ' FontAwesome';
self._mainContext.textBaseline = 'middle';
self._mainContext.textAlign = 'center';
self._mainContext.fillStyle = 'white';
self._mainContext.fillText(d.icon, d.x, d.y);
});
}
The final result has been quite disappointing, performance didnt improve by a single bit instead of what i was expecting reading on the web and testing various examples. So i'm wondering, what am i doing wrong? Any help would be highly appreciated, thanks in advance.
Some of the examples i'm referring to are the following ones
https://bl.ocks.org/tafsiri/b83f60f23bf4ce130ca2 (svg)
https://bl.ocks.org/tafsiri/e9016e1b8d36bae56572 (canvas)
https://bl.ocks.org/mbostock/3180395 (d3 force layout on canvas)

Related

D3 path not showing

I am trying to make some features from a .geojson file render using D3 and D3geo and i am following this https://www.d3indepth.com/geographic/ as a guide. I don't really know what i am doing since i am not good at programing. It shows no errors on devtool console and when i inspect it in chrome it shows that the paths are created but the are not showing.
here is my code, the js part:
var width = Math.max(
document.documentElement.clientWidth,
window.innerWidth || 0
);
var height = Math.max(
document.documentElement.clientHeight,
window.innerHeight || 0
);
d3.json("2022.json").then(function (data) {
let geojson = data;
console.log(data);
let projection = d3
.geoMercator()
//.scale(1000)
//.center([20, 40])
//.translate([456, 250]);
projection.fitExtent([width, height], data);
let geoGenerator = d3.geoPath().projection(projection);
function update(geojson) {
let u = d3
.select("#content")
.data(geojson.features)
.enter()
u.append('path')
.join("path")
.attr("d", geoGenerator);
}
update(geojson);
});
enter image description here

Error displaying geotiff raster data on leaflet map

I hope someone can help me. I am trying to place isolines with labels from a geotiff file onto a leaflet map. The webpage https://geoexamples.com/d3-raster-tools-docs/plot/isolines.html is the example I am looking at, but the problem is that they perform this task on a non-moving leaflet map (no zoom feature). I have found the page https://bost.ocks.org/mike/leaflet/ where he places JSON data over a leaflet map, but does not cover how to transform geotiff data.
I can get the leaflet map to work and zoomable the JSON data remaps correctly but the geotiff data, while it displays, is not remapping correctly in my code and I don't quite understand how to make the isolines resize correctly. I thought it was taken care of with the svg.insert command but it doesn't. I know I am missing a step somewhere but I'm not sure where.
It seems like I am trying to do two things that are not quite explained anywhere combined. I eventually want to use the streamline code, too, which is why I am not using the XML code in the simple isolines code (and I need the labels). I got that version to work fine. I have also tried adding a canvas layer but that didn't work either, but perhaps I did something wrong with that.
I would greatly appreciate any suggestions on this. I think I have provided the necessary code and links to the files.
https://geoexamples.com/d3-raster-tools-docs/code_samples/tz850.tiff
https://geoexamples.com/d3-raster-tools-docs/code_samples/world-110m.json
var map = L.map('map').setView([-0.2858, 60.7868], 3);
mapLink =
'OpenStreetMap';
// Add an SVG element to Leaflet’s overlay pane
var svg = d3.select(map.getPanes().overlayPane).append("svg"),
g = svg.append("g").attr("class", "leaflet-zoom-hide");
d3.request("http://geoexamples.com/d3-raster-tools-docs/code_samples/tz850.tiff")
.responseType('arraybuffer')
.get(function(tiffData) {
d3.json("https://raw.githubusercontent.com/datasets/geo-countries/master/data/countries.geojson", function(geoShape) {
// create a d3.geo.path to convert GeoJSON to SVG
var transform = d3.geo.transform({
point: projectPoint
}),
path = d3.geo.path().projection(transform);
// create path elements for each of the features
d3_features = g.selectAll("path")
.data(geoShape.features)
.enter().append("path");
map.on("viewreset", reset);
reset();
// fit the SVG element to leaflet's map layer
function reset() {
bounds = path.bounds(geoShape);
var topLeft = bounds[0],
bottomRight = bounds[1];
svg.attr("width", bottomRight[0] - topLeft[0])
.attr("height", bottomRight[1] - topLeft[1])
.style("left", topLeft[0] + "px")
.style("top", topLeft[1] + "px");
g.attr("transform", "translate(" + -topLeft[0] + "," +
-topLeft[1] + ")");
// initialize the path data
d3_features.attr("d", path)
.style("fill-opacity", 0.7)
.attr('fill', 'blue');
}
// Use Leaflet to implement a D3 geometric transformation.
function projectPoint(x, y) {
var point = map.latLngToLayerPoint(new L.LatLng(y, x));
this.stream.point(point.x, point.y);
}
var tiff = GeoTIFF.parse(tiffData.response);
var image = tiff.getImage();
var rasters = image.readRasters();
var tiepoint = image.getTiePoints()[0];
var pixelScale = image.getFileDirectory().ModelPixelScale;
var geoTransform = [tiepoint.x, pixelScale[0], 0, tiepoint.y, 0, -1 * pixelScale[1]];
var zData = new Array(image.getHeight());
for (var j = 0; j < image.getHeight(); j++) {
zData[j] = new Array(image.getWidth());
for (var i = 0; i < image.getWidth(); i++) {
zData[j][i] = rasters[0][i + j * image.getWidth()];
}
}
var invGeoTransform = [-geoTransform[0] / geoTransform[1], 1 / geoTransform[1], 0, -geoTransform[3] / geoTransform[5], 0, 1 / geoTransform[5]];
var intervalsZ = [1400, 1420, 1440, 1460, 1480, 1500, 1520, 1540];
var linesZ = rastertools.isolines(zData, invGeoTransform, intervalsZ);
var colorScale = d3.scaleSequential(d3.interpolateYlOrRd)
.domain([1400, 1540]);
linesZ.features.forEach(function(d, i) {
svg.insert("path", ".streamline")
.datum(d)
.attr("d", path)
.style("stroke", colorScale(intervalsZ[i]))
.style("stroke-width", "2px")
.style("fill", "None");
});
})
})
<link rel="stylesheet" href="https://d19vzq90twjlae.cloudfront.net/leaflet-0.7/leaflet.css" />
<div id="map" style="width: 600px; height: 400px"></div>
<script src="http://bl.ocks.org/rveciana/raw/bef48021e38a77a520109d2088bff9eb/98a0b74b01109afae76d28bc27c4d1dbc7a87da8/geotiff.min.js"></script>
<script src="http://bl.ocks.org/rveciana/raw/bef48021e38a77a520109d2088bff9eb/98a0b74b01109afae76d28bc27c4d1dbc7a87da8/d3-marching-squares.min.js"></script>
<script src="http://bl.ocks.org/rveciana/raw/bef48021e38a77a520109d2088bff9eb/98a0b74b01109afae76d28bc27c4d1dbc7a87da8/path-properties.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="https://d19vzq90twjlae.cloudfront.net/leaflet-0.7/leaflet.js">
</script>

Zooming in D3 v4 on a globe

I'm looking for a way to zoom from place to place on a globe in D3 v4 (v4 is important). What I'm looking for is basically exactly this: https://www.jasondavies.com/maps/zoom/ My problem is that Jason Davies obfuscated his code, so I can't read it, and I can't find a bl.ock containing that project or anything similar to it. I'll provide a link to what I've got here: http://plnkr.co/edit/0mjyR3ovTfkDXB8FTG0j?p=preview
The relevant is probably inside the .tween():
.tween("rotate", function () {
var p = d3.geoCentroid(points[i]),
r = d3.geoInterpolate(projection.rotate(), [-p[0], -p[1]]);
return function (t) {
projection.rotate(r(t));
convertedLongLats = [projection(points[0].coordinates), projection(points[1].coordinates)]
c.clearRect(0, 0, width, height);
c.fillStyle = colorGlobe, c.beginPath(), path(sphere), c.fill();
c.fillStyle = colorLand, c.beginPath(), path(land), c.fill();
for (var j = 0; j < points.length; j++) {
var textCoords = projection(points[j].coordinates);
c.fillStyle = textColors, c.textAlign = "center", c.font = "18px FontAwesome", c.fillText(points[j].icon, textCoords[0], textCoords[1]);
textCoords[0] += 15;
c.textAlign = "left", c.font = " 12px Roboto", c.fillText(points[j].location, textCoords[0], textCoords[1]);
}
c.strokeStyle = textColors, c.lineWidth = 4, c.setLineDash([10, 10]), c.beginPath(), c.moveTo(convertedLongLats[0][0], convertedLongLats[0][1]), c.lineTo(convertedLongLats[1][0], convertedLongLats[1][1]), c.stroke();
};
})
Basically, I want what I've got now but I want it to zoom in, pretty much exactly like it is in the Animated World Zoom example I provided above. I'm not really looking for code, I'd rather someone point me in the right direction with an example or something (it's worth noting that I'm fairly new to d3 and that this project is heavily based on World Tour by mbostock, so it uses canvas). Thank you all in advance!
Based on your plunker and comment, a challenge in zooming out between two points in a transition is that the interpolator will only interpolate between two values. The solution in your plunker relies on two interpolators, one for zooming in and zooming out. This method has added un-needed complexity and somewhere along the line, as you note, it jumps to an incorrect scale. You could simplify this:
Take an interpolator that interpolates between -1 and 1, and weight each scale according to the absolute value of the interpolator. At zero, the zoom should be out all the way, while at -1,1, you should be zoomed in:
var s = d3.interpolate(-1,1);
// get the appropriate scale:
scale = Math.abs(0-s(t))*startEndScale + (1-Mat.abs(0-s(t)))*middleScale
This is a little clunky as it goes from zooming out to zooming in rather abruptly, so you could ease it with a sine type easing:
var s = d3.interpolate(0.0000001,Math.PI);
// get the appropriate scale:
scale = (1-Math.abs(Math.sin(s(t))))*startEndScale + Math.abs(Math.sin(s(t)))*middleScale
I've applied this to your plunker here.
For a simple and minimal example using the example that I suggested and your two points and path (and using your plunkr as a base), stripping out the animated line and icons, I would probably put together something like (plunker, snippet below best viewed on full screen):
var width = 600,
height = 600;
var points = [{
type: "Point",
coordinates: [-74.2582011, 40.7058316],
location: "Your Location",
icon: "\uF015"
}, {
type: "Point",
coordinates: [34.8887969, 32.4406351],
location: "Caribe Royale Orlando",
icon: "\uF236"
}];
var canvas = d3.select("body").append("canvas")
.attr("width", width)
.attr("height", height);
var c = canvas.node().getContext("2d");
var point = points[0].coordinates;
var projection = d3.geoOrthographic()
.translate([width / 2, height / 2])
.scale(width / 2)
.clipAngle(90)
.precision(0.6)
.rotate([-point[0], -point[1]]);
var path = d3.geoPath()
.projection(projection)
.context(c);
var colorLand = "#4d4f51",
colorGlobe = "#2e3133",
textColors = "#fff";
d3.json("https://unpkg.com/world-atlas#1/world/110m.json", function (error, world) {
if (error) throw error;
var sphere = { type: "Sphere" };
var land = topojson.feature(world, world.objects.land);
var i = 0;
var scaleMiddle = width/2;
var scaleStartEnd = width * 2;
loop();
function loop() {
d3.transition()
.tween("rotate",function() {
var flightPath = {
type: 'Feature',
geometry: {
type: "LineString",
coordinates: [points[i++%2].coordinates, points[i%2].coordinates]
}
};
// next point:
var p = points[i%2].coordinates;
// current rotation:
var currentRotation = projection.rotate();
// next rotation:
var nextRotation = projection.rotate([-p[0],-p[1]]).rotate();
// Interpolaters:
var r = d3.geoInterpolate(currentRotation,nextRotation);
var s = d3.interpolate(0.0000001,Math.PI);
return function(t) {
// apply interpolated values
projection.rotate(r(t))
.scale( (1-Math.abs(Math.sin(s(t))))*scaleStartEnd + Math.abs(Math.sin(s(t)))*scaleMiddle ) ;
c.clearRect(0, 0, width, height);
c.fillStyle = colorGlobe, c.beginPath(), path(sphere), c.fill();
c.fillStyle = colorLand, c.beginPath(), path(land), c.fill();
c.beginPath(), path(flightPath), c.globalAlpha = 0.5, c.shadowColor = "#fff", c.shadowBlur = 5, c.lineWidth = 0.5, c.strokeStyle = "#fff", c.stroke(), c.shadowBlur = 0, c.globalAlpha = 1;
}
})
.duration(3000)
.on("end", function() { loop(); })
}
});
<script src="http://d3js.org/d3.v4.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>

Semantic zooming on Canvas using d3.js

I'm having trouble implementing a semantic zoom on Canvas. I tried to replicate this example (https://bl.ocks.org/mbostock/3681006) but some of the relevant functions have been changed as d3 transitioned to v4, and I have a hunch that the problem is probably how I'm rescaling.
jsFiddle here: https://jsfiddle.net/kdqpxvff/
var canvas = d3.select("#mainCanvas"),
mainContext = canvas.node().getContext("2d"),
width = canvas.property("width"),
height = canvas.property("height")
var zoom = d3.zoom()
.scaleExtent([1, 400])
.on("zoom", zoomed);
var canvasScaleX = d3.scaleLinear()
.domain([-100,500])
.range([0, 800]);
var canvasScaleY= d3.scaleLinear()
.domain([-150,200])
.range([0, 500]);
var randomX = d3.randomUniform(-100,500),
randomY = d3.randomUniform(-150,200);
var data = d3.range(2000).map(function() {
return [randomX(),randomY()];
});
function zoomed() {
mainContext.clearRect(0, 0, width, height);
var transform = d3.zoomTransform(this);
canvasScaleX=transform.rescaleX(canvasScaleX);
canvasScaleY=transform.rescaleY(canvasScaleY);
draw();
}
canvas.call(zoom);
function draw(){
data.forEach(function(each,something){
mainContext.beginPath();
mainContext.fillRect(canvasScaleX(each[0]), canvasScaleY(each[1]),3,3);
mainContext.fill();
});
}
draw();
I was able to fix my problem, the error seems to be induced by my linear scales namely:
var canvasScaleX = d3.scaleLinear().domain([-100,500]).range([0, 800]);
var canvasScaleY= d3.scaleLinear().domain([-150,200]).range([0, 500]);
I'm still not sure why these were tripping my code up, but I will continue to work on it and post an update in case I find out. In the meanwhile here is the solution I found — https://jsfiddle.net/4ay21wk1/

Possibility of creating reusable D3.js/jQuery data-attribute components or modules

We are trying to create a component/module/etc. that takes the value of a custom data attribute and creates a D3 based pie chart displaying a percentage based on that data-attribute.
Examples of the div elements with custom data-attributes:
////// HTML
<div class="donut" data-donut="22"></div>
<div class="donut" data-donut="48"></div>
<div class="donut" data-donut="75></div>
Here is the CSS for it:
////// CSS
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: auto;
width: 960px;
padding-top: 20px;
background-color: #012647;
}
text {
font-family: "RamaGothicM-Heavy",Impact,Haettenschweiler,"Franklin Gothic Bold",Charcoal,"Helvetica Inserat","Bitstream Vera Sans Bold","Arial Black",sans-serif;
font-size: 7rem;
font-weight: 400;
line-height: 16rem;
fill: #1072b8;
}
.donut {
width: 29rem;
height: 29rem;
margin: 0 auto;
}
path.color0 {
fill: #1072b8;
}
path.color1 {
fill: #35526b;
}
The D3.js / jQuery example we're trying to convert to a reusable compunent is below. (To see full working example go to this link - http://codepen.io/anon/pen/JgyCz)
////// D3.js
var duration = 500,
transition = 200;
drawDonutChart(
'.donut',
$('.donut').data('donut'),
290,
290,
".35em"
);
function drawDonutChart(element, percent, width, height, text_y) {
width = typeof width !== 'undefined' ? width : 290;
height = typeof height !== 'undefined' ? height : 290;
text_y = typeof text_y !== 'undefined' ? text_y : "-.10em";
var dataset = {
lower: calcPercent(0),
upper: calcPercent(percent)
},
radius = Math.min(width, height) / 2,
pie = d3.layout.pie().sort(null),
format = d3.format(".0%");
var arc = d3.svg.arc()
.innerRadius(radius - 20)
.outerRadius(radius);
var svg = d3.select(element).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll("path")
.data(pie(dataset.lower))
.enter().append("path")
.attr("class", function(d, i) { return "color" + i })
.attr("d", arc)
.each(function(d) { this._current = d; }); // store the initial values
var text = svg.append("text")
.attr("text-anchor", "middle")
.attr("dy", text_y);
if (typeof(percent) === "string") {
text.text(percent);
}
else {
var progress = 0;
var timeout = setTimeout(function () {
clearTimeout(timeout);
path = path.data(pie(dataset.upper)); // update the data
path.transition().duration(duration).attrTween("d", function (a) {
// Store the displayed angles in _current.
// Then, interpolate from _current to the new angles.
// During the transition, _current is updated in-place by d3.interpolate.
var i = d3.interpolate(this._current, a);
var i2 = d3.interpolate(progress, percent)
this._current = i(0);
return function(t) {
text.text( format(i2(t) / 100) );
return arc(i(t));
};
}); // redraw the arcs
}, 200);
}
};
function calcPercent(percent) {
return [percent, 100-percent];
};
I experimented on using Backbone.js with my charts(I used NVD3.js).
Here is a question on SO regarding this
D3 + Backbone by #jtuulos (I personally found this link very useful to understand the whole concept of extending a parent class and instantiating )
Responsive Charts with D3 and Backbone
d3.js & backbone.js
I was able to create a parent chart class for the common chart types I use and then simply create an instance of that chart so I could re-use it when ever I want. Makes life easier when trying to maintain the charts.
May not be the best way of doing it, but I find this way very usefull.

Categories