Are there any issues when using Konva on iOS mobile devices? - ios

I am using a Konva stage to hover over floor areas on browser (see running Next app here https://www.planpoint.io/themes/modern-1) and it works great on any device, except on iPhones. After some touches on the floors areas, they suddenly dissapear as if the canvas element would no longer be there. Has tested it with Safari and Chrome on iOS16.2 . The only difference for mobile devices is the use of touchstart, touchend events, instead of mouseover, mouseout events.
Here is the code for rendering the canvas component and background image
import { useEffect, useState, useRef, useLayoutEffect } from 'react'
import Image from 'next/image'
import Konva from 'konva';
import i18nService from '../../../helpers/i18nService'
import useWindowSize from '../../../hooks/useWindowSize'
export default function InteractiveCanvas(props) {
const [showCanvas, setShowCanvas] = useState(false);
const [placeholderWidth, setPlaceholderWidth] = useState(1);
const [placeholderHeight, setPlaceholderHeight] = useState(1);
const [width, height] = useWindowSize();
const canvasAreaRef = useRef(null)
const canvasPlaceholderRef = useRef()
let stage, shapesLayer, tooltipLayer;
useEffect(() => {
if (showCanvas) {
props.loaded() // Canvas has loaded
}
},[showCanvas])
useLayoutEffect(() => { initializeCanvas() });
useEffect(() => { initializeCanvas() }, [width, height]);
function initializeCanvas() {
if (!canvasPlaceholderRef.current || !canvasPlaceholderRef.current.clientWidth || !canvasPlaceholderRef.current.clientHeight || !canvasPlaceholderRef.current.clientWidth) {
// wait until the placeholder is available
setTimeout(function() { initializeCanvas() }, 1000)
} else if (canvasPlaceholderRef.current) {
// initialize placeholder
setPlaceholderWidth(canvasAreaRef.current.clientWidth)
setPlaceholderHeight(canvasPlaceholderRef.current.clientHeight)
// initialize konva
let width = canvasPlaceholderRef.current.clientWidth;
let height = canvasPlaceholderRef.current.clientHeight;
stage = new Konva.Stage({
container: props.konvaContainer,
width: width,
height: height,
name: 'stage'
});
shapesLayer = new Konva.Layer({ name: 'shapes' });
tooltipLayer = new Konva.Layer({ name: 'tooltips' });
let tooltip = new Konva.Label({
opacity: 1,
visible: false,
listening: false,
name: 'label'
});
tooltip.add(
new Konva.Tag({
fill: '#313131',
pointerDirection: 'down',
pointerWidth: 20,
pointerHeight: 10,
cornerRadius: 4,
lineJoin: 'round',
shadowColor: 'black',
shadowBlur: 10,
shadowOffsetX: 10,
shadowOffsetY: 10,
shadowOpacity: 0.25,
name: 'tag'
})
);
tooltip.add(
new Konva.Text({
text: '',
align: 'center',
lineHeight: 2,
fontFamily: 'Inter',
fontSize: 13,
padding: 5,
fill: 'white',
name: 'tag',
width: props.wideTooltip ? 160 : 80
})
);
tooltipLayer.add(tooltip);
let areas = getData();
// draw areas
for (let key in areas) {
let area = areas[key];
let shape = new Konva.Line({
stroke: 'white',
strokeWidth: 2,
points: area.points,
fill: area.color,
opacity: area.disabled || area.visible ? 1 : key === props.selected?.name ? 1 : 0, // Keep the selected floor/unit highlight shape with opacity 1 after render
disabled: area.disabled,
closed: true,
target: area.target,
key: key,
subline: area.subline,
perfectDrawEnabled: false,
name: 'line',
cursor: 'pointer'
});
let group = new Konva.Group({name: 'group'})
shapesLayer.add(group);
group.add(shape);
}
stage.add(shapesLayer);
stage.add(tooltipLayer);
stage.on('mouseover touchstart', function (evt) {
if (evt && evt.target) {
let shape = evt.target;
if (shape && !shape.attrs.editable) {
shape.opacity(1);
shapesLayer.draw();
if (evt.type === 'touchstart' && shape.attrs.name === 'line') props.onClick(shape.attrs.target)
}
}
});
stage.on('mouseout touchend', function (evt) {
if (evt && evt.target) {
let shape = evt.target;
if(shape.attrs.name === 'stage' || shape.attrs.target === props.selected?._id) return // Do nothing unless it is a different line shape
if (shape && !shape.attrs.editable) {
shape.opacity(shape.attrs.disabled || shape.attrs.visible ? 1 : 0);
shapesLayer.draw();
tooltip.hide();
tooltipLayer.draw();
}
}
});
stage.on('mousemove', function(evt) {
let shape = evt.target;
if (shape) {
let mousePos = stage.getPointerPosition();
let x = mousePos.x;
let y = mousePos.y - 5;
updateTooltip(tooltip, x, y, shape.attrs.key, shape.attrs.subline);
tooltipLayer.draw();
}
});
stage.on('click tap', function(evt) {
let shape = evt.target;
if(shape.attrs.name === 'stage' || shape.attrs.target === props.selected?._id) return // Do nothing unless it is a different line shape
if (shape && shape.attrs.target) props.onClick(shape.attrs.target)
})
setTimeout(() => {
setShowCanvas(true)
shapesLayer.draw();
tooltipLayer.draw();
}, 100)
}
}
function getData() {
let areas = {}
function transformPoints(points) {
const c = JSON.parse(points || '[]')
return c.map((e, i) => (i % 2) ? (placeholderWidth * e) : placeholderHeight * e)
}
// add inactive paths
for (let p of props.inactivePaths) {
let newData = {
target: p.target,
color: p.disabled ? (props.disabledColor || '#E1171799') : props.accentColor || 'rgba(15, 33, 49, 0.65)',
points: transformPoints(p.path),
visible: p.visible,
disabled: p.disabled
}
if (props.project && props.project.showFloorOverview) {
let availableUnits = p.units.filter(u => u.availability.toLowerCase() === 'available').length
newData.subline = props.project.showFloorOverview ? `${availableUnits} ${i18nService.i18n(props.locale, "canvas.unitsavailable")}` : ''
}
areas[p.title] = newData
}
return areas;
}
function updateTooltip(tooltip, x, y, text, subline) {
const conditionalSubline = subline ? `\n${subline}` : ''
tooltip.children[1].text(text+conditionalSubline);
// tooltip.getText().text(text);
tooltip.position({ x: x, y: y });
if (text) tooltip.show();
};
return (
<div className={props.styles.canvasContainer}>
<img
className={props.styles.canvasImgPlaceholder}
ref={canvasPlaceholderRef}
alt='Canvas Placeholder'
src={props.background}
/>
<div className={props.styles.canvasPlaceholderBox} data-hide={showCanvas}>
<Image src="/images/planpoint_icon.svg" width={200} height={200} alt="" />
</div>
<div
className={props.styles.canvasKonvaContainer}
id={props.konvaContainer}
ref={canvasAreaRef}
data-hide={!showCanvas}
style={{backgroundImage: `url('${props.background}')`}}
>
</div>
</div>
)
}

Related

Highcharts react X-range: how to add a custom component?

I have an x-range chart, and I need to add custom components (SVG icons) at the start of each data point ("x" prop value). Something like this (red circles are where custom components should be):
My idea is to place a custom component with position absolute, but I can't figure out how to transform timestamp values of the X axis to pixels. Or maybe there is a better solution?
Codesandbox demo
There are multiple ways to achieve what you want:
A. You can get a chart reference and use Axis.toPixels method to calculate the required coordinates:
export default function Chart() {
const chartComponentRef = useRef(null);
const [x1, setX1] = useState(0);
useEffect(() => {
if (chartComponentRef) {
const chart = chartComponentRef.current.chart;
const xAxis = chart?.xAxis[0];
const x1 = xAxis?.toPixels(Date.UTC(2022, 7, 10));
setX1(x1);
}
}, [chartComponentRef]);
return (
<div style={{ position: "relative" }}>
<HighchartsReact
ref={chartComponentRef}
highcharts={Highcharts}
options={staticOptions}
/>
<div style={{ position: "absolute", bottom: 70, left: x1 }}>ICON</div>
</div>
);
}
Live demo: https://codesandbox.io/s/highcharts-react-custom-componentt-ysjfh8?file=/src/Chart.js
API Reference: https://api.highcharts.com/class-reference/Highcharts.Axis#toPixels
B. You can also get coordinates from a point:
useEffect(() => {
if (chartComponentRef) {
const chart = chartComponentRef.current.chart;
const point = chart.series[0].points[2];
setX1(chart.plotLeft + point.plotX);
}
}, [chartComponentRef]);
Live demo: https://codesandbox.io/s/highcharts-react-custom-componentt-f7nveg?file=/src/Chart.js
C. Use Highcharts API to render and update the custom icons:
const staticOptions = {
chart: {
type: "xrange",
height: 200,
events: {
render: function () {
const chart = this;
const r = 10;
const distance = 25;
chart.series[0].points.forEach((point, index) => {
if (!point.customIcon) {
point.customIcon = chart.renderer
.circle()
.attr({
fill: "transparent",
stroke: "red",
"stroke-width": 2
})
.add();
}
point.customIcon.attr({
x: point.plotX + chart.plotLeft + r,
y:
point.plotY +
chart.plotTop +
(index % 2 ? distance : -distance),
r
});
});
}
}
},
...
};
Live demo: https://codesandbox.io/s/highcharts-react-custom-componentt-f-87eof0?file=/src/Chart.js
API Reference: https://api.highcharts.com/class-reference/Highcharts.SVGRenderer#circle

Is there a way to set cursor to Konvajs transformer anchor

I want to set cursor to anchor on mouse enter event.
I tried the following code, but without success
const tr = new Konva.Transformer({
nodes: [shape]
})
tr.update = function() {
Konva.Transformer.prototype.update.call(tr);
var rot = this.findOne('.rotater');
rot.on("mouseenter", () => {
stage.container().style.cursor = 'move';
})
rot.on('mouseleave', () => {
stage.container().style.cursor = 'default';
})
}
tr.forceUpdate();
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const shape = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 50,
fill: 'green'
});
layer.add(shape);
const tr = new Konva.Transformer({
nodes: [shape]
})
layer.add(tr);
tr.findOne('.rotater').on('mouseenter', () => {
// "content" property is not documented and private
// but for now you can use it
// it is element where transformer is applying its styles
stage.content.style.cursor = 'move';
});
<script src="https://unpkg.com/konva#^8/konva.min.js"></script>
<div id="container"></div>

