JointJS: make link label movable over the link itself - javascript

For the links - in a JointJS diagram - I tried to implement this tutorial (http://jointjs.com/tutorial/constraint-move-to-circle) to move the labels on the link, but I don't understand where to put the ConstraintElementView.
I would like to make the label of a link moveable over the link. So how can I define the link as the 'path' for the moveable label?
ConstraintElementView
var constraint = label; // ???
var ConstraintElementView = joint.dia.ElementView.extend({
pointerdown: function(evt, x, y) {
var position = this.model.get('position');
var size = this.model.get('size');
var center = g.rect(position.x, position.y, size.width, size.height).center();
var intersection = constraint.intersectionWithLineFromCenterToPoint(center);
joint.dia.ElementView.prototype.pointerdown.apply(this, [evt, intersection.x, intersection.y]);
},
pointermove: function(evt, x, y) {
var intersection = constraint.intersectionWithLineFromCenterToPoint(g.point(x, y));
joint.dia.ElementView.prototype.pointermove.apply(this, [evt, intersection.x, intersection.y]);
}
});
link label
paper.on({
/**
* Doubleclick on link: Add label for link
*/
'cell:pointerdblclick': function(cellView, event, x, y){
if (cellView.model.isLink()) {
cellView.model.label(0, {
position: .5,
attrs: {
rect: { fill: '#eeeeee' },
text: { text: 'text', 'font-size': 12, ref: 'rect' }
}
});
}
}
});
paper
var paper = new joint.dia.Paper({
el: $('#canvas'),
width: 801,
height: 496,
model: graph,
gridSize: 10,
elementView: ConstraintElementView,
defaultLink: new joint.dia.Link({
router: { name: 'manhattan' },
connector: { name: 'rounded' },
attrs: {
'.marker-target': { d: 'M 10 0 L 0 5 L 10 10 z', fill: '#6a6c8a', stroke: '#6a6c8a' },
'.connection': { stroke: '#6a6c8a', 'stroke-width': 2 }
}
})
});
As it is moveable over the link, it should be snap to the center of each segment of the manhattan-style link. But I don't see any chance to get the value of the center of each segment.

You do not need to create any path. Just change the position of label by calculating its relative value - of course can also use absolute values.
'cell:pointermove': function(event, x, y) {
if (event.model.isLink()) {
var clickPoint = { x: event._dx, y: event._dy },
lengthTotal = event.sourcePoint.manhattanDistance(event.targetPoint),
length = event.sourcePoint.manhattanDistance(clickPoint),
position = round(length / lengthTotal, 2);
event.model.label(0, { position: position });
}
}

Enabling labels to be movable along links can be done via the labelMove option of the interactive object on the paper:
var paper = new joint.dia.Paper({
// ...
interactive: { labelMove: true }
// ...
})
This flag defaults to false.

Related

Exclude edges from participating in the layout

Consider a graph like the one shown below:
I would like to be able to display/hide the red edges (forget that they are hand drawn) shown below when the user clicks a button or similar:
I don't want the red edges to participate in the layout but instead for them to be shown as a kind of overlay. It would be nice if the edges could try to avoid overlapping any nodes in their path, but its definitely not required.
I think if I could set a boolean flag on the edges telling the layout engine to either include or exclude them from the layout setup, it could work. There is a physics parameter on the edge that I can override, but it doesn't seem to help - the edge still participates in the layout.
I could probably also write some scripting which tracks the nodes and draw the red edges in another graph above, but that is specifically what I want to avoid.
When using a hierarchical layout in vis network (options.layout.hierarchical.enabled = true) there doesn't appear to be an option which achieves this. This could however be achieved with an overlay. The question mentions that this isn't desired, but adding it as an option. An example is incorporated into the post below and also at https://jsfiddle.net/7abovhtu/.
In summary the solution places an overlay canvas on top of the vis network canvas. Clicks on the overlay canvas are passed through to the vis network canvas due to the CSS pointer-events: none;. Extra edges are drawn onto the overlay canvas using the positioning of the nodes. Updates to the overlay canvas are triggered by the vis network event afterDrawing which triggers whenever the network changes (dragging, zooming, etc.).
This answer makes use of the closest point to an ellipse calculation provided in the answer https://stackoverflow.com/a/18363333/1620449 to end the lines at the edge of the nodes. This answer also makes use of the function in the answer https://stackoverflow.com/a/6333775/1620449 to draw an arrow on a canvas.
// create an array with nodes
var nodes = new vis.DataSet([
{ id: 1, label: "Node 1" },
{ id: 2, label: "Node 2" },
{ id: 3, label: "Node 3" },
{ id: 4, label: "Node 4" },
{ id: 5, label: "Node 5" },
{ id: 6, label: "Node 6" },
{ id: 7, label: "Node 7" },
]);
// create an array with edges
var edges = new vis.DataSet([
{ from: 1, to: 2 },
{ from: 2, to: 3 },
{ from: 3, to: 4 },
{ from: 3, to: 5 },
{ from: 3, to: 6 },
{ from: 6, to: 7 }
]);
// create an array with extra edges displayed on button press
var extraEdges = [
{ from: 7, to: 5 },
{ from: 6, to: 1 }
];
// create a network
var container = document.getElementById("network");
var data = {
nodes: nodes,
edges: edges,
};
var options = {
layout: {
hierarchical: {
enabled: true,
direction: 'LR',
sortMethod: 'directed',
shakeTowards: 'roots'
}
}
};
var network = new vis.Network(container, data, options);
// Create an overlay for displaying extra edges
var overlayCanvas = document.getElementById("overlay");
var overlayContext = overlayCanvas.getContext("2d");
// Function called to draw the extra edges, called on initial display and
// when the network completes each draw (due to drag, zoom etc.)
function drawExtraEdges(){
// Resize overlay canvas in case the continer has changed
overlayCanvas.height = container.clientHeight;
overlayCanvas.width = container.clientWidth;
// Begin drawing path on overlay canvas
overlayContext.beginPath();
// Clear any existing lines from overlay canvas
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
// Loop through extra edges to draw them
extraEdges.forEach(edge => {
// Gather the necessary coordinates for the start and end shapres
const startPos = network.canvasToDOM(network.getPosition(edge.from));
const endPos = network.canvasToDOM(network.getPosition(edge.to));
const endBox = network.getBoundingBox(edge.to);
// Determine the radius of the ellipse based on the scale of network
// Start and end ellipse are presumed to be the same size
const scale = network.getScale();
const radiusX = ((endBox.right * scale) - (endBox.left * scale)) / 2;
const radiusY = ((endBox.bottom * scale) - (endBox.top * scale)) / 2;
// Get the closest point on the end ellipse to the start point
const endClosest = getEllipsePt(endPos.x, endPos.y, radiusX, radiusY, startPos.x, startPos.y);
// Now we have an end point get the point on the ellipse for the start
const startClosest = getEllipsePt(startPos.x, startPos.y, radiusX, radiusY, endClosest.x, endClosest.y);
// Draw arrow on diagram
drawArrow(overlayContext, startClosest.x, startClosest.y, endClosest.x, endClosest.y);
});
// Apply red color to overlay canvas context
overlayContext.strokeStyle = '#ff0000';
// Make the line dashed
overlayContext.setLineDash([10, 3]);
// Apply lines to overlay canvas
overlayContext.stroke();
}
// Adjust the positioning of the lines each time the network is redrawn
network.on("afterDrawing", function (event) {
// Only draw the lines if they have been toggled on with the button
if(extraEdgesShown){
drawExtraEdges();
}
});
// Add button event to show / hide extra edges
var extraEdgesShown = false;
document.getElementById('extraEdges').onclick = function() {
if(!extraEdgesShown){
if(extraEdges.length > 0){
// Call function to draw extra lines
drawExtraEdges();
extraEdgesShown = true;
}
} else {
// Remove extra edges
// Clear the overlay canvas
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
extraEdgesShown = false;
}
}
//////////////////////////////////////////////////////////////////////
// Elllipse closest point calculation
// https://stackoverflow.com/a/18363333/1620449
//////////////////////////////////////////////////////////////////////
var halfPI = Math.PI / 2;
var steps = 8; // larger == greater accuracy
// calc a point on the ellipse that is "near-ish" the target point
// uses "brute force"
function getEllipsePt(cx, cy, radiusX, radiusY, targetPtX, targetPtY) {
// calculate which ellipse quadrant the targetPt is in
var q;
if (targetPtX > cx) {
q = (targetPtY > cy) ? 0 : 3;
} else {
q = (targetPtY > cy) ? 1 : 2;
}
// calc beginning and ending radian angles to check
var r1 = q * halfPI;
var r2 = (q + 1) * halfPI;
var dr = halfPI / steps;
var minLengthSquared = 200000000;
var minX, minY;
// walk the ellipse quadrant and find a near-point
for (var r = r1; r < r2; r += dr) {
// get a point on the ellipse at radian angle == r
var ellipseX = cx + radiusX * Math.cos(r);
var ellipseY = cy + radiusY * Math.sin(r);
// calc distance from ellipsePt to targetPt
var dx = targetPtX - ellipseX;
var dy = targetPtY - ellipseY;
var lengthSquared = dx * dx + dy * dy;
// if new length is shortest, save this ellipse point
if (lengthSquared < minLengthSquared) {
minX = ellipseX;
minY = ellipseY;
minLengthSquared = lengthSquared;
}
}
return ({
x: minX,
y: minY
});
}
//////////////////////////////////////////////////////////////////////
// Draw Arrow on Canvas Function
// https://stackoverflow.com/a/6333775/1620449
//////////////////////////////////////////////////////////////////////
function drawArrow(ctx, fromX, fromY, toX, toY) {
var headLength = 10; // length of head in pixels
var dX = toX - fromX;
var dY = toY - fromY;
var angle = Math.atan2(dY, dX);
ctx.fillStyle = "red";
ctx.moveTo(fromX, fromY);
ctx.lineTo(toX, toY);
ctx.lineTo(toX - headLength * Math.cos(angle - Math.PI / 6), toY - headLength * Math.sin(angle - Math.PI / 6));
ctx.moveTo(toX, toY);
ctx.lineTo(toX - headLength * Math.cos(angle + Math.PI / 6), toY - headLength * Math.sin(angle + Math.PI / 6));
}
#container {
width: 100%;
height: 80vh;
border: 1px solid lightgray;
position: relative;
}
#network, #overlay {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
#overlay {
z-index: 100;
pointer-events: none;
}
<script src="https://visjs.github.io/vis-network/standalone/umd/vis-network.min.js"></script>
<button id="extraEdges">Toggle Extra Edges</button>
<div id="container">
<div id="network"></div>
<canvas width="600" height="400" id="overlay"></canvas>
</div>
This can be achieved using either the physics or hidden options on the extra edges (those in red). For reference, these options are described in more detail at https://visjs.github.io/vis-network/docs/network/edges.html.
Please note the below options do not work when hierarchical layout is used as set in the Vis Network options options.layout.hierarchical.enabled = true.
Physics - An example of using the physics option is https://jsfiddle.net/6oac73p0. However as you mentioned this may cause overlaps with nodes which have physics enabled. The extra edges are set to dashed in this example to ensure everything is still visible.
Hidden - An example of using the hidden option is https://jsfiddle.net/xfcuvtgk/ and also incorporated into this post below. Edges set to hidden are still part of the physics calculation when the layout is generated, which you mentioned wasn't desired, however this does mean they fit nicely when later displayed.
// create an array with nodes
var nodes = new vis.DataSet([
{ id: 1, label: "Node 1" },
{ id: 2, label: "Node 2" },
{ id: 3, label: "Node 3" },
{ id: 4, label: "Node 4" },
{ id: 5, label: "Node 5" },
]);
// create an array with edges
var edges = new vis.DataSet([
{ from: 1, to: 3 },
{ from: 1, to: 2 },
{ from: 2, to: 4 },
{ from: 2, to: 5 },
{ from: 3, to: 3 },
{ from: 4, to: 5, color: 'red', hidden: true, arrows: 'to', extra: true },
{ from: 3, to: 5, color: 'red', hidden: true, arrows: 'to', extra: true },
{ from: 1, to: 5, color: 'red', hidden: true, arrows: 'to', extra: true }
]);
// create a network
var container = document.getElementById("mynetwork");
var data = {
nodes: nodes,
edges: edges,
};
var options = {};
var network = new vis.Network(container, data, options);
document.getElementById('extraEdges').onclick = function() {
// Extract the list of extra edges
edges.forEach(function(edge){
if(edge.extra){
// Toggle the hidden value
edge.hidden = !edge.hidden;
// Update edge back onto data set
edges.update(edge);
}
});
}
#mynetwork {
width: 600px;
/* Height adjusted for Stack Overflow inline demo */
height: 160px;
border: 1px solid lightgray;
}
<script src="https://visjs.github.io/vis-network/standalone/umd/vis-network.min.js"></script>
<button id="extraEdges">Show/Hide Extra Edges</button>
<div id="mynetwork"></div>

