d3 scaleLinear update and zoom conflict - javascript

i have a problem combining manual update and zoom functionality in d3.
Here is a small demo code cut off out of a larger module which creates only a linear scale.
https://jsfiddle.net/superkamil/x3v2yc7j/2
class LinearScale {
constructor(element, options) {
this.element = d3.select(element);
this.options = options;
this.scale = this._createScale();
this.axis = this._createAxis();
this.linearscale = this._create();
}
update(options) {
this.options = Object.assign(this.options, options);
this.scale = this._createScale();
this.axis = this._createAxis();
this.linearscale.call(this.axis);
}
_create() {
const scale = this.element
.append('g')
.attr('class', 'linearscale')
.call(this.axis);
this.zoom = d3.zoom().on('zoom', () => this._zoomed());
this.element.append('rect')
.style('visibility', 'hidden')
.style('width', this.options.width)
.style('height', this.options.height)
.attr('pointer-events', 'all')
.call(this.zoom);
return scale;
}
_createScale() {
let range = this.options.width;
this.scale = this.scale || d3.scaleLinear();
this.scale.domain([
this.options.from,
this.options.to
]).range([0, range]);
return this.scale;
}
_createAxis() {
if (this.axis) {
this.axis.scale(this.scale);
return this.axis;
}
return d3.axisBottom(this.scale);
}
_zoomed() {
this.linearscale
.call(this.axis.scale(d3.event.transform.rescaleX(this.scale)));
let domain = this.axis.scale().domain();
this.element.dispatch('zoomed', {
detail: {
from: domain[0],
to: domain[1],
},
});
}
}
const scale = new LinearScale(document.getElementById('axis'), {
from: 0,
to: 600,
width: 600,
height: 100
});
document.getElementById('set').addEventListener('click', () => {
scale.update({
from: 0,
to: 100
});
});
Zoom x axis out to 0 - 10000 (random numbers)
Click on the "set" button
X axis sets the domain from 0 - 100
Start zooming out again
-> Expected: Zoom starts from the domain 0 - 100
-> Result: Zoom jumps back to the previous zoom level 0 - 10000
I know, that d3 is working with a scale copy and i'm updating the original scale but i don't find a way how to combine them or how to set the zoom level to the original scale.
https://github.com/d3/d3-zoom/blob/master/README.md#transform_rescaleX
Thanks!

You either recreate the zoom behaviour or just reset the current zoom transforms.
You should be able to reset each parameter( zoom and translate) or just reset them all. The following example also triggers the zoom specific events, but as you already have the domain set it should not be a visual problem.
Ex:
Add
this.element.select("rect").call(this.zoom.transform, d3.zoomIdentity);
in your update function.
More information regarding the zoom api in https://github.com/d3/d3-zoom/blob/master/README.md#zoom_transform . Also I found an example using a transition animation at https://bl.ocks.org/mbostock/db6b4335bf1662b413e7968910104f0f .
I also updated the fiddle:
update(options) {
this.options = Object.assign(this.options, options);
this.scale = this._createScale();
this.axis = this._createAxis();
this.linearscale.call(this.axis);
this.element.select("rect").call(this.zoom.transform, d3.zoomIdentity);
}
https://jsfiddle.net/x3v2yc7j/8/

Related

Smooth zooming through the canvas

