ChartJS - Display a single line data in tooltip - javascript

I'm working with the chartJS library and trying to figure out what I need to do to get a single lines data to display in the tooltip.
For example,
I am hovering over the blue line here and see every data point at that mark. What I would like to do is see all three data points for the blue line only.
I've made some progress from chart js tooltip how to control the data that show
getPointsAtEvent: function(e) {
var pointsArray = [], eventPosition = helpers.getRelativePosition(e);
var breakLoop = 0;
helpers.each(this.datasets, function(dataset) {
helpers.each(dataset.points, function(point) {
if (point.inRange(eventPosition.x, eventPosition.y) && point.showTooltip && !point.ignore) {
if(eventPosition.y + 2 >= point.y && eventPosition.y - 2 <= point.y) {
pointsArray.push(point);
breakLoop = 1;
return false;
}
}
});
if(breakLoop) {
return false;
}
}, this);
//console.log(pointsArray);
return pointsArray;
},
Is my chart modification that will return 1 data point on the graph. I'm assuming the next step is to overwrite the showToolTip method.

If this is the only chart you have (i.e. because the following code changes some of the global chart.js elements), you can use the following bit of code
var originalMultiTooltip = Chart.MultiTooltip;
Chart.MultiTooltip = function () {
var argument = arguments[0];
// locate the series using the active point
var activeDatasetLabel = myChart.activeElements[0].datasetLabel;
myChart.datasets.forEach(function (dataset) {
if (dataset.label === activeDatasetLabel) {
// swap out the labels and colors in arguments
argument.labels = dataset.points.map(function (point) { return point.value; });
argument.legendColors = dataset.points.map(function (point) {
return {
fill: point._saved.fillColor || point.fillColor,
stroke: point._saved.strokeColor || point.strokeColor
};
});
argument.title = activeDatasetLabel;
// position it near the active point
argument.y = myChart.activeElements[0].y;
}
})
return new originalMultiTooltip(arguments[0]);
}
// this distance function returns the square of the distance if within detection range, otherwise it returns Infinity
var distance = function (chartX, chartY) {
var hitDetectionRange = this.hitDetectionRadius + this.radius;
var distance = Math.pow(chartX - this.x, 2) + Math.pow(chartY - this.y, 2);
return (distance < Math.pow(hitDetectionRange, 2)) ? distance : Infinity;
}
myChart.getPointsAtEvent = function (e) {
var pointsArray = [],
eventPosition = Chart.helpers.getRelativePosition(e);
var leastDistance = Infinity;
Chart.helpers.each(myChart.datasets, function (dataset) {
Chart.helpers.each(dataset.points, function (point) {
// our active point is the one closest to the hover event
var pointDistance = distance.call(point, eventPosition.x, eventPosition.y)
if (isFinite(pointDistance) && pointDistance < leastDistance) {
leastDistance = pointDistance;
pointsArray = [ point ];
}
});
}, myChart);
return pointsArray;
}
It does 2 things
Replaces the getPointsAtEvent to just pick one point
Wraps the MultiTooltip constructor to swap out the list of values passed with all the values from the active point's series.
Fiddle - http://jsfiddle.net/h93pyavk/

