Semantic zooming on Canvas using d3.js - javascript

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/

Related

How can I define map extent based on points displayed?

I have the following map I've made, by overlaying points on a mapbox map using d3.js.
I'm trying to get the map to zoom so that the map extent just includes the d3 markers (points).
I think the pseudocode would look something like this:
//find the northernmost, easternmost, southernmost, westernmost points in the data
//get some sort of bounding box?
//make map extent the bounding box?
Existing code:
<div id="map"></div>
<script>
mapboxgl.accessToken = "YOUR_TOKEN";
var map = new mapboxgl.Map({
container: "map",
style: "mapbox://styles/mapbox/streets-v9",
center: [-74.5, 40.0],
zoom: 9
});
var container = map.getCanvasContainer();
var svg = d3
.select(container)
.append("svg")
.attr("width", "100%")
.attr("height", "500")
.style("position", "absolute")
.style("z-index", 2);
function project(d) {
return map.project(new mapboxgl.LngLat(d[0], d[1]));
}
#Lat, long, and value
var data = [
[-74.5, 40.05, 23],
[-74.45, 40.0, 56],
[-74.55, 40.0, 1],
[-74.85, 40.0, 500],
];
var dots = svg
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("r", 20)
.style("fill", "#ff0000");
function render() {
dots
.attr("cx", function (d) {
return project(d).x;
})
.attr("cy", function (d) {
return project(d).y;
});
}
map.on("viewreset", render);
map.on("move", render);
map.on("moveend", render);
render(); // Call once to render
</script>
Update
I found this code for reference, linked here at https://data-map-d3.readthedocs.io/en/latest/steps/step_03.html:
function calculateScaleCenter(features) {
// Get the bounding box of the paths (in pixels!) and calculate a
// scale factor based on the size of the bounding box and the map
// size.
var bbox_path = path.bounds(features),
scale = 0.95 / Math.max(
(bbox_path[1][0] - bbox_path[0][0]) / width,
(bbox_path[1][1] - bbox_path[0][1]) / height
);
// Get the bounding box of the features (in map units!) and use it
// to calculate the center of the features.
var bbox_feature = d3.geo.bounds(features),
center = [
(bbox_feature[1][0] + bbox_feature[0][0]) / 2,
(bbox_feature[1][1] + bbox_feature[0][1]) / 2];
return {
'scale': scale,
'center': center
};
}
However, when I run the function:
var scaleCenter = calculateScaleCenter(data);
console.log("scalecenter is", scaleCenter)
I get the error:
path is not defined
Furthermore, it seems like I would need to dynamically adjust the center and zoom parameters of the mapbox map. Would I just set these values dynamically with the values produced by the calculateScaleCenter function?
The readthedocs example code is erroneously missing a bit of code
Your javascript code is reporting that you have not defined the variable path before using it. You have copied it correctly from readthedocs, but the code you are copying contains an omission.
Fortunately, on the readthedocs version of the code snippet, there is a mention of a Stack Overflow answer http://stackoverflow.com/a/17067379/841644
that gives more information.
Does adding this information, or something similar corresponding to your situation, help fix your problem?
var projection = d3.geo.mercator()
.scale(1);
var path = d3.geo.path()
.projection(projection);

Create artificial zoom transform event