I have a zooming feature in my fabricjs app. The that runs zoom is just very similar to this: http://fabricjs.com/fabric-intro-part-5
canvas.on('mouse:wheel', function(opt) {
var delta = opt.e.deltaY;
var zoom = canvas.getZoom();
zoom *= 0.999 ** delta;
if (zoom > 20) zoom = 20;
if (zoom < 0.01) zoom = 0.01;
// canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom); commented because I run canvas.zoomToPoint in main canvas update function, not just inside handler
this.zoom = zoom;
opt.e.preventDefault();
opt.e.stopPropagation();
});
Now I would like to program a smooth zooming on mouse wheel - so that it zooms like here: https://mudin.github.io/indoorjs but I completely don't know where to start.
I feel like I need to debounce somehow the handler for mouse wheel, because for now it happens whenever you wheel the mouse - is this the right direction? How to accomplish something like that?
Maybe this is usefull. There is an eventlistener for the wheel event and then I set the scale of the element and control the scaling in CSS.
var box = document.querySelector('#box');
box.scale = 1; // initial scale
box.addEventListener('wheel', e => {
e.preventDefault();
var deltaScale = (e.deltaY > 0) ? .1 : -.1;
e.target.scale = e.target.scale + deltaScale;
e.target.style.transform = `scale(${e.target.scale})`;
});
body {
display: flex;
justify-content: space-around;
align-items: center;
height: 100vh;
}
#box {
border: thin solid black;
width: 100px;
height: 100px;
transition: transform 1s ease-in-out;
}
<div id="box">content</div>
For anyone still wondering how to implement smooth zooming in Fabric.js
First you need to register a change event for the mouse wheel:
(keeping track of the values in a Mouse object)
canvas.on('mouse:wheel', (e) => {
Mouse.x = e.e.offsetX;
Mouse.y = e.e.offsetY;
Mouse.w += e.e.wheelDelta;
return false;
});
Then you need to call this function in your render loop when Mouse.w !== 0:
let targetZoomLevel = canvas.getZoom();
let isAnimationRunning = false;
let abort = false;
// Call this function in your render loop if Mouse.W != 0
function zoom() {
// mousewheel value changed: abort animation if it's running
abort = isAnimationRunning;
// get current zoom level
let curZoom = canvas.getZoom();
// Add mousewheel delta to target zoom level
targetZoomLevel += Mouse.w / 2000;
// Animate the zoom if smooth zooming is enabled
fabric.util.animate({
startValue: curZoom,
endValue: targetZoomLevel,
// Set this value to your liking
duration: 500,
easing: fabric.util.ease.easeOutQuad,
onChange: (newZoomValue) => {
isAnimationRunning = true;
// Zoom to mouse pointer location
canvas.zoomToPoint({
x: Mouse.x,
y: Mouse.y
}, newZoomValue);
// call canvas.renderAll here
},
onComplete: () => {
isAnimationRunning = false;
},
abort: () => {
// The animation aborts if this function returns true
let abortValue = abort;
if (abortValue == true) {
abort = false;
}
return abortValue;
}
});
}
It aborts the animation when the mousewheel value changes and adds the value to a zoom target

Fabric.JS and Fabric-Brush - Can't add to lower canvas