Highchart on mobile (Touch to zoom out and zoom in) - LineChart

I'm trying to apply ZoomIn and ZoomOut in a line chart on a mobile device. The goal is to click on a zone of the chart and ZoomIn in the first click and ZoomOut on the second. The sequence will always be this one.
I already live to see the documentation / examples and I can not find anything to solve this situation.
I have already tried using this properties in the chart: property
pinchType : 'y',
zoomType: 'none'
I tried the zoomtype but the behavior is not what I expect. I want to have a click to zoom this specific area of ​​the chart. I do not want to zoom with two fingers.
{
chart: {
pinchType : 'x'
},
legend: {
itemStyle: {
color: '#fff'
}
},
plotOptions: {
series: {
animation: {
duration: 2000
}
}
},
xAxis: {
tickInterval: 1
},
series: [
{
type: 'spline',
color : '#fff'
},
{
dashStyle: 'longdash',
color: '#b3be77'
}
],
}
As simple as clicking to get zoomin and zoomout
Yes, the second challenge can be easily achieved by adding this logic to plotOptions.series.events.click callback function:
chart: {
events: {
load: function() {
this.clickedOnce = false;
},
click: function() {
const chart = this;
if (chart.clickedOnce) {
chart.zoomOut();
chart.clickedOnce = false;
}
}
}
},
plotOptions: {
series: {
events: {
click: function(e) {
const chart = this.chart,
yAxis = chart.yAxis[0],
xAxis = chart.xAxis[0];
let x,
y,
rangeX,
rangeY;
if (!chart.clickedOnce) {
x = xAxis.toValue(e.chartX);
y = yAxis.toValue(e.chartY);
rangeX = xAxis.max - xAxis.min;
rangeY = yAxis.max - yAxis.min;
xAxis.setExtremes(x - rangeX / 10, x + rangeX / 10, false);
yAxis.setExtremes(y - rangeY / 10, y + rangeY / 10, false);
chart.redraw();
chart.clickedOnce = true;
} else {
chart.zoomOut();
chart.clickedOnce = false;
}
}
}
}
}
Demos:
https://jsfiddle.net/BlackLabel/kotgea5n/
https://jsfiddle.net/BlackLabel/s8w2xg3e/1/
This functionality is not implemented in Highcharts by default, but you can easily achieve it by adding your custom logic when the chart area is clicked.
When area is clicked the first time use axis.setExtremes() method to zoom in. On the second click use chart.zoomOut() to zoom out the chart. Check demo and code posted below.
Code:
chart: {
events: {
load: function() {
this.clickedOnce = false;
},
click: function(e) {
const chart = this,
yAxis = chart.yAxis[0],
xAxis = chart.xAxis[0];
let x,
y,
rangeX,
rangeY;
if (!chart.clickedOnce) {
x = xAxis.toValue(e.chartX);
y = yAxis.toValue(e.chartY);
rangeX = xAxis.max - xAxis.min;
rangeY = yAxis.max - yAxis.min;
xAxis.setExtremes(x - rangeX / 10, x + rangeX / 10, false);
yAxis.setExtremes(y - rangeY / 10, y + rangeY / 10, false);
chart.redraw();
chart.clickedOnce = true;
} else {
chart.zoomOut();
chart.clickedOnce = false;
}
}
}
}
Demo:
https://jsfiddle.net/BlackLabel/fxm812k4/
API reference:
https://api.highcharts.com/class-reference/Highcharts.Axis#setExtremes
https://api.highcharts.com/class-reference/Highcharts.Chart#zoomOut
https://api.highcharts.com/highcharts/chart.events.click
Using a customEvents plugin (see: https://github.com/blacklabel/custom_events) and adding plotBand on the whole chart area you can register a callback on click and double click events. Using this approach you can make a zoom in on click event and zoom out on double click (not working on mobile devices).
Demo:
https://jsfiddle.net/BlackLabel/6tpb5q2z/

Highcharts Polar charts display labels on specific tick quarters

Considering that my data array is always composed of 4 elements as :
var data = [
{"type":"column","name":"My Label 1","y":38.9500000000003,"color":"#7cb342"},
{"type":"column","name":"My Label 2","y":30,"color":"#7cb342"}, {"type":"column","name":"My Label 3","y":51.85,"color":"#fbc02d"}, {"type":"column","name":"My Label 4","y":55.2999999999997,"color":"#fbc02d"}];
I want to know how to set my data names (data.name) each 45 degrees tick interval to keep them well positioned ?
Here is the example:
http://jsfiddle.net/eento/7rupgxde/4/
It's important for me to display only those labels & keep them inside the global highchart container.
Like this?
dataLabels: {
enabled: true,
formatter: function() {
return this.point.name;
}
}
Example:
http://jsfiddle.net/jlbriggs/7rupgxde/5/
You can use renderer to render those labels, for example, create two methods (one to add labels and one to position them):
function renderLabels(chart) {
var alignments = ['right', 'right', 'left', 'left'];
$.each(chart.series[0].points, function(i, point) {
point.myName = chart.renderer.text(point.name, -9999, -9999).attr({
align: alignments[i],
color: 'black'
}).add();
});
}
function positionLabels(chart, anim) {
var positions = [
// top right label
[chart.plotLeft + chart.plotWidth, chart.plotTop],
// bottom right label
[chart.plotLeft + chart.plotWidth, chart.plotTop + chart.plotHeight],
// bottom left label
[chart.plotLeft, chart.plotTop + chart.plotHeight],
// top left label
[chart.plotLeft, chart.plotTop],
]
$.each(chart.series[0].points, function(i, point) {
if (point.myName) {
point.myName[(anim ? 'animate' : 'attr')]({
x: positions[i][0],
y: positions[i][1],
})
}
});
}
Then use those methods in chart.events:
chart: {
polar: true,
renderTo: 'container',
backgroundColor: null,
events: {
load: function() {
renderLabels(this);
positionLabels(this, false);
},
redraw: function() {
positionLabels(this, true);
}
}
},
And working demo: http://jsfiddle.net/7rupgxde/7/

Javascript - Highchart - tooltip position of data points and flags

Using Highchart, tooltip position of each data point
tooltip: {
positioner: function () {
return { x: 80, y: 50 };
}
},
The problem is that the above option also changes the tooltip position of the flags.
How do I change the options so that the tooltip of the data point is fixed as the above option, but the tooltip position of flags remain as before?
Solution #1:
Example: http://jsfiddle.net/QcR4k/
And code:
positioner: function (w,h,p) {
var points = this.chart.hoverPoints;
//hover points contains only standard points, not flags
if(points) {
return { x: 320, y: 50 };
} else {
return { x: 80, y: 50 };
}
}
As you can see, flags are not part of hoverPoints, so you can that way determine where to display tooltip.
Solution #2:
Example: http://jsfiddle.net/QcR4k/1/
Code:
formatter: function() {
var series = this.series || this.points[0].series; //shared or non-shared tooltip
if(series.userOptions.isFlag){
series.chart.tooltip.isFlag = true;
return 'flag';
} else {
series.chart.tooltip.isFlag = false;
return this.y + '<br> non-flag';
}
},
positioner: function (w,h,p) {
if(!this.chart.tooltip.isFlag) {
return { x: 320, y: 50 };
} else {
return { x: p.plotX, y: p.plotY };
}
}
That solution uses tooltip.formatter to set flag, where point should be displayed - first formatter is returned, then we can manage position accordingly.

Dynamically draw marker on last point in highcharts

I want to draw a marker on the last point. Data source is dynamic.
Have a look at following code
$(function() {
$("#btn").click(function() {
var l = chart.series[0].points.length;
var p = chart.series[0].points[l - 1];
p.marker = {
symbol: 'square',
fillColor: "#A0F",
lineColor: "A0F0",
radius: 5
};
a = 1;
chart.series[0].points[l - 1] = p;
chart.redraw(false);
});
var ix = 13;
var a = 0;
var chart = new Highcharts.Chart({
chart: {
renderTo: 'container',
events: {
load: function() {
var series = this.series[0];
setInterval(function() {
ix++;
var vv = 500 + Math.round(Math.random() * 40);
chart.series[0].data[0].remove();
var v;
if (a == 1) v = {
y: vv,
x: ix,
marker: {
symbol: 'square',
fillColor: "#A0F",
lineColor: "A0F0",
radius: 5
}
}
else v = {
y: vv,
x: ix
}
a = 0;
series.addPoint(v);
}, 1500);
}
}
},
plotOptions: {
series: {}
},
series: [{
data: [500, 510, 540, 537, 510, 540, 537, 500, 510, 540, 537, 510, 540, 537]}]
});
});
http://jsfiddle.net/9zNUP/
On button click event I am trying to draw marker on last point which is already added to chart.
Is there a way to do that??
$("#btn").click(function() {
var l = chart.series[0].points.length;
var p = chart.series[0].points[l - 1];
p.update({
marker: {
symbol: 'square',
fillColor: "#A0F",
lineColor: "A0F0",
radius: 5
}
});
a = 1;
});
solution # http://jsfiddle.net/jugal/zJZSx/
Also tidied up your code a little, removed the removal of point before adding one at the end, highcharts supports it inbuilt with the third param to addPoint as true, which denotes shift series, which removes first point and then adds the given point.
I didn't really understand what the a vv etc were, but well i didn't bother much either. I think this is enough based on what you asked for.

Categories