Using d3v4, react and chai (chai-enzyme, chai-jquery) for unit testing.
So I have a zoom behavior attached to my graph.
const zoom = d3
.zoom()
.scaleExtent([1, Infinity])
.on('zoom', () => {
this.zoomed()
})
And the zoom behavior is attached to the svg element.
const svg = d3
.select(this.node)
.select('svg')
.attr('preserveAspectRatio', 'xMinYMin meet')
.attr('viewBox', `0 0 ${svgWidth} ${svgHeight}`)
.classed('svg-content-responsive', true)
.select('g')
.attr('transform', `translate(${margin.left},${margin.top})`)
.attr('height', height + margin.top + margin.bottom)
.attr('width', width + margin.left + margin.right)
.call(zoom)
The on('zoom') callback is defined as
zoomed () {
const {gXAxis, plotPlanText, plotZones, width, xAxis, xScale} = this.graphObjects
const transform = d3.event.transform
// get xMin and xMax in the viewable world
const [xMin, xMax] = xScale.domain().map(d => xScale(d))
// Get reverse transform for xMin and xMax.
if (transform.invertX(xMin) < 0) {
transform.x = -xMin * transform.k
}
if (transform.invertX(xMax) > width) {
transform.x = xMax - width * transform.k
}
// transform the bars for zones
if (!isNaN(transform.x) && !isNaN(transform.y) && !isNaN(transform.k)) {
// rescale the x linear scale so that we can draw the x axis
const xNewScale = transform.rescaleX(xScale)
xAxis.scale(xNewScale)
// rescale xaxis
gXAxis.call(xAxis)
plotZones
.selectAll('rect')
.attr('x', (d, i) => transform.applyX(xScale(d.maxFrequency)))
.attr('width', (d) => -(transform.applyX(xScale(d.maxFrequency)) - transform.applyX(xScale(d.minFrequency))))
// transform the flow text
plotPlanText
.selectAll('.plan-text-src')
.attr('x', d => (transform.applyX(xScale(d.maxFrequency)) + transform.applyX(xScale(d.minFrequency))) / 2)
plotPlanText
.selectAll('.plan-text-dest')
.attr('x', d => (transform.applyX(xScale(d.maxFrequency)) + transform.applyX(xScale(d.minFrequency))) / 2)
}}
My unit test for firing up the "zoom" event is
describe('d3 Event handling', function () {
beforeEach(function () {
$.fn.triggerSVGEvent = function (eventName, delta) {
let event = new Event(eventName, {'bubbles': true, 'cancelable': false})
if (delta) {
event.deltaX = delta.x
event.deltaY = delta.y
}
this[0].dispatchEvent(event)
return $(this)
}
})
describe('When the chart is zoomed', function () {
let initialX
beforeEach(function () {
$('.flow-zone-container > svg > g').triggerSVGEvent('wheel')
})
it('should update elements when zoomed', function () {
...
})
})
})
When I use chai-jquery to trigger the "wheel" event, the zoom event is fired but the d3.event.tranform in the zoomed() method gives NaN for x, y and k. I want to test the zoomed() callback such that I have the d3.event.transform values to test the logic in the zoomed() method.
How can I fire the zoomEvent using "wheel" event or any other event such that d3.event is fired with some values?
Solved this. Instead of using the jquery to fire the event, I am now using zoomBehaviour.scaleBy(selection, scale) to fire the callback.
Related
Here is my map svg:
useEffect(() => {
const svg = select(svgRef.current)
const { width, height } = wrapperRef.current.getBoundingClientRect();
// projects geo-coordinates on a 2D plane
const projection = geoMercator()
.fitSize([width, height], data)
.precision(100);
// takes geojson data,
// transforms that into the d attribute of a path element
const pathGenerator = geoPath().projection(projection);
initZoom()
// render each country
svg
.selectAll(".country")
.data(data.features)
.join("path")
.attr("class", "country")
.transition()
.attr("fill", 'lightgrey')
.attr("d", feature => pathGenerator(feature));
;
}, [data ]);
And here is the zoom logic:
let zoom = d3.zoom()
.on('zoom', handleZoom);
function handleZoom(e) {
d3.select(svgRef.current)
.attr('transform', e.transform)
}
function initZoom() {
d3.select(svgRef.current)
.call(zoom);
}
Instead of zooming in around the mouse pointer like with examples, it instead zooms out from the bottom right corner. Why is this and how do I fix it?
I am using React to develop a real-time paint app. So, the idea is to store mouse events in an array(to pass it through socket) and pass it to draw function. However, when I move mouse fast, I'm getting a dotted line instead of smooth line. If I directly draw using mouse events instead of an array, I'm getting a smooth line. So I guess the issue is in pushing mouse events into the array.
This is my output:
The following is my PaintCanvas component
function PaintCanvas(props) {
let ctx;
const canvasRef = useRef("");
const [isDrawing, changeIsDrawing] = useState(false);
let strokes = [];
const mouseDownFunction = e => {
changeIsDrawing(true);
if (ctx) {
wrapperForDraw(e);
}
};
const mouseUpFunction = e => {
if (ctx) {
ctx.beginPath();
}
changeIsDrawing(false);
};
const mouseMoveFunction = e => {
if (ctx) {
wrapperForDraw(e);
}
};
const wrapperForDraw = e => {
if (!isDrawing) return;
strokes.push({
x: e.clientX,
y: e.clientY
});
drawFunction(strokes);
};
const drawFunction = strokes => {
let { top, left } = canvasRef.current.getBoundingClientRect();
if (!isDrawing) return;
ctx.lineWidth = 3;
ctx.lineCap = "round";
for (let i = 0; i < strokes.length; i++) {
ctx.beginPath();
//adding 32px to offset my custom mouse icon
ctx.moveTo(strokes[i].x - left, strokes[i].y - top + 32);
ctx.lineTo(strokes[i].x - left, strokes[i].y - top + 32);
ctx.closePath();
ctx.stroke();
}
};
useEffect(() => {
let canvas = canvasRef.current;
ctx = canvas.getContext("2d");
});
return (
<div>
<canvas
ref={canvasRef}
width="500px"
height="500px"
onMouseDown={mouseDownFunction}
onMouseUp={mouseUpFunction}
onMouseMove={mouseMoveFunction}
className={styles.canvasClass}
/>
</div>
);
}
export default PaintCanvas;
How can I get a smooth line using the array implementation.
In your loop where you are drawing the lines, you don't need to call moveTo every iteration. Each call of lineTo() automatically adds to the current sub-path, which means that all the lines will all be stroked or filled together.
You need to pull beginPath out of the loop, remove the moveTo call and take the stroke outside the loop for efficiency.
ctx.beginPath();
for (let i = 0; i < strokes.length; i++) {
//adding 32px to offset my custom mouse icon
//ctx.moveTo(strokes[i].x - left, strokes[i].y - top + 32); // Remove this line
ctx.lineTo(strokes[i].x - left, strokes[i].y - top + 32);
}
// Its also more efficient to call these once for the whole line
ctx.stroke();
I am rendering Highcharts 3D scatter graph and enabled scrolling as follows
xAxis: {
categories: xlist,
min: 0,
max: 4,
scrollbar: {
enabled: true
},
}
Also having chart scrolling with below code
$(chart.container).on('mousedown.hc touchstart.hc', function (eStart) {
eStart = chart.pointer.normalize(eStart);
var posX = eStart.chartX,
posY = eStart.chartY,
alpha = chart.options.chart.options3d.alpha,
beta = chart.options.chart.options3d.beta,
newAlpha,
newBeta,
sensitivity = 5; // lower is more sensitive
$(document).on({
'mousemove.hc touchmove.hc': function (e) {
e = chart.pointer.normalize(e);
newBeta = beta + (posX - e.chartX) / sensitivity;
chart.options.chart.options3d.beta = newBeta;
newAlpha = alpha + (e.chartY - posY) / sensitivity;
chart.options.chart.options3d.alpha = newAlpha;
chart.redraw(false);
},
'mouseup touchend': function () {
$(document).off('.hc');
}
});
});
If I scroll through scrollbar then the chart also moves. Is there any way to avoid chart scrolling if I scroll the scrollbar and if I take mouse on chart then the chart should move.
I made a hack and updated the above function as below, where I have restricted the chart rotation with if(e.target.classList.length == 0) when we use scrollbar.
$(chart.container).on('mousedown.hc touchstart.hc', function (eStart) {
eStart = chart.pointer.normalize(eStart);
var posX = eStart.chartX,
posY = eStart.chartY,
alpha = chart.options.chart.options3d.alpha,
beta = chart.options.chart.options3d.beta,
newAlpha,
newBeta,
sensitivity = 5; // lower is more sensitive
$(document).on({
'mousemove.hc touchmove.hc': function (e) {
if (e.target.classList.length == 0) {
e = chart.pointer.normalize(e);
newBeta = beta + (posX - e.chartX) / sensitivity;
chart.options.chart.options3d.beta = newBeta;
newAlpha = alpha + (e.chartY - posY) / sensitivity;
chart.options.chart.options3d.alpha = newAlpha;
chart.redraw(false);
}
},
'mouseup touchend': function () {
$(document).off('.hc');
}
});
});
My exercise with Leaflet.js : resize the icon of a marker when zooming in or out by modifying the iconSize option (ie not by changing the icon source).
I tried this :
function resize(e) {
for (const marker of markers) {
const newY = marker.options.icon.options.iconSize.y * (mymap.getZoom() / parameterInitZoom);
const newX = marker.options.icon.options.iconSize.x * (mymap.getZoom() / parameterInitZoom);
marker.setIcon(marker.options.icon.options.iconsize = [newX, newY]);
}
}
mymap.on('zoomend', resize)
but I ended up with :
t.icon.createIcon is not a function
I saw also the method muliplyBy but couldn't find out the way to make it work.
How to do it ?
What I did finally, and it's working well :
let factor;
let markers = [];
//New class of icons
const MyIcon = L.Icon.extend({
options: {
iconSize: new L.Point(iconInitWidth, iconInitHeight) //Define your iconInitWidth and iconInitHeight before
},
});
/*------------ Functions - Callbacks ----------------*/
//Small function to keep the factor up to date with the current zoom
function updateFactor() {
let currentZoom = mymap.getZoom();
factor = Math.pow(currentZoom / mymap.options.zoom, 5);
};
updateFactor();
//Create a new marker
function makeMarker(e) {
const newX = Math.round(iconInitWidth * factor);
const newY = newX * iconInitHeight / iconInitWidth;
const newMarker = new L.Marker(new L.LatLng(e.latlng.lat, e.latlng.lng), {
icon: new MyIcon({ iconSize: new L.Point(newX, newY) })
}).addTo(mymap);
markers[markers.length] = newMarker;
}
//Update the marker
function resize(e) {
updateFactor();
for (const marker of markers) {
const newX = Math.round(iconInitWidth * factor);
const newY = newX * iconInitHeight / iconInitWidth;
marker.setIcon(new MyIcon({ iconSize: new L.Point(newX, newY) }));
}
}
/*------------ Event listeners ----------------*/
mymap.addEventListener('click', makeMarker);
mymap.on('zoom', resize);
#mbostock
I have a draggable panel Extjs and i have included a graph d3js, customized drag and drop and added the zoom function.
In a simple html page all work fine, but in Extjs panel when drag a node in addition to move that, also activates pan, i would not disable the pan.
where I'm wrong?
This is the ExtjsPanel:
Ext.namespace("libs");
CSP.prj.tresprj.trestest.libs.MainPanel = Ext.extend(Ext.Panel, {
initComponent : function(){
this.addEvents({
'afterrender': true
});
Ext.apply(this,{title:'My Custom Panel2'});
Ext.apply(this,{html:'<div id="content"><div id="buttonDiv"><p>Selezionare il test da effettuare</p></div><!--buttonDiv--><div id="svgContent"></div><!--svgContent--></div><!--content-->'});
this.addListener('afterrender',function(){
Xxx.createButton("buttonDiv");
});
libs.MainPanel.superclass.initComponent.call(this);
}
});
and this is the Graph code:
var initSvg = function (target) {
//Activate context menu on right click
d3.select("body").attr("oncontextmenu", "return true");
//Create SVG element
if (jQuery(target + " svg").length) {
clearSvgDiv();
}
var svg = d3.select(target)
.append("svg")
.attr("width", width)
.attr("height", heigth)
.attr("id", "drawSvg")
.attr('preserveAspectRatio', 'xMinYMin slice')
.append('g');
return svg;
};
var clearSvgDiv = function () {
d3.select("svg")
.remove();
};
// Init force layout
var initLayout = function (svg) {
svg.append('svg:rect')
.attr('width', width)
.attr('height', heigth)
.attr('fill', 'white');
force = d3.layout.force()
.nodes(graph.getNodes())
.links(graph.getEdges())
.size([width, heigth])
.linkDistance([50])
.charge([-300])
.start();
};
// Creates nodes and edge to draw them on the page, calculated positions and repulsions.
var createNodesEdges = function (svg) {
var x = d3.scale.linear()
.domain([0, width])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, heigth])
.range([heigth, 0]);
svg.call(d3.behavior.zoom().x(x).y(y).scaleExtent([1, 8]).on("zoom", rescale));
var path = svg.append("svg:g").selectAll("path")
.data(graph.getEdges())
.enter()
.insert("svg:path")
.attr("class", "line")
.style("stroke", "#ccc");
var node_drag = d3.behavior.drag()
.on("dragstart", dragstart)
.on("drag", dragmove)
.on("dragend", dragend);
var nodes = svg.selectAll("circle")
.data(graph.getNodes(), function (d) { return d.id; })
.enter()
.append("circle")
.attr("id", function (d) { return d.id; })
.attr("r", 5)
.call(node_drag);
force.on("tick", tick);
function tick() {
path.attr("d", function (d) {
var coordinatesP = findEdgeControlPoints(d);
return "M" + d.source.x + "," + d.source.y + "S" + coordinatesP.xp + "," + coordinatesP.yp + " " + d.target.x + "," + d.target.y;
});
nodes.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; });
}
function dragstart(d, i) {
force.stop(); // stops the force auto positioning before you start dragging
}
function dragmove(d, i) {
d.px += d3.event.dx;
d.py += d3.event.dy;
d.x += d3.event.dx;
d.y += d3.event.dy;
tick();
}
function dragend(d, i) {
d.fixed = true; // of course set the node to fixed so the force doesn't include the node in its auto positioning stuff
tick();
force.resume();
}
};
var rescale = function () {
var trans = d3.event.translate;
var scale = d3.event.scale;
svg.attr("transform","translate(" + trans + ")" + " scale(" + scale + ")");
};
i've found the problem, isn't extjs but minified version of d3js library downloaded by http://d3js.org/, with extended version i've no problem.