I'm trying to use Fabric.js with Fabric Brush This issue that I'm running into is that Fabric Brush only puts the brush strokes onto the Top Canvas and not the lower canvas. (The stock brushes in fabric.js save to the bottom canvas) I think I need to convert "this.canvas.contextTop.canvas" to an object and add that object to the the lower canvas. Any ideas?
I've tried running:
this.canvas.add(this.canvas.contextTop)
in
onMouseUp: function (pointer) {this.canvas.add(this.canvas.contextTop)}
But I'm getting the error
Uncaught TypeError: obj._set is not a function
So the contextTop is CanvasHTMLElement context. You cannot add it.
You can add to the fabricJS canvas just fabric.Object derived classes.
Look like is not possible for now.
They draw as pixel effect and then they allow you to export as an image.
Would be nice to extend fabricJS brush interface to create redrawable objects.
As of now with fabricJS and that particular version of fabric brush, the only thing you can do is:
var canvas = new fabric.Canvas(document.getElementById('c'))
canvas.freeDrawingBrush = new fabric.CrayonBrush(canvas, {
width: 70,
opacity: 0.6,
color: "#ff0000"
});
canvas.isDrawingMode = true
canvas.on('mouse:up', function(opt) {
if (canvas.isDrawingMode) {
var c = fabric.util.copyCanvasElement(canvas.upperCanvasEl);
var img = new fabric.Image(c);
canvas.contextTopDirty = true;
canvas.add(img);
canvas.isDrawingMode = false;
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.1/fabric.min.js"></script>
<script src="https://tennisonchan.github.io/fabric-brush/bower_components/fabric-brush/dist/fabric-brush.min.js"></script>
<button>Enter free drawing</button>
<canvas id="c" width="500" height="500" ></canvas>
That is just creating an image from the contextTop and add as an object.
I have taken the approach suggested by AndreaBogazzi and modified the Fabric Brush so that it does the transfer from upper to lower canvas (as an image) internal to Fabric Brush. I also used some code I found which crops the image to a smaller bounding box so that is smaller than the full size of the canvas. Each of the brushes in Fabric Brush has an onMouseUp function where the code should be placed. Using the case of the SprayBrush, the original code here was:
onMouseUp: function(pointer) {
},
And it is replaced with this code:
onMouseUp: function(pointer){
function trimbrushandcopytocanvas() {
let ctx = this.canvas.contextTop;
let pixels = ctx.getImageData(0, 0, canvas.upperCanvasEl.width, canvas.upperCanvasEl.height),
l = pixels.data.length,
bound = {
top: null,
left: null,
right: null,
bottom: null
},
x, y;
// Iterate over every pixel to find the highest
// and where it ends on every axis ()
for (let i = 0; i < l; i += 4) {
if (pixels.data[i + 3] !== 0) {
x = (i / 4) % canvas.upperCanvasEl.width;
y = ~~((i / 4) / canvas.upperCanvasEl.width);
if (bound.top === null) {
bound.top = y;
}
if (bound.left === null) {
bound.left = x;
} else if (x < bound.left) {
bound.left = x;
}
if (bound.right === null) {
bound.right = x;
} else if (bound.right < x) {
bound.right = x;
}
if (bound.bottom === null) {
bound.bottom = y;
} else if (bound.bottom < y) {
bound.bottom = y;
}
}
}
// Calculate the height and width of the content
var trimHeight = bound.bottom - bound.top,
trimWidth = bound.right - bound.left,
trimmed = ctx.getImageData(bound.left, bound.top, trimWidth, trimHeight);
// generate a second canvas
var renderer = document.createElement('canvas');
renderer.width = trimWidth;
renderer.height = trimHeight;
// render our ImageData on this canvas
renderer.getContext('2d').putImageData(trimmed, 0, 0);
var img = new fabric.Image(renderer,{
scaleY: 1./fabric.devicePixelRatio,
scaleX: 1./fabric.devicePixelRatio,
left: bound.left/fabric.devicePixelRatio,
top:bound.top/fabric.devicePixelRatio
});
this.canvas.clearContext(ctx);
canvas.add(img);
}
setTimeout(trimbrushandcopytocanvas, this._interval); // added delay because last spray was on delay and may not have finished
},
The setTimeout function was used because Fabric Brush could still be drawing to the upper canvas after the mouseup event occurred, and there were occasions where the brush would continue painting the upper canvas after its context was cleared.

CSS Zoom property not working with BoundingClientRectangle

I'm having trouble getting the coordinates of an element after it has been transformed using the "zoom" property. I need to know the coordinates of all 4 corners. I would typically accomplish this with the getBoundingClientRect property however, that does not seem to work correctly when the element is zoomed. I've attached a short jsfiddle link to show what doesn't seem to work. I'm using Chrome but the behavior is present on Firefox as well.
http://jsfiddle.net/GCam11489/0hu7kvqt/
HTML:
<div id="box" style="zoom:100%">Hello</div><div></div><div></div>
JS:
var div = document.getElementsByTagName("div");
div[1].innerHTML = "PreZoom Width: " + div[0].getBoundingClientRect().width;
div[0].style.zoom = "150%";
div[2].innerHTML = "PostZoom Width: " + div[0].getBoundingClientRect().width;
When I run that code, "100" is displayed for both the before and after zoom widths.
I have found a lot of information but everything I find seems to say that it was a known bug on Chrome and has since been fixed. Does anyone know how this can be corrected or what I might be doing wrong?
This comment on the bug report goes into some detail reg. the issue, but its status appears to be 'WontFix', so you're probably out of luck there.
Some alternatives:
If you were to use transform: scale(1.5) instead of zoom, you'd get the correct value in getBoundingClientRect(), but it would mess with the page layout.
You could use window.getComputedStyle(div[0]).zoom to get the zoom value of the element (in decimals) and multiply it with the width from getBoundingClientRect()
This is quite an old post but I've encountered the same problem, I have an angular material project on a small panel which has a pixel ratio of under 1 which makes everything very small. To fix that I've added a zoom on the body to counter this.
The angular material (7) slider uses getBoundingClientRect() to determine it's position, which made the slider go way further then I'd wish.
I've used a solution like eruditespirit mentioned above.
if (Element.prototype.getBoundingClientRect) {
const DefaultGetBoundingClientRect = Element.prototype.getBoundingClientRect;
Element.prototype.getBoundingClientRect = function () {
let zoom = +window.getComputedStyle(document.body).zoom;
let data = DefaultGetBoundingClientRect.apply(this, arguments);
if (zoom !== 1) {
data.x = data.x * zoom;
data.y = data.y * zoom;
data.top = data.top * zoom;
data.left = data.left * zoom;
data.right = data.right * zoom;
data.bottom = data.bottom * zoom;
data.width = data.width * zoom;
data.height = data.height * zoom;
}
return data;
};
}
Had the issue with a tooltip component.
Created this function to fetch all zooms applied to an element. Then I used that zoom to apply it to the global tooltip as well. Once done it aligned correctly.
Notice that it checks for parents parentel.parentElement?.parentElement, this is so it doesn't take the global browser zoom into account.
export const getZoomLevel = (el: HTMLElement) : number[] => {
const zooms = []
const getZoom = (el: HTMLElement) => {
const zoom = window.getComputedStyle(el).getPropertyValue('zoom')
const rzoom = zoom ? parseFloat(zoom) : 1
if (rzoom !== 1) zooms.push(rzoom)
if (el.parentElement?.parentElement) getZoom(el.parentElement)
}
getZoom(el)
zooms.reverse()
return zooms
}
If you don't want to to zoom the 'tooltip' component would be to get rect with zoom adjusted for, like this:
export const getRect = (el: HTMLElement) : Partial<DOMRect> => {
if (!el) return { x: 0, y: 0, width: 0, height: 0, top: 0, right: 0, bottom: 0, left: 0 }
let rect = el?.getBoundingClientRect();
const zooms = getZoomLevel(el)
const rectWithZoom = {
bottom: zooms.reduce((a, b) => a * b, rect.bottom),
height: zooms.reduce((a, b) => a * b, rect.height),
left: zooms.reduce((a, b) => a * b, rect.left),
right: zooms.reduce((a, b) => a * b, rect.right),
top: zooms.reduce((a, b) => a * b, rect.top),
width: zooms.reduce((a, b) => a * b, rect.width),
x: zooms.reduce((a, b) => a * b, rect.x),
y: zooms.reduce((a, b) => a * b, rect.y),
}
return rectWithZoom
}

planetaryjs globe not working - javascript

I want this type of globe and I am using planetaryjs for this.
I have added the necessary resources for that in external resources link
including all js files and data file.
why the globe is not loading?
jsfiddle link
(function() {
var canvas = document.getElementById('quakeCanvas');
// Create our Planetary.js planet and set some initial values;
// we use several custom plugins, defined at the bottom of the file
var planet = planetaryjs.planet();
planet.loadPlugin(autocenter({extraHeight: -120}));
planet.loadPlugin(autoscale({extraHeight: -120}));
planet.loadPlugin(planetaryjs.plugins.earth({
topojson: { file: '/world-110m.json' },
oceans: { fill: '#001320' },
land: { fill: '#06304e' },
borders: { stroke: '#001320' }
}));
planet.loadPlugin(planetaryjs.plugins.pings());
planet.loadPlugin(planetaryjs.plugins.zoom({
scaleExtent: [50, 5000]
}));
planet.loadPlugin(planetaryjs.plugins.drag({
onDragStart: function() {
this.plugins.autorotate.pause();
},
onDragEnd: function() {
this.plugins.autorotate.resume();
}
}));
planet.loadPlugin(autorotate(5));
planet.projection.rotate([100, -10, 0]);
planet.draw(canvas);
// Create a color scale for the various earthquake magnitudes; the
// minimum magnitude in our data set is 2.5.
var colors = d3.scale.pow()
.exponent(3)
.domain([2, 4, 6, 8, 10])
.range(['white', 'yellow', 'orange', 'red', 'purple']);
// Also create a scale for mapping magnitudes to ping angle sizes
var angles = d3.scale.pow()
.exponent(3)
.domain([2.5, 10])
.range([0.5, 15]);
// And finally, a scale for mapping magnitudes to ping TTLs
var ttls = d3.scale.pow()
.exponent(3)
.domain([2.5, 10])
.range([2000, 5000]);
// Create a key to show the magnitudes and their colors
d3.select('#magnitudes').selectAll('li')
.data(colors.ticks(9))
.enter()
.append('li')
.style('color', colors)
.text(function(d) {
return "Magnitude " + d;
});
// Load our earthquake data and set up the controls.
// The data consists of an array of objects in the following format:
// {
// mag: magnitude_of_quake
// lng: longitude_coordinates
// lat: latitude_coordinates
// time: timestamp_of_quake
// }
// The data is ordered, with the earliest data being the first in the file.
d3.json('/examples/quake/year_quakes_small.json', function(err, data) {
if (err) {
alert("Problem loading the quake data.");
return;
}
var start = parseInt(data[0].time, 10);
var end = parseInt(data[data.length - 1].time, 10);
var currentTime = start;
var lastTick = new Date().getTime();
var updateDate = function() {
d3.select('#date').text(moment(currentTime).utc().format("MMM DD YYYY HH:mm UTC"));
};
// A scale that maps a percentage of playback to a time
// from the data; for example, `50` would map to the halfway
// mark between the first and last items in our data array.
var percentToDate = d3.scale.linear()
.domain([0, 100])
.range([start, end]);
// A scale that maps real time passage to data playback time.
// 12 minutes of real time maps to the entirety of the
// timespan covered by the data.
var realToData = d3.scale.linear()
.domain([0, 1000 * 60 * 12])
.range([0, end - start]);
var paused = false;
// Pause playback and update the time display
// while scrubbing using the range input.
d3.select('#slider')
.on('change', function(d) {
currentTime = percentToDate(d3.event.target.value);
updateDate();
})
.call(d3.behavior.drag()
.on('dragstart', function() {
paused = true;
})
.on('dragend', function() {
paused = false;
})
);
// The main playback loop; for each tick, we'll see how much
// time passed in our accelerated playback reel and find all
// the earthquakes that happened in that timespan, adding
// them to the globe with a color and angle relative to their magnitudes.
d3.timer(function() {
var now = new Date().getTime();
if (paused) {
lastTick = now;
return;
}
var realDelta = now - lastTick;
// Avoid switching back to the window only to see thousands of pings;
// if it's been more than 500 milliseconds since we've updated playback,
// we'll just set the value to 500 milliseconds.
if (realDelta > 500) realDelta = 500;
var dataDelta = realToData(realDelta);
var toPing = data.filter(function(d) {
return d.time > currentTime && d.time <= currentTime + dataDelta;
});
for (var i = 0; i < toPing.length; i++) {
var ping = toPing[i];
planet.plugins.pings.add(ping.lng, ping.lat, {
// Here we use the `angles` and `colors` scales we built earlier
// to convert magnitudes to appropriate angles and colors.
angle: angles(ping.mag),
color: colors(ping.mag),
ttl: ttls(ping.mag)
});
}
currentTime += dataDelta;
if (currentTime > end) currentTime = start;
updateDate();
d3.select('#slider').property('value', percentToDate.invert(currentTime));
lastTick = now;
});
});
// Plugin to resize the canvas to fill the window and to
// automatically center the planet when the window size changes
function autocenter(options) {
options = options || {};
var needsCentering = false;
var globe = null;
var resize = function() {
var width = window.innerWidth + (options.extraWidth || 0);
var height = window.innerHeight + (options.extraHeight || 0);
globe.canvas.width = width;
globe.canvas.height = height;
globe.projection.translate([width / 2, height / 2]);
};
return function(planet) {
globe = planet;
planet.onInit(function() {
needsCentering = true;
d3.select(window).on('resize', function() {
needsCentering = true;
});
});
planet.onDraw(function() {
if (needsCentering) { resize(); needsCentering = false; }
});
};
};
// Plugin to automatically scale the planet's projection based
// on the window size when the planet is initialized
function autoscale(options) {
options = options || {};
return function(planet) {
planet.onInit(function() {
var width = window.innerWidth + (options.extraWidth || 0);
var height = window.innerHeight + (options.extraHeight || 0);
planet.projection.scale(Math.min(width, height) / 2);
});
};
};
// Plugin to automatically rotate the globe around its vertical
// axis a configured number of degrees every second.
function autorotate(degPerSec) {
return function(planet) {
var lastTick = null;
var paused = false;
planet.plugins.autorotate = {
pause: function() { paused = true; },
resume: function() { paused = false; }
};
planet.onDraw(function() {
if (paused || !lastTick) {
lastTick = new Date();
} else {
var now = new Date();
var delta = now - lastTick;
var rotation = planet.projection.rotate();
rotation[0] += degPerSec * delta / 1000;
if (rotation[0] >= 180) rotation[0] -= 360;
planet.projection.rotate(rotation);
lastTick = now;
}
});
};
};
})();
Your globe is not loading for multiple reasons. First, if you open up your console you would see that the external resources were not loaded properly. As the error notes, your external resources failed to load because:
MIME type ('text/plain') is not executable, and strict MIME type checking is enabled.
You could overcome this issue in a twofold manner. You could either refer to this answer--which will help you sort the MIME type issue--or you may just use other links for you external resources. If you rather take the second route, you should add the links below to your external resources section, in the following order:
moment.js
d3.js
planetary.js
After you solved the first issue, you will see that you would not be able to link your JSON file by simply adding it to your external resources. These answers will offer you multiple approaches that will help you solve the JSON issue as well.
After you have successfully linked all of your external files, you will face a third problem. If you look at your code you will see that you are trying to get the time property for your data (JSON) object in line 75--var start = parseInt(data[0].time, 10);. However, as far as I could tell, your data object does not hold a time property--go ahead, and console.log() your data object to see its structure and properties. In other words, you might want to double check if the world-110m.json is the file that you want to work with.
Hope this helps.

animation glitch in sunburst

I started with the default Sunburst example and tried to add an animation on click - clicking on a wedge makes that wedge the center element. This works great except in a few cases - it looks like if the depth goes above 2 (0 based) then some wedges don't animate correctly; they seem to start the animation in the wrong place, but end up in the correct place.
Here is what I think is the relevant code, and I hope it's useful to someone else:
animate = function (d) {
var oldDs = [], topD, includeds;
if (animating)
return;
animating = true;
topD = (d == selected && d.parent) ? d.parent : d;
if (selected == topD) {
animating = false;
return;
}
selected = topD;
svg.selectAll("path").filter(function (f) {
return (!isDescendant(f, topD))
}).transition().duration(1000).style("opacity", "0").attr("pointer-events","none");
includeds = svg.datum(topD).selectAll("path").filter(function (f) {
return isDescendant(f, topD);
});
// step 1 - save the current arc settings
includeds.data().each(function (d) {
oldDs[d.ac_id] = {x: d.x, dx: d.dx, y: d.y, dy: d.dy};
});
// step 2 - recalculate arc settings and animate
includeds.data(partition.nodes) // recalculates
.transition().duration(1000).attrTween("d", function (d) {
var oldD = oldDs[d.ac_id],
interpolator = d3.interpolateObject(oldD, d);
return function (t) {
return arc(interpolator(t));
}
}).style("opacity", "1").attr("pointer-events","all");
setTimeout(function(){ animating = false; }, 1000);
};
Because my code doesn't have anything to do with the depth, I expect the issue is somewhere else, but I haven't been able to find it. The glitch seems to affect all browsers (at least Chrome, Firefox, and IE).
So, my question: what's the issue and how can I fix it?

Categories

Resources