React Konva - undo free draw lines

I was following this tutorial on how to build a whiteboard with react and konva and it provides an undo function for shapes but does not work for lines because lines are not added to the layer in the same way. How can I implement undo for free draw line?
EDIT:
To expand on my question, here is the relevant code:
I have a public repo that you can check out (and make a PR if that's easier).
https://github.com/ChristopherHButler/Sandbox-react-whiteboard
I have also have a demo you can try out here:
https://whiteboard-rho.now.sh/
Here is the relevant code
line component:
import Konva from "konva";
export const addLine = (stage, layer, mode = "brush") => {
let isPaint = false;
let lastLine;
stage.on("mousedown touchstart", function(e) {
isPaint = true;
let pos = stage.getPointerPosition();
lastLine = new Konva.Line({
stroke: mode == "brush" ? "red" : "white",
strokeWidth: mode == "brush" ? 5 : 20,
globalCompositeOperation:
mode === "brush" ? "source-over" : "destination-out",
points: [pos.x, pos.y],
draggable: mode == "brush",
});
layer.add(lastLine);
});
stage.on("mouseup touchend", function() {
isPaint = false;
});
stage.on("mousemove touchmove", function() {
if (!isPaint) {
return;
}
const pos = stage.getPointerPosition();
let newPoints = lastLine.points().concat([pos.x, pos.y]);
lastLine.points(newPoints);
layer.batchDraw();
});
};
HomePage component:
import React, { useState, createRef } from "react";
import { v1 as uuidv1 } from 'uuid';
import ButtonGroup from "react-bootstrap/ButtonGroup";
import Button from "react-bootstrap/Button";
import { Stage, Layer } from "react-konva";
import Rectangle from "../Shapes/Rectangle";
import Circle from "../Shapes/Circle";
import { addLine } from "../Shapes/Line";
import { addTextNode } from "../Shapes/Text";
import Image from "../Shapes/Image";
const HomePage = () => {
const [rectangles, setRectangles] = useState([]);
const [circles, setCircles] = useState([]);
const [images, setImages] = useState([]);
const [selectedId, selectShape] = useState(null);
const [shapes, setShapes] = useState([]);
const [, updateState] = useState();
const stageEl = createRef();
const layerEl = createRef();
const fileUploadEl = createRef();
const getRandomInt = max => {
return Math.floor(Math.random() * Math.floor(max));
};
const addRectangle = () => {
const rect = {
x: getRandomInt(100),
y: getRandomInt(100),
width: 100,
height: 100,
fill: "red",
id: `rect${rectangles.length + 1}`,
};
const rects = rectangles.concat([rect]);
setRectangles(rects);
const shs = shapes.concat([`rect${rectangles.length + 1}`]);
setShapes(shs);
};
const addCircle = () => {
const circ = {
x: getRandomInt(100),
y: getRandomInt(100),
width: 100,
height: 100,
fill: "red",
id: `circ${circles.length + 1}`,
};
const circs = circles.concat([circ]);
setCircles(circs);
const shs = shapes.concat([`circ${circles.length + 1}`]);
setShapes(shs);
};
const drawLine = () => {
addLine(stageEl.current.getStage(), layerEl.current);
};
const eraseLine = () => {
addLine(stageEl.current.getStage(), layerEl.current, "erase");
};
const drawText = () => {
const id = addTextNode(stageEl.current.getStage(), layerEl.current);
const shs = shapes.concat([id]);
setShapes(shs);
};
const drawImage = () => {
fileUploadEl.current.click();
};
const forceUpdate = React.useCallback(() => updateState({}), []);
const fileChange = ev => {
let file = ev.target.files[0];
let reader = new FileReader();
reader.addEventListener(
"load",
() => {
const id = uuidv1();
images.push({
content: reader.result,
id,
});
setImages(images);
fileUploadEl.current.value = null;
shapes.push(id);
setShapes(shapes);
forceUpdate();
},
false
);
if (file) {
reader.readAsDataURL(file);
}
};
const undo = () => {
const lastId = shapes[shapes.length - 1];
let index = circles.findIndex(c => c.id == lastId);
if (index != -1) {
circles.splice(index, 1);
setCircles(circles);
}
index = rectangles.findIndex(r => r.id == lastId);
if (index != -1) {
rectangles.splice(index, 1);
setRectangles(rectangles);
}
index = images.findIndex(r => r.id == lastId);
if (index != -1) {
images.splice(index, 1);
setImages(images);
}
shapes.pop();
setShapes(shapes);
forceUpdate();
};
document.addEventListener("keydown", ev => {
if (ev.code == "Delete") {
let index = circles.findIndex(c => c.id == selectedId);
if (index != -1) {
circles.splice(index, 1);
setCircles(circles);
}
index = rectangles.findIndex(r => r.id == selectedId);
if (index != -1) {
rectangles.splice(index, 1);
setRectangles(rectangles);
}
index = images.findIndex(r => r.id == selectedId);
if (index != -1) {
images.splice(index, 1);
setImages(images);
}
forceUpdate();
}
});
return (
<div className="home-page">
<ButtonGroup style={{ marginTop: '1em', marginLeft: '1em' }}>
<Button variant="secondary" onClick={addRectangle}>
Rectangle
</Button>
<Button variant="secondary" onClick={addCircle}>
Circle
</Button>
<Button variant="secondary" onClick={drawLine}>
Line
</Button>
<Button variant="secondary" onClick={eraseLine}>
Erase
</Button>
<Button variant="secondary" onClick={drawText}>
Text
</Button>
<Button variant="secondary" onClick={drawImage}>
Image
</Button>
<Button variant="secondary" onClick={undo}>
Undo
</Button>
</ButtonGroup>
<input
style={{ display: "none" }}
type="file"
ref={fileUploadEl}
onChange={fileChange}
/>
<Stage
style={{ margin: '1em', border: '2px solid grey' }}
width={window.innerWidth * 0.9}
height={window.innerHeight - 150}
ref={stageEl}
onMouseDown={e => {
// deselect when clicked on empty area
const clickedOnEmpty = e.target === e.target.getStage();
if (clickedOnEmpty) {
selectShape(null);
}
}}
>
<Layer ref={layerEl}>
{rectangles.map((rect, i) => {
return (
<Rectangle
key={i}
shapeProps={rect}
isSelected={rect.id === selectedId}
onSelect={() => {
selectShape(rect.id);
}}
onChange={newAttrs => {
const rects = rectangles.slice();
rects[i] = newAttrs;
setRectangles(rects);
}}
/>
);
})}
{circles.map((circle, i) => {
return (
<Circle
key={i}
shapeProps={circle}
isSelected={circle.id === selectedId}
onSelect={() => {
selectShape(circle.id);
}}
onChange={newAttrs => {
const circs = circles.slice();
circs[i] = newAttrs;
setCircles(circs);
}}
/>
);
})}
{images.map((image, i) => {
return (
<Image
key={i}
imageUrl={image.content}
isSelected={image.id === selectedId}
onSelect={() => {
selectShape(image.id);
}}
onChange={newAttrs => {
const imgs = images.slice();
imgs[i] = newAttrs;
}}
/>
);
})}
</Layer>
</Stage>
</div>
);
}
export default HomePage;
As a solution, you should just use the same react modal for lines. It is not recommended to create shape instances manually (like new Konva.Line) when you work with react-konva.
Just define your state and make a correct render() from it, as you do in HomePage component.
You may store all shapes in one array. Or use a separate for lines. So to draw lines in react-konva way you can do this:
const App = () => {
const [lines, setLines] = React.useState([]);
const isDrawing = React.useRef(false);
const handleMouseDown = (e) => {
isDrawing.current = true;
const pos = e.target.getStage().getPointerPosition();
setLines([...lines, [pos.x, pos.y]]);
};
const handleMouseMove = (e) => {
// no drawing - skipping
if (!isDrawing.current) {
return;
}
const stage = e.target.getStage();
const point = stage.getPointerPosition();
let lastLine = lines[lines.length - 1];
// add point
lastLine = lastLine.concat([point.x, point.y]);
// replace last
lines.splice(lines.length - 1, 1, lastLine);
setLines(lines.concat());
};
const handleMouseUp = () => {
isDrawing.current = false;
};
return (
<Stage
width={window.innerWidth}
height={window.innerHeight}
onMouseDown={handleMouseDown}
onMousemove={handleMouseMove}
onMouseup={handleMouseUp}
>
<Layer>
<Text text="Just start drawing" />
{lines.map((line, i) => (
<Line key={i} points={line} stroke="red" />
))}
</Layer>
</Stage>
);
};
Demo: https://codesandbox.io/s/hungry-architecture-v380jlvwrl?file=/index.js
Then the next step is how to implement undo/redo. You just need to keep a history of state changes. Take a look here for demo: https://konvajs.org/docs/react/Undo-Redo.html
If I understand this right you saying that for shapes which are added individually there is an easy 'undo' process, but for lines which use an array of points for their segments, there is no simple undo - and no code in the tutorial you are following?
I can't give you a react code sample but I can explain some of the concepts you need to code up.
The 'freehand line' in your whiteboard is created as a sequence of points. You mousedown and the first point is noted, then you move the mouse and on each movemove event that fires the current mouse position is added to the end of the array. By the time you complete the line and mouseup fires, you have thrown multiple points into the line array.
In the Konvajs line tutorial it states:
To define the path of the line you should use points property. If you
have three points with x and y coordinates you should define points
property as: [x1, y1, x2, y2, x3, y3].
[Because...] Flat array of numbers should work faster and use less memory than
array of objects.
So - your line points are added as separate values into the line.points array.
Now lets think about undo - you are probably there already but I'll write it out anyway - to undo a single segment of the line you need to erase the last 2 entries in the array. To erase the entire line - well you can use the standard shape.remove() or shape.destroy() methods.
In the following snippet the two buttons link to code to 'undo' lines. The 'Undo by segment' button shows how to pop the last two entries in the line.points array to remove a segment of the line, and the 'Undo by line' button removes entire lines. This is not a react example specifically, but you will in the end create something very close to this in your react case.
// Code to erase line one segment at a time.
$('#undosegment').on('click', function(){
// get the last line we added to the canvas - tracked via lines array in this demo
if (lines.length === 0){
return;
}
lastLine = lines[lines.length - 1];
let pointsArray = lastLine.points(); // get current points in line
if (pointsArray.length === 0){ // no more points so destroy this line object.
lastLine.destroy();
layer.batchDraw();
lines.pop(); // remove from our lines-tracking array.
return;
}
// remove last x & y entrie, pop appears to be fastest way to achieve AND adjust array length
pointsArray.pop(); // remove the last Y pos
pointsArray.pop(); // remove the last X pos
lastLine.points(pointsArray); // give the points back into the line
layer.batchDraw();
})
// Code to erase entire lines.
$('#undoline').on('click', function(){
// get the last line we added to the canvas - tracked via lines array in this demo
if (lines.length === 0){
return;
}
lastLine = lines[lines.length - 1];
lastLine.destroy(); // remove from our lines-tracking array.
lines.pop();
layer.batchDraw();
})
// code from here on is all about drawing the lines.
let
stage = new Konva.Stage({
container: 'container',
width: $('#container').width(),
height: $('#container').height()
}),
// add a layer to draw on
layer = new Konva.Layer();
stage.add(layer);
stage.draw();
let isPaint = false;
let lastLine;
let lines = [];
stage.on('mousedown', function(){
isPaint = true;
let pos = stage.getPointerPosition();
lastLine = new Konva.Line({ stroke: 'magenta', strokeWidth: 4, points: [pos.x, pos.y]});
layer.add(lastLine);
lines.push(lastLine);
})
stage.on("mouseup touchend", function() {
isPaint = false;
});
stage.on("mousemove touchmove", function() {
if (!isPaint) {
return;
}
const pos = stage.getPointerPosition();
let newPoints = lastLine.points().concat([pos.x, pos.y]);
lastLine.points(newPoints);
layer.batchDraw();
});
body {
margin: 10;
padding: 10;
overflow: hidden;
background-color: #f0f0f0;
}
#container {
border: 1px solid silver;
width: 500px;
height: 300px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/konva#^3/konva.min.js"></script>
<p>Click and drag to draw a line </p>
<p>
<button id='undosegment'>Undo by segment</button> <button id='undoline'>Undo by line</button>
</p>
<div id="container"></div>

Resizing Annotation shapes

I am trying to create annotations in high charts and resizing the shapes on clicking inside a shape. I have created a js fiddle.
Run the jsfiddle: http://jsfiddle.net/1e1jnv7w/
HTML:
<h3>Add annotation via simple form</h3>
<div style="width: 1054px; float: left;">
<div id="container" style="float: left; height: 342px; width: 800px">
</div>
JAVASCRIPT:
$(function() {
var options = {
chart: {
borderWidth: 5,
borderColor: '#e8eaeb',
borderRadius: 0,
renderTo: 'container',
backgroundColor: '#f7f7f7',
//zoomType: 'x',
events: {
load: chartLoad
}
},
title: {
style: {
'fontSize': '1em'
},
useHTML: true,
x: -27,
y: 8,
text: '<span class="chart-title"> Drag and drop on a chart to add annotation <span class="chart-href"> Black Label </span> <span class="chart-subtitle">plugin by </span></span>'
},
annotationsOptions: {
enabledButtons: false
},
annotations: [{
title: {
text: '<span style="">drag me anywhere <br> dblclick to remove</span>',
style: {
color: 'red'
}
},
anchorX: "left",
anchorY: "top",
allowDragX: true,
allowDragY: true,
x: 515,
y: 55
}, {
title: 'drag me <br> horizontaly',
anchorX: "left",
anchorY: "top",
allowDragY: false,
allowDragX: true,
xValue: 3,
yValue: 10,
shape: {
type: 'path',
params: {
d: ['M', 0, 0, 'L', 110, 0],
stroke: '#c55'
}
}
}, {
title: 'on point <br> drag&drop <br> disabled',
linkedTo: 'high',
anchorX: "middle",
anchorY: "middle",
allowDragY: false,
allowDragX: false,
shape: {
type: 'circle',
params: {
r: 40,
stroke: '#c55'
}
}
}, {
x: 100,
y: 200,
title: 'drag me <br> verticaly',
anchorX: "left",
anchorY: "top",
allowDragY: true,
allowDragX: false,
shape: {
type: 'rect',
params: {
x: 0,
y: 0,
width: 55,
height: 40
}
}
}],
series: [{
data: [13, 4, 5, {
y: 1,
id: 'high'
},
2, 14, 3, 2, 11, 6
]
}]
};
var chart = new Highcharts.Chart(options, function(chart) {
var container = chart.container,
offsetX = chart.plotLeft - container.offsetLeft,
offsetY = chart.plotTop - container.offsetTop;
Highcharts.addEvent(container, 'mousedown', function(e) {
var isInside = chart.isInsidePlot(e.clientX - offsetX, e.pageY - offsetY);
});
});
function chartLoad() {
var chart = this,
container = chart.container,
annotations = chart.annotations.allItems,
annotation,
clickX,
clickY;
function getParams(e) {
function getRadius(e) {
var x = e.pageX - container.offsetLeft,
y = e.pageY - container.offsetTop,
dx = Math.abs(x - clickX),
dy = Math.abs(y - clickY);
return parseInt(Math.sqrt(dx * dx + dy * dy), 10);
}
function getPath(e) {
var x = e.pageX - container.offsetLeft,
y = e.pageY - container.offsetTop,
dx = x - clickX,
dy = y - clickY;
return ["M", 0, 0, 'L', parseInt(dx, 10), parseInt(dy, 10)];
}
function getWidth(e) {
var x = e.clientX - container.offsetLeft,
dx = Math.abs(x - clickX);
return parseInt(dx, 10) + 1;
}
function getHeight(e) {
var y = e.pageY - container.offsetTop,
dy = Math.abs(y - clickY);
return parseInt(dy, 10) + 1;
}
if (!annotation.options.shape) return;
var shape = annotation.options.shape.params;
var newShape = {};
if (shape.r) {
newShape.r = getRadius(e);
}
if (shape.d) {
newShape.d = getPath(e);
}
if (shape.width) {
newShape.width = getWidth(e);
}
if (shape.height) {
newShape.height = getHeight(e);
}
return newShape;
}
function drag(e) {
// alert("Hii");
var shape = $("input[type='radio']:checked").val(),
stroke = $("#stroke").val(),
strokeWidth = $("#strokeWidth").val(),
title = $("#title").val(),
fill = $("#fill").val(),
shapeOpt = null,
x = null,
y = null,
width = null,
height = null,
radius = 20;
clickX = e.pageX - container.offsetLeft;
clickY = e.pageY - container.offsetTop;
if (!chart.isInsidePlot(clickX - chart.plotLeft, clickY - chart.plotTop)) {
return;
}
if (shape == 'rect') {
x = 0;
y = 0;
width = 1;
height = 1;
radius = 1;
}
if (shape !== 'text') {
shapeOpt = {
type: shape,
params: {
r: shape == 'circle' ? 1 : 0,
d: shape == 'path' ? ['M', 0, 0, 'L', 1, 1] : null,
x: x,
y: y,
width: width,
height: height
}
};
title = null;
Highcharts.addEvent(document, 'mousemove', step);
}
chart.addAnnotation({
x: clickX,
y: clickY,
allowDragX: true,
allowDragY: true,
anchorX: 'left',
anchorY: 'top',
title: title,
shape: shapeOpt
});
annotation = annotations[annotations.length - 1];
}
function step(e) {
// use renderer api for better performance
annotation.shape.attr(getParams(e));
}
function drop(e) {
Highcharts.removeEvent(document, 'mousemove', step);
// store annotation details
if (annotation) {
annotation.update({
shape: {
params: getParams(e)
}
});
}
annotation = null;
}
function sal(e)
{
// Highcharts.removeEvent(container, 'dblclick', step);
var each = Highcharts.each;
each(chart.annotations.allItems, function (item, i) {
if (item.selectionMarker) {
shape = item.shape.element.localName;
}
});
// var shape = $("input[type='radio']:checked").val(),
var shapeOpt = null,
x = null,
y = null,
width = null,
height = null,
radius = null;
clickX = e.pageX - container.offsetLeft;
clickY = e.pageY - container.offsetTop;
if (!chart.isInsidePlot(clickX - chart.plotLeft, clickY - chart.plotTop)) {
return;
}
if (shape == 'rect') {
x = clickX;
y = clickY;
width = 1;
height = 1;
radius = 1;
}
if (shape !== 'text') {
shapeOpt = {
type: shape,
params: {
r: shape == 'circle' ? 1 : 0,
d: shape == 'path' ? ['M', 0, 0, 'L', 1, 1] : null,
x: x,
y: y,
width: width,
height: height
}
};
title = null;
Highcharts.addEvent(document, 'mousemove', step);
}
annotation = annotations[annotations.length - 1];
}
// Highcharts.addEvent(container, 'mousedown', drag);
Highcharts.addEvent(document, 'mouseup', drop);
Highcharts.addEvent(container, 'dblclick', sal);
$('#ann1size').click(function() {
var ann = annotations[annotations.length - 1];
ann.update({
shape: {
params: {
r: 200
}
}
})
});
}
});
double click on square shape, you can resize it now.
double click on circle shape, its still resizing square shape.
Can you please let me know how to fix this issue that no matter which shape is double clicked, square is getting resized.
I think that right now you have problem with getting right annotation in your chart. You are getting always the same rect shape because you are using fixed:
annotation = annotations[annotations.length - 1];
Instead this line, you can set your annotation object inside Highcharts.each:
var each = Highcharts.each;
each(chart.annotations.allItems, function(item, i) {
if (item.selectionMarker) {
annotation = item;
shape = item.shape.element.localName;
}
});
Here you can see an example how it can work:
http://jsfiddle.net/1e1jnv7w/3/
Best regards.

React Native multiple panresponders

With this code how would I add a second or multiple panresponders that can be moved independently of each other? If I use the same panresponder instance and code they move together as one. I want to know how to have several independently draggable panresponders.
'use strict';
var React = require('react-native');
var {
PanResponder,
StyleSheet,
View,
processColor,
} = React;
var CIRCLE_SIZE = 80;
var CIRCLE_COLOR = 'blue';
var CIRCLE_HIGHLIGHT_COLOR = 'green';
var PanResponderExample = React.createClass({
statics: {
title: 'PanResponder Sample',
description: 'Shows the use of PanResponder to provide basic gesture handling.',
},
_panResponder: {},
_previousLeft: 0,
_previousTop: 0,
_circleStyles: {},
circle: (null : ?{ setNativeProps(props: Object): void }),
componentWillMount: function() {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder,
onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder,
onPanResponderGrant: this._handlePanResponderGrant,
onPanResponderMove: this._handlePanResponderMove,
onPanResponderRelease: this._handlePanResponderEnd,
onPanResponderTerminate: this._handlePanResponderEnd,
});
this._previousLeft = 20;
this._previousTop = 84;
this._circleStyles = {
style: {
left: this._previousLeft,
top: this._previousTop
}
};
},
componentDidMount: function() {
this._updatePosition();
},
render: function() {
return (
<View
style={styles.container}>
<View
ref={(circle) => {
this.circle = circle;
}}
style={styles.circle}
{...this._panResponder.panHandlers}
/>
</View>
);
},
_highlight: function() {
const circle = this.circle;
circle && circle.setNativeProps({
style: {
backgroundColor: processColor(CIRCLE_HIGHLIGHT_COLOR)
}
});
},
_unHighlight: function() {
const circle = this.circle;
circle && circle.setNativeProps({
style: {
backgroundColor: processColor(CIRCLE_COLOR)
}
});
},
_updatePosition: function() {
this.circle && this.circle.setNativeProps(this._circleStyles);
},
_handleStartShouldSetPanResponder: function(e: Object, gestureState: Object): boolean {
// Should we become active when the user presses down on the circle?
return true;
},
_handleMoveShouldSetPanResponder: function(e: Object, gestureState: Object): boolean {
// Should we become active when the user moves a touch over the circle?
return true;
},
_handlePanResponderGrant: function(e: Object, gestureState: Object) {
this._highlight();
},
_handlePanResponderMove: function(e: Object, gestureState: Object) {
this._circleStyles.style.left = this._previousLeft + gestureState.dx;
this._circleStyles.style.top = this._previousTop + gestureState.dy;
this._updatePosition();
},
_handlePanResponderEnd: function(e: Object, gestureState: Object) {
this._unHighlight();
this._previousLeft += gestureState.dx;
this._previousTop += gestureState.dy;
},
});
var styles = StyleSheet.create({
circle: {
width: CIRCLE_SIZE,
height: CIRCLE_SIZE,
borderRadius: CIRCLE_SIZE / 2,
backgroundColor: CIRCLE_COLOR,
position: 'absolute',
left: 0,
top: 0,
},
container: {
flex: 1,
paddingTop: 64,
},
});
module.exports = PanResponderExample;
You can use an array of PanResponders, created like so:
this._panResponders = yourObjectsArray.map((_, index) => (
PanResponder.create({
onMoveShouldSetPanResponder: () => true,
...
})
));
yourObjectsArray is an array that you use for creating as many panResponders as you want, I imagine each object in that array will correspond to a data instance of whatever data structure you use to create the moveable Views.
Then to actually use it in your View:
render: function() {
return yourObjectsArray.map((_, index) => (
<View
style={styles.container}>
<View
... some stuff here ...
{...this._panResponders[index].panHandlers}
/>
</View>
)
};

Resources