If you extend the line chart, use the code I have above, and the code I pasted below you can get the desired effect to some degree.
showTooltip: function(ChartElements, forceRedraw) { //custom edit
//we will get value from ChartElements (which should be only 1 element long in this case) and use it to match the line row we want to see.
try {
var numMatch = ChartElements[0].value;
}
catch(err) {
var isChanged = (function(Elements) {
var changed = true;
return changed;
}).call(this, ChartElements);
}
// Only redraw the chart if we've actually changed what we're hovering on.
if (typeof this.activeElements === 'undefined') this.activeElements = [];
var isChanged = (function(Elements) {
var changed = false;
if (Elements.length !== this.activeElements.length) {
changed = true;
return changed;
}
helpers.each(Elements, function(element, index) {
if (element !== this.activeElements[index]) {
changed = true;
}
}, this);
return changed;
}).call(this, ChartElements);
if (!isChanged && !forceRedraw) {
return;
} else {
this.activeElements = ChartElements;
}
this.draw();
if (this.options.customTooltips) {
this.options.customTooltips(false);
}
if (ChartElements.length > 0) {
// If we have multiple datasets, show a MultiTooltip for all of the data points at that index
if (this.datasets && this.datasets.length > 1) {
var dataArray,
dataIndex;
for (var i = this.datasets.length - 1; i >= 0; i--) {
dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments;
dataIndex = helpers.indexOf(dataArray, ChartElements[0]);
if (dataIndex !== -1) {
break;
}
}
var eleLast = "";
var eleFirst = "";
var tooltipLabels = [],
tooltipColors = [],
medianPosition = (function(index) {
// Get all the points at that particular index
var Elements = [],
dataCollection,
xPositions = [],
yPositions = [],
xMax,
yMax,
xMin,
yMin;
helpers.each(this.datasets, function(dataset) {
dataCollection = dataset.points || dataset.bars || dataset.segments;
//console.log(dataset);
for(i = 0; i < dataset.points.length; i++) {
if(dataset.points[i].value === numMatch) {
for(var k = 0; k < dataset.points.length; k++) {
Elements.push(dataset.points[k]);
}
}
}
});
//save elements last label string
eleLast = Elements[Elements.length-1].label;
eleFirst = Elements[0].label;
//console.log(Elements);
helpers.each(Elements, function(element) {
if(element.value === numMatch) {
xPositions.push(element.x);
yPositions.push(element.y);
}
//Include any colour information about the element
tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element));
tooltipColors.push({
fill: element._saved.fillColor || element.fillColor,
stroke: element._saved.strokeColor || element.strokeColor
});
}, this);
yMin = helpers.min(yPositions);
yMax = helpers.max(yPositions);
xMin = helpers.min(xPositions);
xMax = helpers.max(xPositions);
return {
x: (xMin > this.chart.width / 2) ? xMin : xMax,
y: (yMin + yMax) / 2
};
}).call(this, dataIndex);
var newLabel = eleFirst + " to " + eleLast;
new Chart.MultiTooltip({
x: medianPosition.x,
y: medianPosition.y,
xPadding: this.options.tooltipXPadding,
yPadding: this.options.tooltipYPadding,
xOffset: this.options.tooltipXOffset,
fillColor: this.options.tooltipFillColor,
textColor: this.options.tooltipFontColor,
fontFamily: this.options.tooltipFontFamily,
fontStyle: this.options.tooltipFontStyle,
fontSize: this.options.tooltipFontSize,
titleTextColor: this.options.tooltipTitleFontColor,
titleFontFamily: this.options.tooltipTitleFontFamily,
titleFontStyle: this.options.tooltipTitleFontStyle,
titleFontSize: this.options.tooltipTitleFontSize,
cornerRadius: this.options.tooltipCornerRadius,
labels: tooltipLabels,
legendColors: tooltipColors,
legendColorBackground: this.options.multiTooltipKeyBackground,
title: newLabel,
chart: this.chart,
ctx: this.chart.ctx,
custom: this.options.customTooltips
}).draw();
} else {
helpers.each(ChartElements, function(Element) {
var tooltipPosition = Element.tooltipPosition();
new Chart.Tooltip({
x: Math.round(tooltipPosition.x),
y: Math.round(tooltipPosition.y),
xPadding: this.options.tooltipXPadding,
yPadding: this.options.tooltipYPadding,
fillColor: this.options.tooltipFillColor,
textColor: this.options.tooltipFontColor,
fontFamily: this.options.tooltipFontFamily,
fontStyle: this.options.tooltipFontStyle,
fontSize: this.options.tooltipFontSize,
caretHeight: this.options.tooltipCaretSize,
cornerRadius: this.options.tooltipCornerRadius,
text: helpers.template(this.options.tooltipTemplate, Element),
chart: this.chart,
custom: this.options.customTooltips
}).draw();
}, this);
}
}
return this;
},
Obviously this is just a quick and dirty fix, if I get more time to work on it I would like to have each data point show its corresponding value above it.

Related

How to make draggable div elements not stack upon each other

