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');
}
});
});
Related
When I click on the spin button, I want the value to be highlighted one by one in the canvas.
Here you can see the whole code: https://codesandbox.io/s/spinning-wheel-react-forked-s528m
renderWheel() {
// determine number/size of sectors that need to created
let numOptions = this.state.list.length;
let arcSize = (2 * Math.PI) / numOptions;
this.setState({
angle: arcSize
});
// dynamically generate sectors from state list
let angle = 0;
for (let i = 0; i < numOptions; i++) {
let text = this.state.list[i];
this.renderSector(i + 1, text, angle, arcSize);
angle += arcSize;
}
}
renderSector(index, text, start, arc) {
// create canvas arc for each list element
let canvas = document.getElementById("wheel");
let ctx = canvas.getContext("2d");
let x = canvas.width / 2;
let y = canvas.height / 2;
let radius = 200;
let startAngle = start;
let endAngle = start + arc - 0.02;
let angle = index * arc;
let baseSize = radius * 1.25;
let textRadius = baseSize - 48;
ctx.beginPath();
ctx.arc(x, y, radius, startAngle, endAngle, false);
ctx.lineWidth = radius * 0.25;
ctx.strokeStyle = "white";
ctx.font = "25px Arial";
ctx.fillStyle = "blue";
ctx.stroke();
ctx.save();
ctx.translate(
baseSize + Math.cos(angle - arc / 2) * textRadius,
baseSize + Math.sin(angle - arc / 2) * textRadius
);
ctx.rotate(0);
ctx.fillText(text, -ctx.measureText(text).width / 2, 7);
ctx.restore();
}
When I click on the spin button, this spin () method is executed.
The iteration ends between two and five seconds as written in the setTimeout method.
spin = () => {
var index = 0;
let timer = setInterval(() => {
index = (index) % this.state.list.length + 1;
console.log(index)
}, 150);
// calcalute result after wheel stops spinning
setTimeout(() => {
clearInterval(timer);
this.setState({
hasFinishedLoading: true,
display: this.state.list[index]
});
}, Math.floor(Math.random() * 5000 + 2000));
this.setState({
spinning: true
});
};
Remember: In canvas you cannot access a particular object, so the solution is always to erase and redraw. Don't worry about performance, this is pretty great. I leave you an idea with your code and at the end some suggestions. Try this code, the only thing I did is clean the canvas and redraw without the setTimeOut index
spin = () => {
let canvas = document.getElementById("wheel");
let ctx = canvas.getContext("2d");
var index = 0;
index = (index % this.state.list.length) + 1;
let angle = 0;
let numOptions = this.state.list.length;
let arcSize = (2 * Math.PI) / numOptions;
let timer = setInterval(() => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
this.setState({
angle: arcSize
});
for (let i = 0; i < numOptions; i++) {
if (i !== index) {
let text = this.state.list[i];
this.renderSector(i + 1, text, angle, arcSize);
angle += arcSize;
} else {
}
}
}, 150);
// calcalute result after wheel stops spinning
setTimeout(() => {
clearInterval(timer);
this.setState({
hasFinishedLoading: true,
display: this.state.list[index]
});
}, Math.floor(Math.random() * 5000 + 2000));
this.setState({
spinning: true
});
};
Don't use "getElementById" in react, it's dangerous. Use a reference.
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();
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);
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.
I'm writing a Leaflet plugin that extends the polyline functionality. In the my plugin I'm accessing the path segments using the SVGPathSegList interface. But according to the Chrome DevTools the interface will be removed in Chrome 48. I'm seeking for another possibility to access the the path segments.
Here's my fiddle.
(function () {
var __onAdd = L.Polyline.prototype.onAdd,
__onRemove = L.Polyline.prototype.onRemove,
__updatePath = L.Polyline.prototype._updatePath,
__bringToFront = L.Polyline.prototype.bringToFront;
L.Polyline.include({
onAdd: function (map) {
__onAdd.call(this, map);
this._textRedraw();
},
onRemove: function (map) {
__onRemove.call(this, map);
},
bringToFront: function () {
__bringToFront.call(this);
this._textRedraw();
},
_textRedraw: function () {
var textNodes = this._path.parentElement.getElementsByTagName('text'),
tnIndex;
if (textNodes.length > 0) {
for (tnIndex = textNodes.length - 1; tnIndex >= 0; tnIndex -= 1) {
textNodes[tnIndex].parentNode.removeChild(textNodes[tnIndex]);
}
}
if (this.options.measurements) {
this.setText();
}
},
setText: function () {
var path = this._path,
points = this.getLatLngs(),
pathSeg,
prevPathSeg,
center,
angle,
rotation,
textNode;
/*
* If not in SVG mode or Polyline not added to map yet return
* setText will be called by onAdd, using value stored in this._text
*/
if (!L.Browser.svg || typeof this._map === 'undefined') {
return this;
}
for (pathSeg = 0; pathSeg < path.pathSegList.length; pathSeg += 1) {
if (pathSeg > 0) {
prevPathSeg = path.pathSegList[pathSeg - 1];
center = this._calcCenter(
prevPathSeg.x,
prevPathSeg.y,
path.pathSegList[pathSeg].x,
path.pathSegList[pathSeg].y
);
angle = this._calcAngle(
prevPathSeg.x,
prevPathSeg.y,
path.pathSegList[pathSeg].x,
path.pathSegList[pathSeg].y
);
rotation = 'rotate(' + angle + ' ' +
center.x + ',' + center.y + ')';
debugger;
textNode = document
.createElementNS('http://www.w3.org/2000/svg', 'text');
textNode.setAttribute('text-anchor', 'middle');
textNode.setAttribute('x', center.x);
textNode.setAttribute('y', center.y);
textNode.setAttribute('transform', rotation);
textNode.textContent = points[pathSeg - 1]
.distanceTo(points[pathSeg]);
this._path.parentElement.appendChild(textNode);
} else {
continue;
}
}
},
_calcCenter: function (x1, y1, x2, y2) {
return {
x: (x1 + x2) / 2,
y: (y1 + y2) / 2
}
},
_calcAngle: function (x1, y1, x2, y2) {
return Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
},
_updatePath: function () {
__updatePath.call(this);
this._textRedraw();
}
});
})();
#holger-will gave some very useful links.
You can modify your js code to use the new API.
for example:
Use var pathData = path.getPathData() instead of old var segs = path.pathSegList;
Use pathData[1].values[4] instead of old path.pathSegList.getItem(1).x
Use path.setPathData(pathData) to update the path element instead of old path.pathSegList.appendItem/insertItem/removeItem
Include the path-data-polyfill.js for browsers which have not supported the new API.
(Chrome 50 still has not implemented getPathData and setPathData. There may be a long way...)
Here is a code sample:
//svg code:
//...
//<path d="M0,0 L100,50" id="mypath"></path>
//<script href="/js/path-data-polyfill.js"></script>
//...
//js code:
var path = document.getElementById('mypath');
var pathdata = path.getPathData();
console.log(pathdata);
console.log(pathdata.length); //2
console.log(pathdata[0].type); //"M"
console.log(pathdata[0].values); //[0,0]
console.log(pathdata[1].type); //"L"
console.log(pathdata[1].values); //[100,50]
pathdata.push({type: "C", values: [100,-50,200,150,200,50]}); //add path segment
path.setPathData(pathdata); //set new path data
console.log(path.getAttribute('d')); //"M0,0 L100,50 C100,-50,200,150,200,50"
path data polyfill: https://github.com/jarek-foksa/path-data-polyfill