I have a timeline in D3 with a highly modified drag/scroll pan/zoom. The zoom callbacks use the d3.event.transform objects generated by the zoom behavior.
I need to add a programmatic zoom that uses my existing callbacks. I have tried and tried to do this without doing so, but I haven't gotten it to work and it would be radically easier and faster to reuse the existing structure.
So the input is a new domain, i.e. [new Date(1800,0), new Date(2000,0)], and the output should be a new d3.event.transform that acts exactly like the output of a, say, mousewheel event.
Some example existing code:
this.xScale = d3.scaleTime()
.domain(this.initialDateRange)
.range([0, this.w]);
this.xScaleShadow = d3.scaleTime()
.domain(this.xScale.domain())
.range([0, this.w]);
this.zoomBehavior = d3.zoom()
.extent([[0, 0], [this.w, this.h]])
.on('zoom', this.zoomHandler.bind(this));
this.timelineSVG
.call(zoomBehavior);
...
function zoomHandler(transformEvent) {
this.xScale.domain(transformEvent.rescaleX(this.xScaleShadow).domain());
// update UI
this.timeAxis.transformHandler(transformEvent);
this.updateGraphics();
}
Example goal:
function zoomTo(extents){
var newTransform = ?????(extents);
zoomHandler(newTransform);
}
(Please don't mark as duplicate of this question, my question is more specific and refers to a much newer d3 API)
Assuming I understand the problem:
Simply based on the title of your question, we can assign a zoom transform and trigger a zoom event programatically in d3v4 and d3v5 using zoom.transform, as below:
selection.call(zoom.transform, newTransform)
Where selection is the selection that the zoom was called on, zoom is the name of the zoom behavior object, zoom.transform is a function of the zoom object that sets a zoom transform that is applied on a selection (and emits start, zoom, and end events), while newTransform is a transformation that is provided to zoom.transform as a parameter (see selection.call() in the docs for more info on this pattern, but it is the same as zoom.transform(selection,newTransform)).
Below you can set a zoom on the rectangle by clicking the button: The zoom is applied not spatially but with color, but the principles are the same when zooming on data semantically or geometrically.
var scale = d3.scaleSqrt()
.range(["red","blue","yellow"])
.domain([1,40,1600]);
var zoom = d3.zoom()
.on("zoom", zoomed)
.scaleExtent([1,1600])
var rect = d3.select("svg")
.append("rect")
.attr("width", 400)
.attr("height", 200)
.attr("fill","red")
.call(zoom);
// Call zoom.transform initially to trigger zoom (otherwise current zoom isn't shown to start).
rect.call(zoom.transform, d3.zoomIdentity);
// Call zoom.transform to set k to 100 on button push:
d3.select("button").on("click", function() {
var newTransform = d3.zoomIdentity.scale(100);
rect.call(zoom.transform, newTransform);
})
// Zoom function:
function zoomed(){
var k = d3.event.transform.k;
rect.attr("fill", scale(k));
d3.select("#currentZoom").text(k);
}
rect {
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<button>Trigger Zoom</button> <br />
<span> Current Zoom: </span><span id="currentZoom"></span><br />
<svg></svg>
If applying a zoom transform to a scale, we need to rescale based on the new extent. This is similar to the brush and zoom examples that exist, but I'll break it out in a bare bones example using only a scale and an axis (you can zoom on the scale itself with the mouse too):
var width = 400;
var height = 200;
var svg = d3.select("svg")
.attr("width",width)
.attr("height",height);
// The scale used to display the axis.
var scale = d3.scaleLinear()
.range([0,width])
.domain([0,100]);
// The reference scale
var shadowScale = scale.copy();
var axis = d3.axisBottom()
.scale(scale);
var g = svg.append("g")
.attr("transform","translate(0,50)")
.call(axis);
// Standard zoom behavior:
var zoom = d3.zoom()
.scaleExtent([1,10])
.translateExtent([[0, 0], [width, height]])
.on("zoom", zoomed);
// Rect to interface with mouse for zoom events.
var rect = svg.append("rect")
.attr("width",width)
.attr("height",height)
.attr("fill","none")
.call(zoom);
d3.select("#extent")
.on("click", function() {
// Redfine the scale based on extent
var extent = [10,20];
// Build a new zoom transform:
var transform = d3.zoomIdentity
.scale( width/ ( scale(extent[1]) - scale(extent[0]) ) ) // how wide is the full domain relative to the shown domain?
.translate(-scale(extent[0]), 0); // Shift the transform to account for starting value
// Apply the new zoom transform:
rect.call(zoom.transform, transform);
})
d3.select("#reset")
.on("click", function() {
// Create an identity transform
var transform = d3.zoomIdentity;
// Apply the transform:
rect.call(zoom.transform, transform);
})
// Handle both regular and artificial zooms:
function zoomed() {
var t = d3.event.transform;
scale.domain(t.rescaleX(shadowScale).domain());
g.call(axis);
}
rect {
pointer-events: all;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<button id="extent">Zoom to extent 10-20</button><button id="reset">Reset</button><br />
<svg></svg>
Taking a look at the key part, when we want to zoom to a certain extent we can use something along the following lines:
d3.select("something")
.on("click", function() {
// Redfine the scale based on scaled extent we want to show
var extent = [10,20];
// Build a new zoom transform (using d3.zoomIdentity as a base)
var transform = d3.zoomIdentity
// how wide is the full domain relative to the shown domain?
.scale( width/(scale(extent[1]) - scale(extent[0])) )
// Shift the transform to account for starting value
.translate(-scale(extent[0]), 0);
// Apply the new zoom transform:
rect.call(zoom.transform, transform);
})
Note that by using d3.zoomIdentity, we can take advantage of the identity transform (with its built in methods for rescaling) and modify its scale and transform to meet our needs.

Switching from svg to canvas on d3 force layout

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)

Error creating axis d3.js

I've been wrestling with this problem for quite a while now. I want to create an x-axis, so I used the following code:
var w = 1024,
h = 1024,
padding = 20,
fill = d3.scale.category20();
var vis = d3.select("#chart")
.append("svg:svg")
.attr("width", w)
.attr("height", h);
d3.json("force.json", function(json) {
//Create array of dates for the x-axis
var dates = json.nodes.map(function(d) {
return Date.parse(d.date);
});
console.log(dates)
// Create scale from the array of dates
var scale = d3.time.scale()
.domain(dates)
.range([padding, w - padding]);
console.log(scale(dates));
var axis = d3.svg.axis();
//more code below, but error gets called at the line above this comment.
}
However, I get the following error returned consistently:
Uncaught TypeError: Object #<Object> has no method 'axis'
This is very perplexing, as I was simply following the d3 API at https://github.com/mbostock/d3/wiki/SVG-Axes.
Would somebody kindly point out where I'm going wrong?
(for reference, force.json is the data I am working with)

Is it possible to have zooming option in NVD3 charts as like in Highcharts?

I recently started using NVD3 charts and like to have zooming option like Highcharts. Is it possible?
There is a "Line Chart with View Finder" example that uses nv.models.lineWithFocusChart(). This chart type sports an additional "mini map" below the main chart that you can use to zoom and pan the chart:
See: Line Chart with View Finder
You could apply this function to your nvd3 chart. It does not yield a draggable box to assist zooming like in Highcharts but it allows zooming by mouse scroll as shown here: pan+zoom example. You will have to remove the transitions/delays from nvd3 to achieve smooth zooming. For now, you might have to do this manually by altering the nvd3 source file. There is a discussion about that here.
Here is a jsfiddle.
function addZoom(options) {
// scaleExtent
var scaleExtent = 10;
// parameters
var yAxis = options.yAxis;
var xAxis = options.xAxis;
var xDomain = options.xDomain || xAxis.scale().domain;
var yDomain = options.yDomain || yAxis.scale().domain;
var redraw = options.redraw;
var svg = options.svg;
var discrete = options.discrete;
// scales
var xScale = xAxis.scale();
var yScale = yAxis.scale();
// min/max boundaries
var x_boundary = xScale.domain().slice();
var y_boundary = yScale.domain().slice();
// create d3 zoom handler
var d3zoom = d3.behavior.zoom();
// fix domain
function fixDomain(domain, boundary) {
if (discrete) {
domain[0] = parseInt(domain[0]);
domain[1] = parseInt(domain[1]);
}
domain[0] = Math.min(Math.max(domain[0], boundary[0]), boundary[1] - boundary[1]/scaleExtent);
domain[1] = Math.max(boundary[0] + boundary[1]/scaleExtent, Math.min(domain[1], boundary[1]));
return domain;
};
// zoom event handler
function zoomed() {
yDomain(fixDomain(yScale.domain(), y_boundary));
xDomain(fixDomain(xScale.domain(), x_boundary));
redraw();
};
// zoom event handler
function unzoomed() {
xDomain(x_boundary);
yDomain(y_boundary);
redraw();
d3zoom.scale(1);
d3zoom.translate([0,0]);
};
// initialize wrapper
d3zoom.x(xScale)
.y(yScale)
.scaleExtent([1, scaleExtent])
.on('zoom', zoomed);
// add handler
svg.call(d3zoom).on('dblclick.zoom', unzoomed);
};
// here chart is your nvd3 model
addZoom({
xAxis : chart.xAxis,
yAxis : chart.yAxis,
yDomain: chart.yDomain,
xDomain: chart.xDomain,
redraw : function() { chart.update() },
svg : svg
});

Categories