I am creating various draggable elements using let divEx = document.createElement('div'), then add it to the main div with main.appendChild(divEx) and finally
setting the attribute draggable to true using memDiv.setAttribute('draggable', 'true'). The whole functionality is in a function that is called in the main Js file when a button is pressed. Although, the drag and drop functionality works fine, the div elements are set upon each if the position style attribute is set to absolute or under each other if it's set to relative in the CSS. In the DOM it shows the different div elements, however the positions are the same for all of them. I hope the following screenshots explain the issue:
Absolute case:
There are three div elements (class=memDiv) on the screen two on top of each other and a newly created
Here is displayed after I move the third element, it is also stacked on top of the others
What is the best solution for this issue?
Answer: The key approach/solution is Collision Detection between div elements
It looks like you are trying to do a game or something similar, please save your self the pain/don't reinvent the wheel :) , just leverage a library and look for collision detection or set Z index to 1 and check of over lap, I prefer the sat.js lib.
note: Here is sat.js and look at this sample from here from a 3rd party/credit to OSU blake
console.clear();
var log = console.log.bind(console);
// Alias a few things in SAT.js to make the code shorter
var V = function (x, y) { return new SAT.Vector(x, y); };
var P = function (pos, points) { return new SAT.Polygon(pos, points); };
var C = function (pos, r) { return new SAT.Circle(pos, r); };
var B = function (pos, w, h) { return new SAT.Box(pos, w, h); };
// Converts a SAT.Polygon into a SVG path string.
function poly2path(polygon) {
var pos = polygon.pos;
var points = polygon.calcPoints;
var result = 'M' + pos.x + ' ' + pos.y;
result += 'M' + (pos.x + points[0].x) + ' ' + (pos.y + points[0].y);
for (var i = 1; i < points.length; i++) {
var point = points[i];
result += 'L' + (pos.x + point.x) + ' ' + (pos.y + point.y);
}
result += 'Z';
return result;
}
// Create a Raphael start drag handler for specified entity
function startDrag(entity) {
return function () {
this.ox = entity.data.pos.x;
this.oy = entity.data.pos.y;
};
}
// Create a Raphael move drag handler for specified entity
function moveDrag(entity, world) {
return function (dx, dy) {
// This position updating is fairly naive - it lets objects tunnel through each other, but it suffices for these examples.
entity.data.pos.x = this.ox + dx;
entity.data.pos.y = this.oy + dy;
world.simulate();
};
}
// Create a Raphael end drag handler for specified entity
function endDrag(entity) {
return function () {
entity.updateDisplay();
};
}
var idCounter = 0;
function noop() {}
function Entity(data, display, options) {
options = _.defaults(options || {}, {
solid: false, // Whether this object is "solid" and therefore should participate in responses.
heavy: false, // Whether this object is "heavy" and can't be moved by other objects.
displayAttrs: {}, // Raphael attrs to set on the display object
onCollide: noop, // Function to execute when this entity collides with another - arguments are (otherEntity, response)
onTick: noop // Function called at the start of every simulation tick - no arguments
});
this.id = idCounter++;
this.data = data;
this.display = display;
this.displayAttrs = _.extend({
fill: '#CCC',
stroke: '#000'
}, options.displayAttrs);
this.isSolid = options.solid;
this.isHeavy = options.heavy;
this.onCollide = options.onCollide;
this.onTick = options.onTick;
}
Entity.prototype = {
remove: function () {
this.display.remove();
},
// Call this to update the display after changing the underlying data.
updateDisplay: function () {
if (this.data instanceof SAT.Circle) {
this.displayAttrs.cx = this.data.pos.x;
this.displayAttrs.cy = this.data.pos.y;
this.displayAttrs.r = this.data.r;
} else {
this.displayAttrs.path = poly2path(this.data);
}
this.display.attr(this.displayAttrs);
},
tick: function () {
this.onTick();
},
respondToCollision: function (other, response) {
this.onCollide(other, response);
// Collisions between "ghostly" objects don't matter, and
// two "heavy" objects will just remain where they are.
if (this.isSolid && other.isSolid &&
!(this.isHeavy && other.isHeavy)) {
if (this.isHeavy) {
// Move the other object out of us
other.data.pos.add(response.overlapV);
} else if (other.isHeavy) {
// Move us out of the other object
this.data.pos.sub(response.overlapV);
} else {
// Move equally out of each other
response.overlapV.scale(0.5);
this.data.pos.sub(response.overlapV);
other.data.pos.add(response.overlapV);
}
}
}
};
function World(canvas, options) {
options = _.defaults(options || {}, {
loopCount: 1 // number of loops to do each time simulation is called. The higher the more accurate the simulation, but slowers.
});
this.canvas = canvas; // A raphael.js canvas
this.response = new SAT.Response(); // Response reused for collisions
this.loopCount = options.loopCount;
this.entities = {};
}
World.prototype = {
addEntity: function(data, options) {
var entity = new Entity(
data,
data instanceof SAT.Circle ? this.canvas.circle() : this.canvas.path(),
options
);
// Make the display item draggable if requested.
if (options.draggable) {
entity.display.drag(moveDrag(entity, this), startDrag(entity), endDrag(entity));
}
entity.updateDisplay();
this.entities[entity.id] = entity;
return entity;
},
removeEntity: function (entity) {
entity.remove();
delete this.entities[entity.id];
},
simulate: function () {
var entities = _.values(this.entities);
var entitiesLen = entities.length;
// Let the entity do something every simulation tick
_.each(entities, function (entity) {
entity.tick();
});
// Handle collisions - loop a configurable number of times to let things "settle"
var loopCount = this.loopCount;
for (var i = 0; i < loopCount; i++) {
// Naively check for collision between all pairs of entities
// E.g if there are 4 entities: (0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)
for (var aCount = 0; aCount < entitiesLen; aCount++) {
var a = entities[aCount];
for (var bCount = aCount + 1; bCount < entitiesLen; bCount++) {
var b = entities[bCount];
this.response.clear();
var collided;
var aData = a.data;
var bData = b.data;
if (aData instanceof SAT.Circle) {
if (bData instanceof SAT.Circle) {
collided = SAT.testCircleCircle(aData, bData, this.response);
} else {
collided = SAT.testCirclePolygon(aData, bData, this.response);
}
} else {
if (bData instanceof SAT.Circle) {
collided = SAT.testPolygonCircle(aData, bData, this.response);
} else {
collided = SAT.testPolygonPolygon(aData, bData, this.response);
}
}
if (collided) {
a.respondToCollision(b, this.response);
}
}
}
}
// Finally, update the display of each entity now that the simulation step is done.
_.each(entities, function (entity) {
entity.updateDisplay();
});
}
};
(function () {
var canvas = Raphael('example1', 640, 480);
var world = new World(canvas);
var poly = world.addEntity(P(V(160,120), [V(0,0), V(60, 0), V(100, 40), V(60, 80), V(0, 80)]).translate(-50, -40), { solid: true, draggable: true });
var poly2 = world.addEntity(P(V(10,10), [V(0,0), V(30, 0), V(30, 30), V(0, 30)]), { solid: true, draggable: true });
var circle1 = world.addEntity(C(V(50, 200), 30), { solid: true, heavy: true, draggable: true });
function doRotate() {
poly.data.setAngle(poly.data.angle + (Math.PI / 60)); // Assuming 60fps this will take ~2 seconds for a full rotation
world.simulate();
window.requestAnimationFrame(doRotate);
}
window.requestAnimationFrame(doRotate);
}());
(function () {
var canvas = Raphael('example2', 640, 640);
var world = new World(canvas, {
loopCount: 5
});
for (var i = 0; i < 16; i++) {
for (var j = 0; j < 16; j++) {
var displayAttrs = {
fill: 'rgba(' + Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ')'
}
var c = world.addEntity(C(V((40 * i) + 20, (40 * j) + 20), 18), { solid: true, draggable: true, displayAttrs: displayAttrs });
}
}
}());

