Create artificial zoom transform event - javascript

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.

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);

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>

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/

NVD3 LineChart: Some xAxis dates do not show at the beginning and end of the axis, despite I am using the tickValues() method

I am using the tickValues() method so that I can choose exactly which dates to show on my LineChart (I use a function that makes them sparser, by creating an xTicks array that contains only the x values that I want to become ticks).
However, there are some points of the xTicks array (at the beginning and end) that do not appear as ticks. See below:
This behavior changes depending on the size of the window, for example more X points are missed when I resize down the window (also, the "Date" label is at a very awkward position, on the 1st graph is so high above that it does not even show):
This is the code:
createChart(dom, props) {
var {width, height, marginLeft, marginRight} = props;
nv.addGraph(function () {
var chart = nv.models.lineChart()
.margin({left: marginLeft, right: marginRight, bottom: 140})
.width(width)
.height(height)
.options({
transitionDuration: 500,
useInteractiveGuideline: true
});
var createTicks = function () {
let skipTicks = Math.round(props.data[0].values.length / 50);
var xTicks = [];
for (let [i,value] of props.data[0].values.entries()) {
if (i && !(i % (skipTicks + 1))) {
xTicks.push(value.x);
}
}
return xTicks;
};
chart.xAxis
.axisLabel(props.xLabel)
.tickFormat((d) => {
return d3.time.format(props.timeFormat)(new Date(d));
})
// Since `axis.ticks()` doesn't work with NVD3 (the internal methods over-write any value you set on the axis),
// we need to use `axis.tickValues()` instead, following this recommendation:
// http://stackoverflow.com/questions/21316617/d3-js-nvd3-date-on-x-axis-only-some-dates-are-show
.tickValues(createTicks());
chart.yAxis
.axisLabel(props.yLabel)
.tickFormat(d3.format('d'));
d3.select(dom).select('svg')
.attr('width', width)
.attr('height', height)
.datum(props.data)
.call(chart);
// Change the rotation of xAxis labels
d3.select(dom).select('.nv-x.nv-axis > g').selectAll('g').selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-90)');
return chart;
});
}
I tried a lot of things, like adding a domain with all values included, adding a range, etc. Nothing worked.
How can I fix this problem? Thank you.
I found how to fix it, so I will share it here.
NVD3 has a showMaxMin function that is true by default. This function "reduces" the values at the axis edges. I set it to false and there is no reduction of values anymore:
.axisLabel(props.xLabel)
.showMaxMin(false)
.tickFormat((d) => {
return d3.time.format(props.timeFormat)(new Date(d));
})
Here is the end result:
reduceXTicks: false
This method will show all x axis values for nvd3 multibarchart

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