Dynamic nodes in TableLayout

I have a nodes diagram in table layout.
In case I have some nodes in cell I got this table:
Image:
https://ibb.co/554y9ck
(behind Q2 there are Q0 and Q1.. they are overlapped)
How can I arrange them nicely? :)
Here is my nodesTemplate:
var nodeSimpleTemplate =
$(go.Node, "Auto",mouseEventHandlers(),
new go.Binding("row").makeTwoWay(),
new go.Binding("column", "col").makeTwoWay(),
new go.Binding("alignment", "align", go.Spot.parse).makeTwoWay(go.Spot.stringify),
new go.Binding("layerName", "isSelected", function(s) { return s ? "Foreground" : ""; }).ofObject(),
{
//locationSpot: go.Spot.Center,
// when the user clicks on a Node, highlight all Links coming out of the node
// and all of the Nodes at the other ends of those Links.
click: function (e, node) {
var diagram = node.diagram;
diagram.startTransaction("Click simple node");
diagram.clearHighlighteds();
// #ts-ignore
node.findLinksOutOf().each(function (l) {
changeLinkCategory(e, l);
l.isHighlighted = true;
});
// #ts-ignore
node.findNodesOutOf().each(function (n) {
n.isHighlighted = true;
});
changeNodeCategory(e, node);
diagram.commitTransaction("Click simple node");
}
},
$(go.Shape, "Ellipse",
{
fill: $(go.Brush, "Linear", {0: "white", 1: "lightblue"}),
stroke: "darkblue", strokeWidth: 2
}),
$(go.Panel, "Table",
{defaultAlignment: go.Spot.Left, margin: 4},
$(go.RowColumnDefinition, {column: 1, width: 4}),
$(go.TextBlock,
{row: 0, column: 0, columnSpan: 3, alignment: go.Spot.Center},
{font: "bold 14pt sans-serif"},
new go.Binding("text", "key"))
));
var nodeDetailedTemplate =
$(go.Node, "Auto",mouseEventHandlers(),
new go.Binding("row").makeTwoWay(),
new go.Binding("column", "col").makeTwoWay(),
new go.Binding("alignment", "align", go.Spot.parse).makeTwoWay(go.Spot.stringify),
new go.Binding("layerName", "isSelected", function(s) { return s ? "Foreground" : ""; }).ofObject(),
{
//locationSpot: go.Spot.Center,
// when the user clicks on a Node, highlight all Links coming out of the node
// and all of the Nodes at the other ends of those Links.
click: function (e, node) {
var diagram = node.diagram;
diagram.startTransaction("Click Details node");
diagram.clearHighlighteds();
// #ts-ignore
node.findLinksOutOf().each(function (l) {
changeLinkCategory(e, l);
l.isHighlighted = true;
});
// #ts-ignore
node.findNodesOutOf().each(function (n) {
n.isHighlighted = true;
});
changeNodeCategory(e, node);
diagram.commitTransaction("Click Details node");
}
},
$(go.Shape, "Ellipse",
{
fill: $(go.Brush, "Linear", {0: "white", 1: "lightblue"}),
stroke: "darkblue", strokeWidth: 2
}),
$(go.Panel, "Table",
{defaultAlignment: go.Spot.Left, margin: 4},
$(go.RowColumnDefinition, {column: 1, width: 4}),
$(go.TextBlock,
{row: 0, column: 0, columnSpan: 3, alignment: go.Spot.Center},
{font: "bold 14pt sans-serif"},
new go.Binding("text", "key")),
$(go.TextBlock, "Time: ",
{row: 1, column: 0}, {font: "bold 10pt sans-serif"}),
$(go.TextBlock,
{row: 1, column: 2},
new go.Binding("text", "time")),
$(go.TextBlock, "Parameters: ",
{row: 2, column: 0}, {font: "bold 10pt sans-serif"}),
$(go.TextBlock,
{row: 2, column: 2},
new go.Binding("text", "parameters"))
)
);
// for each of the node categories, specify which template to use
dia.nodeTemplateMap.add("simple", nodeSimpleTemplate);
dia.nodeTemplateMap.add("detailed", nodeDetailedTemplate);
Here is the diagram definition:
public initDiagram(): go.Diagram {
// define a custom ResizingTool to limit how far one can shrink a row or column
function LaneResizingTool() {
go.ResizingTool.call(this);
}
go.Diagram.inherit(LaneResizingTool, go.ResizingTool);
LaneResizingTool.prototype.computeMinSize = function() {
var diagram = this.diagram;
var lane = this.adornedObject.part; // might be row or column
var horiz = (lane.rowSpan >= 9999); // column header
var margin = diagram.nodeTemplate.margin;
var bounds = new go.Rect();
diagram.findTopLevelGroups().each(function(g) {
if (horiz ? (g.column === lane.column) : (g.row === lane.row)) {
var b = diagram.computePartsBounds(g.memberParts);
if (b.isEmpty()) return; // nothing in there? ignore it
b.unionPoint(g.location); // keep any empty space on the left and top
b.addMargin(margin); // assume the same node margin applies to all nodes
if (bounds.isEmpty()) {
bounds = b;
} else {
bounds.unionRect(b);
}
}
});
// limit the result by the standard value of computeMinSize
var msz = go.ResizingTool.prototype.computeMinSize.call(this);
if (bounds.isEmpty()) return msz;
return new go.Size(Math.max(msz.width, bounds.width), Math.max(msz.height, bounds.height));
};
LaneResizingTool.prototype.resize = function(newr) {
var lane = this.adornedObject.part;
var horiz = (lane.rowSpan >= 9999);
var lay = this.diagram.layout; // the TableLayout
if (horiz) {
var col = lane.column;
var coldef = lay.getColumnDefinition(col);
coldef.width = newr.width;
} else {
var row = lane.row;
var rowdef = lay.getRowDefinition(row);
rowdef.height = newr.height;
}
lay.invalidateLayout();
};
// end LaneResizingTool class
function AlignmentDraggingTool() {
go.DraggingTool.call(this);
}
go.Diagram.inherit(AlignmentDraggingTool, go.DraggingTool);
AlignmentDraggingTool.prototype.moveParts = function(parts, offset, check) {
go.DraggingTool.prototype.moveParts.call(this, parts, offset, check);
var tool = this;
parts.iteratorKeys.each(function(part) {
if (part instanceof go.Link) return;
var col = part.column;
var row = part.row;
if (typeof col === "number" && typeof row === "number") {
var b = computeCellBounds(col, row);
part.alignment = new go.Spot(0.5, 0.5, b.centerX, b.centerY); // offset from center of cell
}
})
}
// end AlignmentDraggingTool
// Utility functions, assuming the Diagram.layout is a TableLayout,
// and that the rows and columns are implemented as Groups
function computeCellBounds(col, row) { // this is only valid after a layout
//#ts-ignore
var coldef = dia.layout.getColumnDefinition(col);
//#ts-ignore
var rowdef = dia.layout.getRowDefinition(row);
return new go.Rect(coldef.position, rowdef.position, coldef.total, rowdef.total);
}
function findColumnGroup(col) {
var it = dia.findTopLevelGroups();
while (it.next()) {
var g = it.value;
if (g.column === col && g.rowSpan >= 9999) return g;
}
return null;
}
function findRowGroup(row) {
var it = dia.findTopLevelGroups();
while (it.next()) {
var g = it.value;
if (g.row === row && g.columnSpan >= 9999) return g;
}
return null;
}
function mouseEventHandlers() { // standard mouse drag-and-drop event handlers
return {
mouseDragEnter: function(e) { mouseInCell(e, true); },
mouseDragLeave: function(e) { mouseInCell(e, false); },
mouseDrop: function(e) { mouseDropInCell(e, e.diagram.selection); }
};
}
function mouseInCell(e, highlight) {
e.diagram.clearHighlighteds();
var col = e.diagram.layout.findColumnForDocumentX(e.documentPoint.x);
if (col < 1) col = 1; // disallow dropping in headers
var g = findColumnGroup(col);
if (g !== null) g.isHighlighted = highlight;
var row = e.diagram.layout.findRowForDocumentY(e.documentPoint.y);
if (row < 1) row = 1;
g = findRowGroup(row);
if (g !== null) g.isHighlighted = highlight;
}
function mouseDropInCell(e, coll) {
var col = e.diagram.layout.findColumnForDocumentX(e.documentPoint.x);
if (col < 1) col = 1; // disallow dropping in headers
var row = e.diagram.layout.findRowForDocumentY(e.documentPoint.y);
if (row < 1) row = 1;
coll.each(function(node) {
if (node instanceof go.Node) {
node.column = col;
node.row = row;
// adjust the alignment to the new cell's center point
var cb = computeCellBounds(col, row);
var ab = node.actualBounds.copy();
//#ts-ignore
if (ab.right > cb.right-node.margin.right) ab.x -= (ab.right - cb.right + node.margin.right);
//#ts-ignore
if (ab.left < cb.left+node.margin.left) ab.x = cb.left + node.margin.left;
//#ts-ignore
if (ab.bottom > cb.bottom-node.margin.bottom) ab.y -= (ab.bottom - cb.bottom + node.margin.bottom);
//#ts-ignore
if (ab.top < cb.top+node.margin.top) ab.y = cb.top + node.margin.top;
var off = ab.center.subtract(cb.center);
node.alignment = new go.Spot(0.5, 0.5, off.x, off.y);
}
});
dia.layoutDiagram(true);
}
const $ = go.GraphObject.make;
const dia = $(go.Diagram,{
layout: $(TableLayout,
$(go.RowColumnDefinition, { row: 0, height: 50, minimum: 50 }),
$(go.RowColumnDefinition, { column: 0, width: 100, minimum: 100 }),
// defaultStretch: go.GraphObject.Horizontal,
),
'initialContentAlignment': go.Spot.Center,
'undoManager.isEnabled': true,
resizingTool: new LaneResizingTool(),
model: $(go.GraphLinksModel,
{
linkToPortIdProperty: 'toPort',
linkFromPortIdProperty: 'fromPort',
linkKeyProperty: 'key' // IMPORTANT! must be defined for merges and data sync when using GraphLinksModel
}
),
});
If you start from the Table Layout sample, you can just add this line in the Group template:
{
layout: $(go.GridLayout, { wrappingColumn: 1 })
}
Adapt the GridLayout as needed, or replace it with a different layout.

Plotly plotting in the past

When extending traces with plotly the points are drawn in the past. As you can see in the picture, any
Picture of the problem
The chart now consists of 2 parts one on the right is data from the database, and to the left is data being plotted in "real-time". The data that is being plotted in realtime is to the left of the data from the database is even though the timestamps are after the ones from the databases.
The time stamp on the console logs is correct but plotly is placing them at the wrong time on the x-axis. The should be drawn at the end of the graph.
updateData (sensorData) {
if (!this.initiated || !this.isLiveData) {
return
}
let y = []
let i = 0
let x = []
let traces = []
for (let sensor in this.configuration) {
for (let signal in this.configuration[sensor]) {
let xDate = new Date(sensorData[sensor].timestamp)
if (sensorData.hasOwnProperty(sensor) && sensorData[sensor].hasOwnProperty(signal)) {
y.push([sensorData[sensor][signal]])
x.push([xDate.getTime()])
}
// x time seems to be good here
console.log('Update data', xDate.getTime(), xDate)
traces.push(i)
i++
}
}
console.log('Plotting', y, x, this.widget.getXRange())
if (y.length > 0) {
this.$plotly.extendTraces('plotly-chart', {
y: y,
x: x
}, traces)
}
},
The data from the db is added with following code.
updateFromDb (sensorData) {
let data = []
let x = []
let yData = {}
for (let sensor in this.configuration) {
for (let i = 0, len = sensorData[sensor].length; i < len; i++) {
let timestamp = sensorData[sensor][i].timestamp
x.push(timestamp)
for (let source in this.configuration[sensor]) {
let name = sensor + ' ' + getSignalName(source, this.angleLabel)
if (!yData.hasOwnProperty(name)) {
yData[name] = {
data: [],
color: this.configuration[sensor][source].color
}
}
if (!sensorData[sensor][i].hasOwnProperty(source)) {
yData[name].data.push(0)
} else {
yData[name].data.push(sensorData[sensor][i][source])
}
if (this.configuration[sensor][source].hasOwnProperty('yaxis')) {
yData[name]['yaxis'] = 'y' + this.configuration[sensor][source].yaxis
}
}
}
}
for (let name in yData) {
let sensorData = {
name: name,
x: x,
y: yData[name].data,
type: 'line',
mode: 'lines',
line: {
width: 2,
color: yData[name].color
},
marker: {
width: 2,
color: yData[name].color
}
}
if (yData[name].hasOwnProperty('yaxis')) {
sensorData['yaxis'] = yData[name].yaxis
}
data.push(sensorData)
}
this.$plotly.react(
document.getElementById('plotly-chart'),
data,
this.getLayout(false),
this.chartProperties
)
}
There is also a function that scroll the window over xaxis every 50 ms to make it look smooth.
windowScroller () {
if (!this.initiated || !this.isLiveData) {
return
}
let timeDifference = 0
if (this.timeDifference !== null) {
timeDifference = this.timeDifference
}
/**
* Make sure the line never gets behind the scroller.
*/
if (Object.keys(this.latestPoint).length > 0) {
let latestTime = this.latestPoint[Object.keys(this.latestPoint)[0]].timestamp
let scrollDiff = new Date().getTime() - latestTime
if (scrollDiff !== this.scrollDelay && this.lastDelayTime !== latestTime && scrollDiff < 60000) {
this.lastDelayTime = latestTime
this.scrollDelay = scrollDiff
// console.log('update scroll', scrollDiff, 'from', latestTime)
}
}
let currentTime = new Date().getTime() - timeDifference - this.scrollDelay
let firstTime = new Date().getTime() - this.getMilisecondsFromMinutes(this.widget.getXRange()) - timeDifference - this.scrollDelay
let relayoutPromise = new Promise((resolve, reject) => {
this.$plotly.relayout('plotly-chart', {
xaxis: {
color: '#fff',
type: 'date',
range: [firstTime, currentTime]
}
})
})
relayoutPromise.then(() => {
console.log('relayout')
})
let data = document.getElementById('plotly-chart').data
// We calculate the max points using 4 hertz
let maxPoints = (this.getSecondsFromMinutes(this.widget.getXRange()) + 10) * this.widget.getRefreshRate()
if (this.minMax) {
maxPoints = maxPoints * 2
}
for (let i in data) {
if (data[i].y.length >= maxPoints) {
data[i].y.shift()
data[i].x.shift()
}
}
}

FillColor on Highcharts working differently

What I'm trying to do is to fill individual symbols with a certain color based on how it's performing against it's goal. It used to work, but for some reason no longer is. I've tried to figure out where the issue is, but couldn't figure it out:
var seriesColor = "#000";
preprocessData = function (divId, data, last, goal) {
var nData = []; var symbol = "diamond"; var radius = 5;
var colorGood = '#348017'; var colorBad = '#E42217'; var colorUse;
for (var i = 0; i < data.length; i++) {
if (data[i] <= goal[i]) { colorUse = colorGood; }
else if (data[i] > goal[1] * 1.17) { colorUse = colorBad; }
else { colorUse = '#FFE303'; }
if((divId == "WebServItRun" || divId == "ISOServicesRun") && i == 9 ){
symbol = "circle";
radius = 10;
}
else if((divId == "WebServItRun" || divId == "ISOServicesRun") && i != 7){
symbol = "diamond";
radius = 5;
}
nData.push({
y: data[i],
x: i,
color: colorUse,
fillColor: colorUse,
marker: {
symbol: symbol,
radius: radius
}
});
}
seriesColor = colorUse;
return nData;
};
Then in the series calls:
{
type: 'spline',
name: series2,
data: preprocessData(divId, current, last, goal),
color: seriesColor,
marker: {
symbol: 'diamond'
}
}
Any idea
If you're trying to set the fill color of the marker, then the fillColor declaration needs to be inside the marker object.
so, this:
fillColor: colorUse,
marker: {
symbol: symbol,
radius: radius
}
should be this:
marker: {
fillColor: colorUse,
symbol: symbol,
radius: radius
}

Object reference passing in raphael drag function

I am creating a module to graphically visualize workflows using raphael,which take data from a data base. For this i have created a class called "FlowEdit", and created move, up and dragger function according to raphael.
But in move function when i am trying to access connections list using object reference, than i am unable to reference it, it gives undefined error.
Code for the class is this:-
//class definition
function FlowView(list) {
this.list = list;
this.connections = [];
this.r = Raphael("holder", 1400, 500);
this.shapes = [];
this.texts = [];
this.y_center = 500 / 2;
//box size
this.r_width = 60;
this.r_height = 40;
// To define virtual regions
this.x_offset = 50;
this.y_offset = 40;
this.x_start = 40;
//this.color, this.tempS, this.tempT;
//Define position in y direction
this.top_count = [0];
this.bottom_count = [0];
//Initialize Top_count & Bottom_Count Arrays
for (var i = 0; i < this.list.length; i++) {
this.top_count.push(0);
this.bottom_count.push(0);
}
}
;
// Give starting points from list
FlowView.prototype.start_point = function () {
var start_list = [];
for (var i in this.list) {
if (this.list[i][1] == this.list[i][2][0]) {
start_list.push(this.list[i][1]);
}
}
return start_list;
};
//For Finding index of an element in list
FlowView.prototype.index_of = function (curr_point) {
for (var i in this.list) {
if (this.list[i][1] == curr_point) {
return i;
}
}
};
//add next function
FlowView.prototype.add_next = function () {
for (var i in this.list) {
if (this.list[i][3][0] == "NULL") {
//For all last nodes add same to their next
this.list[i][3][0] = this.list[i][1];
}
if (this.list[i][3].length == 0) {
//For all last nodes add same to their next
this.list[i][3].push(this.list[i][1]);
}
}
};
//For given next of all nodes add previous to those nodes
FlowView.prototype.add_previous = function () {
for (var i in this.list) {
for (var j in this.list[i][3]) {
//For all next add current node to their previous list
var curr_index = this.index_of(this.list[i][3][j]);
if (this.list[curr_index][2].indexOf(this.list[i][1]) == -1 && (curr_index != i)) {
this.list[curr_index][2].push(this.list[i][1]);
}
}
}
//Add previous of all start node
for (var i in this.list) {
if (this.list[i][2].length == 0) {
this.list[i][2].push(this.list[i][1]);
}
}
};
//Region update recursively
FlowView.prototype.region_update = function (curr_index) {
if (this.list[curr_index][1] != this.list[curr_index][3][0]) {
for (var i in this.list[curr_index][3]) {
var next_index = this.index_of(this.list[curr_index][3][i]);
if (this.list[next_index][0] < this.list[curr_index][0] + 1) {
this.list[next_index][0] = this.list[curr_index][0] + 1;
this.region_update(next_index);
}
}
}
};
//Draw the workflow for given data structure
FlowView.prototype.construct = function () {
var open = this.start_point();
var close = [];
while (open.length != 0) {
var curr_point = open.shift();
var curr_index = this.index_of(curr_point);
//document.write(curr_index);
//draw box
var curr_region = this.list[curr_index][0];
//document.write(curr_region);
var x_cord = parseInt(curr_region) * (this.x_offset + this.r_width) + this.x_start;
//document.write(x_start);
var y_cord = 0;
if (this.top_count[curr_region] == 0 && this.bottom_count[curr_region] == 0) {
y_cord = this.y_center - this.r_height / 2;
this.top_count[curr_region] = 1;
this.bottom_count[curr_region] = 1;
}
else if (this.top_count[curr_region] <= this.bottom_count[curr_region]) {
y_cord = this.y_center - this.r_height / 2 - this.top_count[curr_region] * (this.y_offset + this.r_height);
this.top_count[curr_region] = this.top_count[curr_region] + 1;
}
else {
y_cord = this.y_center + this.r_height / 2 + this.bottom_count[curr_region] * (this.y_offset + this.r_height) - this.r_height;
this.bottom_count[curr_region] = this.bottom_count[curr_region] + 1;
}
//drawing the box
this.shapes[this.list[curr_index][1]] = this.r.rect(x_cord, y_cord, this.r_width, this.r_height, 10);
this.texts[this.list[curr_index][1]] = this.r.text(x_cord + this.r_width / 2, y_cord + this.r_height / 2, this.list[curr_index][1]);
// Adding next nodes to open list
for (var i in this.list[curr_index][3]) {
//If not in open than add to open
if (this.list[curr_index][3][0] != this.list[curr_index][1]) {
if (open.indexOf(this.list[curr_index][3][i]) == -1 && close.indexOf(this.list[curr_index][3][i]) == -1) {
open.push(this.list[curr_index][3][i]);
}
}
}
//Increasing region index for each next node
this.region_update(curr_index);
close.push(curr_point);
//document.write(open.toString()+"</br>");
//document.write(close.toString()+"</br>");
}
for (var j in this.list) {
if (this.list[j][1] != this.list[j][3][0]) {
for (var i in this.list[j][3]) {
//make link for each previous
if (close.indexOf(this.list[j][3][i]) != -1) {
this.connections.push(this.r.connection(this.shapes[this.list[j][1]], this.shapes[this.list[j][3][i]], "bcd"));
}
}
}
}
};
FlowView.prototype.dragger = function () {
// Original cords for main element
this.ox = this.type == "ellipse" ? this.attr("cx") : this.attr("x");
this.oy = this.type == "ellipse" ? this.attr("cy") : this.attr("y");
if (this.type != "text") this.animate({"fill-opacity":.2}, 500);
// Original co-ords for pair element
this.pair.ox = this.pair.type == "ellipse" ? this.pair.attr("cx") : this.pair.attr("x");
this.pair.oy = this.pair.type == "ellipse" ? this.pair.attr("cy") : this.pair.attr("y");
if (this.pair.type != "text") this.pair.animate({"fill-opacity":.2}, 500);
};
FlowView.prototype.move = function (dx, dy) {
// Move main element
var att = this.type == "ellipse" ? {cx:this.ox + dx, cy:this.oy + dy} :
{x:this.ox + dx, y:this.oy + dy};
this.attr(att);
// Move paired element
att = this.pair.type == "ellipse" ? {cx:this.pair.ox + dx, cy:this.pair.oy + dy} :
{x:this.pair.ox + dx, y:this.pair.oy + dy};
this.pair.attr(att);
//document.write("adass");
//document.write(x_offset);
// Move connections
for (var i = this.connections.length; i--;) {
this.r.connection(this.connections[i]);
}
this.r.safari();
};
FlowView.prototype.up = function () {
// Fade original element on mouse up
if (this.type != "text") this.animate({"fill-opacity":0}, 500);
// Fade paired element on mouse up
if (this.pair.type != "text") this.pair.animate({"fill-opacity":0}, 500);
// Move connections
};
FlowView.prototype.drag_initialize = function () {
for (var i in this.shapes) {
var color = Raphael.getColor();
var tempS = this.shapes[i].attr({fill:color, stroke:color, "fill-opacity":0, "stroke-width":2, cursor:"move"});
var tempT = this.texts[i].attr({fill:color, stroke:"none", "font-size":15, cursor:"move"});
this.shapes[i].drag(this.move, this.dragger, this.up);
this.texts[i].drag(this.move, this.dragger, this.up);
// Associate the elements
tempS.pair = tempT;
tempT.pair = tempS;
}
};
Using above code i am able to draw graph & drag items,but when i drag items the connected path are not dragged along it.So where i am doing wrong. For making connection i used the same code given in raphael demos..
This is a common annoyance and there is, fortunately, a very simple solution!
Problem: Raphael is using the functions you specify (this.move, this.dragger, and this.up) but it is NOT calling them in the context of your object. So instead of referring to your object, the this variable is actually referring to window. Decidedly not helpful.
Solution: use a function closure to bind in a reference to your object instance. Update your drag_initialize function with this:
var self = this;
this.shapes[i].drag(function(){ self.move(); }, function() { self.dragger(); }, function() { self.up(); } );
this.texts[i].drag(function() { self.move(); }, function() { self.dragger(); }, function() { self.up(); } );
Hi I found the answer to the problem.
In the move function i return another function and while calling drag i give the object argument in move function hence the context of current object is passed. Now changed move function is:-
FlowView.prototype.move = function (obj) {
// Move main element
return function(dx, dy){
var att = this.type == "ellipse" ? {cx:this.ox + dx, cy:this.oy + dy} :
{x:this.ox + dx, y:this.oy + dy};
this.attr(att);
// Move paired element
att = this.pair.type == "ellipse" ? {cx:this.pair.ox + dx, cy:this.pair.oy + dy} :
{x:this.pair.ox + dx, y:this.pair.oy + dy};
this.pair.attr(att);
// Move connections
for (var i = obj.connections.length; i--;) {
obj.r.connection(obj.connections[i]);
}
obj.r.safari();
}
};
And calling the drag with
this.shapes[i].drag(this.move(this), this.dragger, this.up);
this.texts[i].drag(this.move(this), this.dragger, this.up);

Categories