I'm playing with vis.js because I like its Network Visualization module. I'd like to know, as I can't find it in documentation, if it's possibile to select multiple nodes.
Cheers,
Riccardo
Update!
the link for the documentation is http://visjs.org/docs/network/interaction.html
set the multiselect property to true.
add this section to your network option object.
interaction: { multiselect: true}
Search for "selectable" property in documentation at
http://visjs.org/docs/network/
If true, nodes in the network can be selected by clicking them. Long
press can be used to select multiple nodes.
If you are looking for a rectangle to select your nodes, just look at this thread on visjs-network github : https://github.com/almende/vis/issues/3594
Full demo code :
const 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' }
]);
const edges = new vis.DataSet([
{ from: 1, to: 3 },
{ from: 1, to: 2 },
{ from: 2, to: 4 },
{ from: 2, to: 5 }
]);
const options = {
layout: { randomSeed: 2 },
interaction:{
hover: true,
multiselect: true
}
};
// Everything is in there
const makeMeMultiSelect = (container, network, nodes) => {
const NO_CLICK = 0;
const RIGHT_CLICK = 3;
// Disable default right-click dropdown menu
container[0].oncontextmenu = () => false;
// State
let drag = false, DOMRect = {};
// Selector
const canvasify = (DOMx, DOMy) => {
const { x, y } = network.DOMtoCanvas({ x: DOMx, y: DOMy });
return [x, y];
};
const correctRange = (start, end) =>
start < end ? [start, end] : [end, start];
const selectFromDOMRect = () => {
const [sX, sY] = canvasify(DOMRect.startX, DOMRect.startY);
const [eX, eY] = canvasify(DOMRect.endX, DOMRect.endY);
const [startX, endX] = correctRange(sX, eX);
const [startY, endY] = correctRange(sY, eY);
network.selectNodes(nodes.get().reduce(
(selected, { id }) => {
const { x, y } = network.getPositions(id)[id];
return (startX <= x && x <= endX && startY <= y && y <= endY) ?
selected.concat(id) : selected;
}, []
));
}
// Listeners
container.on("mousedown", function({ which, pageX, pageY }) {
// When mousedown, save the initial rectangle state
if(which === RIGHT_CLICK) {
Object.assign(DOMRect, {
startX: pageX - this.offsetLeft,
startY: pageY - this.offsetTop,
endX: pageX - this.offsetLeft,
endY: pageY - this.offsetTop
});
drag = true;
}
});
container.on("mousemove", function({ which, pageX, pageY }) {
// Make selection rectangle disappear when accidently mouseupped outside 'container'
if(which === NO_CLICK && drag) {
drag = false;
network.redraw();
}
// When mousemove, update the rectangle state
else if(drag) {
Object.assign(DOMRect, {
endX: pageX - this.offsetLeft,
endY: pageY - this.offsetTop
});
network.redraw();
}
});
container.on("mouseup", function({ which }) {
// When mouseup, select the nodes in the rectangle
if(which === RIGHT_CLICK) {
drag = false;
network.redraw();
selectFromDOMRect();
}
});
// Drawer
network.on('afterDrawing', ctx => {
if(drag) {
const [startX, startY] = canvasify(DOMRect.startX, DOMRect.startY);
const [endX, endY] = canvasify(DOMRect.endX, DOMRect.endY);
ctx.setLineDash([5]);
ctx.strokeStyle = 'rgba(78, 146, 237, 0.75)';
ctx.strokeRect(startX, startY, endX - startX, endY - startY);
ctx.setLineDash([]);
ctx.fillStyle = 'rgba(151, 194, 252, 0.45)';
ctx.fillRect(startX, startY, endX - startX, endY - startY);
}
});
}; // end makeMeMultiSelect
$(document).ready(() => {
const container = $("#network");
const network = new vis.Network(container[0], { nodes, edges }, options);
makeMeMultiSelect(container, network, nodes);
});
I've just discovered that in vis.js you can select multiple nodes long pressing them.
Related
This bounty has ended. Answers to this question are eligible for a +50 reputation bounty. Bounty grace period ends in 3 hours.
Software Dev wants to draw more attention to this question.
I have a bar chart in Chart.js (using the latest version), and I want to make some visual change when the mouse is hovering over a category label. How would I implement either or both of the following visual changes?
Make the cursor be a pointer while hovering over a label.
Make the label be in a different color while it is being hovered on.
A related question is here: How to detect click on chart js 3.7.1 axis label?. However, my question is about hovering over a label, without clicking on the label.
In the example below, I want something to happen when hovering on these texts: Item A, Item B, Item C.
window.onload = function() {
var ctx = document.getElementById('myChart').getContext('2d');
window.myBar = new Chart(ctx, {
type: 'bar',
data: {
labels: ['Item A', 'Item B', 'Item C'],
datasets: [{
data: [1, 2, 3],
backgroundColor: 'lightblue'
}]
},
options: {
responsive: true,
indexAxis: 'y',
plugins: {
legend: {
display: false
},
tooltip: {
enabled: false
},
}
}
});
};
.chart-container {
position: relative;
height: 90vh;
}
<script src="https://cdn.jsdelivr.net/npm/chart.js#4.2.0"></script>
<div class="chart-container">
<canvas id="myChart"></canvas>
</div>
You can just use the custom plugin from that question and ignore everything but mousemove events instead of ignoring everything but click events:
const findLabel = (labels, evt) => {
let found = false;
let res = null;
labels.forEach(l => {
l.labels.forEach((label, index) => {
if (evt.x > label.x && evt.x < label.x2 && evt.y > label.y && evt.y < label.y2) {
res = {
label: label.label,
index
};
found = true;
}
});
});
return [found, res];
};
const getLabelHitboxes = (scales) => (Object.values(scales).map((s) => ({
scaleId: s.id,
labels: s._labelItems.map((e, i) => ({
x: e.translation[0] - s._labelSizes.widths[i],
x2: e.translation[0] + s._labelSizes.widths[i] / 2,
y: e.translation[1] - s._labelSizes.heights[i] / 2,
y2: e.translation[1] + s._labelSizes.heights[i] / 2,
label: e.label,
index: i
}))
})));
const plugin = {
id: 'customHover',
afterEvent: (chart, event, opts) => {
const evt = event.event;
if (evt.type !== 'mousemove') {
return;
}
const [found, labelInfo] = findLabel(getLabelHitboxes(chart.scales), evt);
if (found) {
console.log(labelInfo);
}
}
}
Chart.register(plugin);
const options = {
type: 'line',
data: {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
borderColor: 'pink'
},
{
label: '# of Points',
data: [7, 11, 5, 8, 3, 7],
borderColor: 'orange'
}
]
},
options: {}
}
const ctx = document.getElementById('chartJSContainer').getContext('2d');
new Chart(ctx, options);
<body>
<canvas id="chartJSContainer" width="600" height="400"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.js"></script>
</body>
To change the cursor to a pointer when hovering over a category label in a Chart.js bar chart, you can add:
options: {
plugins: {
tooltip: {
mode: 'index',
intersect: false
},
},
interaction: {
mode: 'index',
intersect: false
},
onHover: function(evt, elements) {
if (elements.length) {
document.getElementById("myChart").style.cursor = "pointer";
} else {
document.getElementById("myChart").style.cursor = "default";
}
},
// ...
}
To change the color of a label when it is being hovered on, you can add:
options: {
plugins: {
tooltip: {
mode: 'index',
intersect: false
},
},
interaction: {
mode: 'index',
intersect: false
},
onHover: function(evt, elements) {
if (elements.length) {
var chart = evt.chart;
var datasetIndex = elements[0].datasetIndex;
var index = elements[0].index;
chart.data.labels[index] = '<span style="color: red;">' + chart.data.labels[index] + '</span>';
chart.update();
} else {
var chart = evt.chart;
chart.data.labels = ['Item A', 'Item B', 'Item C'];
chart.update();
}
},
// ...
}
To make the cursor a pointer while hovering over a label, you can try to assign a CSS cursor value to event.native.target.style.cursor when hover is triggered.
event.native.target.style.cursor = 'pointer';
To make the label a different color while it is being hovered on, you can try this
myChart.config.options.scales.y.ticks.color = hoverColors; // ['black','red','black'], ['black','black','red'], ['red','black','black']
UPDATE
Thanks to LeeLenalee for giving an almost correct answer. I've edited the code above so it fits what is required in the problem. Don't forget to change source of the library in the HTML from :
https://cdn.jsdelivr.net/npm/chart.js#4.2.0
to :
https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.js
Updated code:
window.onload = function() {
const findLabel = (labels, evt) => {
let found = false;
let res = null;
try {
labels.forEach(l => {
l.labels.forEach((label, index) => {
if (evt.x > label.x && evt.x < label.x2 && evt.y > label.y && evt.y < label.y2) {
res = {
label: label.label,
index
};
found = true;
}
});
});
} catch (e) {}
return [found, res];
};
const getLabelHitboxes = (scales) => {
try {
return Object.values(scales).map((s) => ({
scaleId: s.id,
labels: s._labelItems.map((e, i) => ({
x: e.translation[0] - s._labelSizes.widths[i],
x2: e.translation[0] + s._labelSizes.widths[i] / 2,
y: e.translation[1] - s._labelSizes.heights[i] / 2,
y2: e.translation[1] + s._labelSizes.heights[i] / 2,
label: e.label,
index: i
}))
}));
} catch (e) {}
};
const changeCursorAndLabelColor = (event, chart, index, hoverMode) => {
// your hover color here
// const hoverColor = '#ff0000';
const hoverColor = 'red';
const hoverColors = [];
for (let i = 0; i < myChart.data.datasets[0].data.length; i++) {
if (hoverMode) {
// change cursor
event.native.target.style.cursor = 'pointer';
if (index === i) {
hoverColors.push(hoverColor);
} else {
hoverColors.push(defaultLabelColor);
}
} else {
// change cursor
event.native.target.style.cursor = 'default';
hoverColors.push(defaultLabelColor);
}
}
// change label to your hover color
myChart.config.options.scales.y.ticks.color = hoverColors;
// update chart when hover is triggered
myChart.update();
}
let foundMode = false;
const plugin = {
id: 'customHover',
afterEvent: (chart, event, opts) => {
const evt = event.event;
if (evt.type !== 'mousemove') {
return;
}
const [found, labelInfo] = findLabel(getLabelHitboxes(chart.scales), evt);
if (found && myChart.data.labels.includes(labelInfo.label)) {
changeCursorAndLabelColor(evt, chart, labelInfo.index, true);
foundMode = true;
} else {
if (foundMode) changeCursorAndLabelColor(evt, chart, null, false);
foundMode = false;
}
}
}
Chart.register(plugin);
var ctx = document.getElementById('myChart');
const myChart = new Chart(ctx, {
type: 'bar',
data: {
labels: ['Item A', 'Item B', 'Item C'],
datasets: [{
label: 'My Data',
data: [1, 2, 3],
backgroundColor: 'lightblue'
}]
},
options: {
responsive: true,
indexAxis: 'y',
plugins: {
legend: {
display: false
},
tooltip: {
enabled: false
},
},
onHover: (event, chart) => {
if (foundMode) changeCursorAndLabelColor(event, chart, null, false);
foundMode = false;
}
}
});
const defaultLabelColor = myChart.config.options.scales.y.ticks.color;
};
.chart-container {
position: relative;
height: 90vh;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.js"></script>
<div class="chart-container">
<canvas id="myChart"></canvas>
</div>
I am trying to apply smart routing of links with the use of ports using JointJS. This documentation shows the one I am trying to achieve. The example on the docs though shows only the programmatic way of adding Link from point A to point B. How do you do this with the use of ports?
Here's my code: JSFiddle.
HTML:
<html>
<body>
<button id="btnAdd">Add Table</button>
<div id="dbLookupCanvas"></div>
</body>
</html>
JS
$(document).ready(function() {
$('#btnAdd').on('click', function() {
AddTable();
});
InitializeCanvas();
// Adding of two sample tables on first load
AddTable(50, 50);
AddTable(250, 50);
});
var graph;
var paper
var selectedElement;
var namespace;
function InitializeCanvas() {
let canvasContainer = $('#dbLookupCanvas').parent();
namespace = joint.shapes;
graph = new joint.dia.Graph({}, {
cellNamespace: namespace
});
paper = new joint.dia.Paper({
el: document.getElementById('dbLookupCanvas'),
model: graph,
width: canvasContainer.width(),
height: 500,
gridSize: 10,
drawGrid: true,
cellViewNamespace: namespace,
validateConnection: function(cellViewS, magnetS, cellViewT, magnetT, end, linkView) {
return (magnetS !== magnetT);
},
snapLinks: {
radius: 20
}
});
//Dragging navigation on canvas
var dragStartPosition;
paper.on('blank:pointerdown',
function(event, x, y) {
dragStartPosition = {
x: x,
y: y
};
}
);
paper.on('cell:pointerup blank:pointerup', function(cellView, x, y) {
dragStartPosition = null;
});
$("#dbLookupCanvas")
.mousemove(function(event) {
if (dragStartPosition)
paper.translate(
event.offsetX - dragStartPosition.x,
event.offsetY - dragStartPosition.y);
});
// Remove links not connected to anything
paper.model.on('batch:stop', function() {
var links = paper.model.getLinks();
_.each(links, function(link) {
var source = link.get('source');
var target = link.get('target');
if (source.id === undefined || target.id === undefined) {
link.remove();
}
});
});
paper.on('cell:pointerdown', function(elementView) {
resetAll(this);
let isElement = elementView.model.isElement();
if (isElement) {
var currentElement = elementView.model;
currentElement.attr('body/stroke', 'orange');
selectedElement = elementView.model;
} else
selectedElement = null;
});
paper.on('blank:pointerdown', function(elementView) {
resetAll(this);
});
$('#dbLookupCanvas')
.attr('tabindex', 0)
.on('mouseover', function() {
this.focus();
})
.on('keydown', function(e) {
if (e.keyCode == 46)
if (selectedElement) selectedElement.remove();
});
}
function AddTable(xCoord = undefined, yCoord = undefined) {
// This is a sample database data here
let data = [
{columnName: "radomData1"},
{columnName: "radomData2"}
];
if (xCoord == undefined && yCoord == undefined)
{
xCoord = 50;
yCoord = 50;
}
const rect = new joint.shapes.standard.Rectangle({
position: {
x: xCoord,
y: yCoord
},
size: {
width: 150,
height: 200
},
ports: {
groups: {
'a': {},
'b': {}
}
}
});
$.each(data, (i, v) => {
const port = {
group: 'a',
args: {}, // Extra arguments for the port layout function, see `layout.Port` section
label: {
position: {
name: 'right',
args: {
y: 6
} // Extra arguments for the label layout function, see `layout.PortLabel` section
},
markup: [{
tagName: 'text',
selector: 'label'
}]
},
attrs: {
body: {
magnet: true,
width: 16,
height: 16,
x: -8,
y: -4,
stroke: 'red',
fill: 'gray'
},
label: {
text: v.columnName,
fill: 'black'
}
},
markup: [{
tagName: 'rect',
selector: 'body'
}]
};
rect.addPort(port);
});
rect.resize(150, data.length * 40);
graph.addCell(rect);
}
function resetAll(paper) {
paper.drawBackground({
color: 'white'
});
var elements = paper.model.getElements();
for (var i = 0, ii = elements.length; i < ii; i++) {
var currentElement = elements[i];
currentElement.attr('body/stroke', 'black');
}
var links = paper.model.getLinks();
for (var j = 0, jj = links.length; j < jj; j++) {
var currentLink = links[j];
currentLink.attr('line/stroke', 'black');
currentLink.label(0, {
attrs: {
body: {
stroke: 'black'
}
}
});
}
}
Any help would be appreciated. Thanks!
The default link created when you draw a link from a port is joint.dia.Link.
To change this you can use the defaultLink paper option, and configure the router you would like.
defaultLink documentation reference
const paper = new joint.dia.Paper({
el: document.getElementById('dbLookupCanvas'),
model: graph,
width: canvasContainer.width(),
height: 500,
gridSize: 10,
drawGrid: true,
cellViewNamespace: namespace,
validateConnection: function(cellViewS, magnetS, cellViewT, magnetT, end, linkView) {
return (magnetS !== magnetT);
},
snapLinks: {
radius: 20
},
defaultLink: () => new joint.shapes.standard.Link({
router: { name: 'manhattan' },
connector: { name: 'rounded' },
})
});
You could also provide several default options in the paper.
defaultLink: () => new joint.shapes.standard.Link(),
defaultRouter: { name: 'manhattan' },
defaultConnector: { name: 'rounded' }
I am trying to make a shooting game in matter.js but can't find a way to shoot bullets from the player's exact location and how to count the collision between player and bullet but not with the walls.
I want to fire a bullet from player1 and then on pressing D again it should fire another bullet from the player1's last position.
My Codepen of this game
let p1= Matter.Bodies.polygon(200, 200, 3, 40, {
chamfer: {
radius: [15,10,15]
},
isStatic: false,
inertia: Infinity,
friction: 0.9,
render: {
fillStyle: '#F9ED69'
},
mass:1
});
let p2 = Matter.Bodies.polygon(1100, 200, 3, 40, {
chamfer: {
radius: [15,10,15]
},
isStatic: false,
inertia: Infinity,
friction: 0.9,
render: {
fillStyle: '#11999E'
},
mass:1
});
let bullet1 = Matter.Bodies.polygon(400, 300, 3, 7, {
chamfer: {
radius: [4,2,4]
},
isStatic: false,
inertia: Infinity,
friction: 0.9,
render: {
fillStyle: '#F9ED69'
},
mass:0
});
const keyHandlers = {
KeyS: () => {
Matter.Body.applyForce(p1, {
x: p1.position.x,
y: p1.position.y
}, {x: 0.0, y: 0.001})
},
KeyW: () => {
Matter.Body.applyForce(p1, {
x: p1.position.x,
y: p1.position.y
}, {x: 0.0, y: -0.002})
},
KeyD:()=>{
Matter.Body.applyForce(bullet1, {
x: p1.position.x,
y: p1.position.y
}, {x: 0.001, y: 0.0})
},
};
const keysDown = new Set();
document.addEventListener("keydown", event => {
keysDown.add(event.code);
});
document.addEventListener("keyup", event => {
keysDown.delete(event.code);
});
Matter.Events.on(engine, "beforeUpdate", event => {
[...keysDown].forEach(k => {
keyHandlers[k]?.();
});
});
// on collision of a bullet with wall and other bodies remove the bullet from the world after some delay and add the score
let score1 = 0;
let score2 = 0;
let health
Matter.Events.on(engine, "collisionStart", event => {
for (let i = 0; i < event.pairs.length; i++) {
const pair = event.pairs[i];
if (pair.bodyA === bullet1 || pair.bodyB === bullet1) {
Matter.World.remove(engine.world, bullet1);
alert('1');
}
if (pair.bodyA === bullet2 || pair.bodyB === bullet2) {
Matter.World.remove(engine.world, bullet2);
alert('2');
}
}
score1++;
alert(`SCore1 is ${score1}`); // these alerts are just to confirm the collision
});
You're on the right track, but if you hardcode bullet1 and bullet2 you're stuck with just those two bullets. Even with a fixed number of bullets and re-using the bodies (good for performance but maybe premature optimization), I'd probably use an array to store these bullets, which is almost always the correct move after you catch yourself doing thing1, thing2...
Here's a proof of concept. I'm creating and destroying bullets here to keep the coding easier, but it'd be more performant to keep a pool of objects and re-use/re-position them.
I'm also using sets to keep track of the types of the bodies, but you might want to use labels. Most of the code here could go in many different directions, specific to your use case.
const engine = Matter.Engine.create();
engine.gravity.y = 0; // enable top-down
const map = {width: 300, height: 300};
const render = Matter.Render.create({
element: document.body,
engine,
options: {...map, wireframes: false},
});
const player = {
score: 0,
body: Matter.Bodies.polygon(
map.width / 2, map.height / 2, 3, 15, {
frictionAir: 0.06,
density: 0.9,
render: {fillStyle: "red"},
},
),
lastShot: Date.now(),
cooldown: 150,
fireForce: 0.1,
rotationAngVel: 0.03,
rotationAmt: 0.03,
rotateLeft() {
Matter.Body.rotate(this.body, -this.rotationAmt);
Matter.Body.setAngularVelocity(
this.body, -this.rotationAngVel
);
},
rotateRight() {
Matter.Body.rotate(this.body, this.rotationAmt);
Matter.Body.setAngularVelocity(
this.body, this.rotationAngVel
);
},
fire() {
if (Date.now() - this.lastShot < this.cooldown) {
return;
}
// move the bullet away from the player a bit
const {x: bx, y: by} = this.body.position;
const x = bx + (Math.cos(this.body.angle) * 10);
const y = by + (Math.sin(this.body.angle) * 10);
const bullet = Matter.Bodies.circle(
x, y, 4, {
frictionAir: 0.006,
density: 0.1,
render: {fillStyle: "yellow"},
},
);
bullets.add(bullet);
Matter.Composite.add(engine.world, bullet);
Matter.Body.applyForce(
bullet, this.body.position, {
x: Math.cos(this.body.angle) * this.fireForce,
y: Math.sin(this.body.angle) * this.fireForce,
},
);
this.lastShot = Date.now();
},
};
const bullets = new Set();
const makeEnemy = () => Matter.Bodies.polygon(
(Math.random() * (map.width - 40)) + 20,
(Math.random() * (map.height - 40)) + 20,
5, 6, {
render: {
fillStyle: "transparent",
strokeStyle: "white",
lineWidth: 1,
},
},
);
const enemies = new Set([...Array(100)].map(makeEnemy));
const walls = new Set([
Matter.Bodies.rectangle(
0, map.height / 2, 20, map.height, {isStatic: true}
),
Matter.Bodies.rectangle(
map.width / 2, 0, map.width, 20, {isStatic: true}
),
Matter.Bodies.rectangle(
map.width, map.height / 2, 20, map.height, {isStatic: true}
),
Matter.Bodies.rectangle(
map.width / 2, map.height, map.width, 20, {isStatic: true}
),
]);
Matter.Composite.add(engine.world, [
player.body, ...walls, ...enemies
]);
const keyHandlers = {
ArrowLeft: () => player.rotateLeft(),
ArrowRight: () => player.rotateRight(),
Space: () => player.fire(),
};
const validKeys = new Set(Object.keys(keyHandlers));
const keysDown = new Set();
document.addEventListener("keydown", e => {
if (validKeys.has(e.code)) {
e.preventDefault();
keysDown.add(e.code);
}
});
document.addEventListener("keyup", e => {
if (validKeys.has(e.code)) {
e.preventDefault();
keysDown.delete(e.code);
}
});
Matter.Events.on(engine, "beforeUpdate", event => {
[...keysDown].forEach(k => {
keyHandlers[k]?.();
});
if (enemies.size < 100 && Math.random() > 0.95) {
const enemy = makeEnemy();
enemies.add(enemy);
Matter.Composite.add(engine.world, enemy);
}
});
Matter.Events.on(engine, "collisionStart", event => {
for (const {bodyA, bodyB} of event.pairs) {
const [a, b] = [bodyA, bodyB].sort((a, b) =>
bullets.has(a) ? -1 : 1
);
if (bullets.has(a) && walls.has(b)) {
Matter.Composite.remove(engine.world, a);
bullets.delete(a);
}
else if (bullets.has(a) && enemies.has(b)) {
Matter.Composite.remove(engine.world, a);
Matter.Composite.remove(engine.world, b);
bullets.delete(a);
enemies.delete(b);
document.querySelector("span").textContent = ++player.score;
}
}
});
Matter.Render.run(render);
Matter.Runner.run(Matter.Runner.create(), engine);
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
<div>press left/right arrow keys to rotate and space to shoot</div>
<div>score: <span>0</span></div>
here's the code & output: https://stackblitz.com/edit/d3-angular-gridster2-working-axhc7u?file=src%2Fapp%2Fgrid%2Fgrid.component.html
GRID-HTML
<gridster [options]="options">
<gridster-item [item]="item" *ngFor="let item of dashboard">
</gridster-item>
</gridster>
GRID-TS
ngOnInit() {
#Input() editing: any;
this.options = {
gridType: GridType.Fit,
displayGrid: DisplayGrid.Always,
enableEmptyCellClick: false,
enableEmptyCellContextMenu: false,
enableEmptyCellDrop: false,
enableEmptyCellDrag: false,
enableOccupiedCellDrop: false,
emptyCellClickCallback: this.emptyCellClick.bind(this),
emptyCellContextMenuCallback: this.emptyCellClick.bind(this),
emptyCellDropCallback: this.emptyCellClick.bind(this),
emptyCellDragCallback: this.emptyCellClick.bind(this),
emptyCellDragMaxCols: 50,
emptyCellDragMaxRows: 50
};
this.dashboard = [
{ cols: 2, rows: 1, y: 0, x: 0 },
{ cols: 2, rows: 2, y: 0, x: 2 },
{ cols: 1, rows: 1, y: 0, x: 4 },
{ cols: 3, rows: 2, y: 1, x: 4 },
{ cols: 1, rows: 1, y: 4, x: 5 },
{ cols: 1, rows: 1, y: 2, x: 1 },
{ cols: 2, rows: 2, y: 5, x: 5 },
{ cols: 2, rows: 2, y: 3, x: 2 },
{ cols: 2, rows: 1, y: 2, x: 2 },
{ cols: 1, rows: 1, y: 3, x: 4 },
{ cols: 1, rows: 1, y: 0, x: 6 }
];
}
What I'm trying to do here is to enabled the enableEmptyCellDrag.
for example I clicked the button edit then the value of edit is true and then the value of enableEmptyCellDrag is true.
I already tried this:
ngOnChanges() {
///this.options['enableEmptyCellDrag'] = true // the enableEmptyCellDrag is undefined
///if (this.editing) {
/// this.options['enableEmptyCellDrag'] = true // the value of enableEmptyCellDrag is change to true, but when I try to drag from the empty cell it doesn't work
///}
}
If I understand correctly, you want to set this.options['enableEmptyCellDrag'] to the value of #Input() editing.
And you want to gridster2 (I must admit I don't know what this is) to recognise the change.
So you have 2 problems:
When you're in ngOnChanges, accessing your #Input() directly will give you the "old" value.
Usually, for Angular to detect changes in objects, you need to change the reference of the object.
So this is what your ngOnChanges should look like.
ngOnChanges(changes: SimpleChanges) {
if (changes.editing && changes.editing.currentValue) {
// Solve problem 1)
const newValueOfEditingInput = changes.editing.currentValue;
// Solve Problem 2) - Create a new reference for this.options, so that angular (grister2) can detect the change
this.options = {
...this.options,
enableEmptyCellDrag: newValueOfEditingInput
};
}
}
Of Course I haven't tested this but I hope it can help you
I found a more elegant solution IMHO.
The GridsterConfig object has an api.optionsChanged sub-object which is a function. If you run it that also tells Gridster the options changes without having to, essentially, re-instantiate the object (which probably just runs this function anyway). Seems safer and more elegant.
Thus, your on change can now look like this:
ngOnChanges(changes: SimpleChanges) {
if (changes.editing && changes.editing.currentValue) {
this.options.enableEmptyCellDrag = changes.editing.currentValue;
this.options.api.optionsChanged();
}
}
I also suggest creating a class like the following, which will prevent you from being forced to check if these options exist or not (the if statement is just checking to see if the GridsterConfig interface optional options are defined... so if you define them ahead of time there is no need to do that... not sure why Gridster made there existence optional... IMHO the options should always exist but can be set to null or a default).
export class DashboardOptions implements GridsterConfig{
gridType = GridType.Fit;
compactType = CompactType.None;
margin = 10;
outerMargin = false;
outerMarginTop = null;
outerMarginRight = null;
outerMarginBottom = null;
outerMarginLeft = null;
useTransformPositioning = true;
mobileBreakpoint = 720;
minCols = 1;
maxCols = 100;
minRows = 1;
maxRows = 100;
maxItemCols = 100;
minItemCols = 1;
maxItemRows = 100;
minItemRows = 1;
maxItemArea = 2500;
minItemArea = 1;
defaultItemCols = 1;
defaultItemRows = 1;
fixedColWidth = 105;
fixedRowHeight = 105;
keepFixedHeightInMobile = false;
keepFixedWidthInMobile = false;
scrollSensitivity = 10;
scrollSpeed = 20;
enableEmptyCellClick = false;
enableEmptyCellContextMenu = false;
enableEmptyCellDrop = false;
enableEmptyCellDrag = false;
enableOccupiedCellDrop = false;
emptyCellDragMaxCols = 50;
emptyCellDragMaxRows = 50;
ignoreMarginInRow = false;
public draggable = {
enabled: false,
delayStart: 200,
start: () => {},
stop: () => {}
};
public resizable = {
enabled: true,
delayStart: 200,
start: () => {},
stop: () => {}
};
swap = false;
pushItems = true;
disablePushOnDrag = false;
disablePushOnResize = false;
pushDirections = {north: true, east: true, south: true, west: true};
pushResizeItems = false;
displayGrid = DisplayGrid.Always;
disableWindowResize = false;
disableWarnings = false;
scrollToNewItems = false;
api = {
resize: () => {},
optionsChanged: () => {},
};
itemChangeCallback = (item: GridsterItem, itemComponent: GridsterItemComponentInterface) => {};
}
Then, your on change can now look like this:
ngOnChanges(changes: SimpleChanges) {
this.options.enableEmptyCellDrag = changes.editing.currentValue;
this.options.api.optionsChanged();
}
When I dragged the datapoint out of chart's ticks limit like from max x and y axis value, the canvas increase the limits too fast. How can I control this scaling speed? So that it increase with specific number defined in the config of chart.
Here is the js fiddle link.
https://jsfiddle.net/rz7pw6j0/67/
JS
(function () {
let chartInstance;
let chartElement;
function createChart(chartElement) {
const chartConfig = {
type: 'scatter',
data: {
datasets: [
{
data: [
{
x: 0,
y:0
}
],
fill: true,
showLine: true,
lineTension: 0
}
]
},
options: {
legend: {
display: false,
},
layout: {
padding: {
left: 50,
right: 50,
top: 0,
bottom: 0
}
},
title: {
display: false,
text: 'Chart.js Interactive Points',
},
scales: {
xAxes: [
{
type: 'linear',
display: true,
padding: 20,
paddingLeft: 10,
paddingRight: 10,
paddingTop: 10,
paddingBottom: 10,
scaleLabel: {
display: true,
labelString: 'Time',
},
ticks: {
suggestedMin: -5,
suggestedMax: 5,
stepValue: 1,
}
}
],
yAxes: [
{
type: 'linear',
display: true,
scaleLabel: {
display: true,
labelString: 'Weight'
},
paddingLeft: 10,
paddingRight: 10,
paddingTop: 10,
paddingBottom: 10,
ticks: {
suggestedMin: 0,
suggestedMax: 3,
stepValue: 1
},
}
]
},
responsive: true,
maintainAspectRatio: true,
tooltips: {
intersect: true,
}
}
};
chartInstance = new Chart(chartElement.getContext('2d'), chartConfig);
let element = null;
let scaleY = null;
let scaleX = null;
let datasetIndex = null;
let index = null;
let valueY = null;
let valueX = null;
function onGetElement() {
const event = d3.event.sourceEvent;
element = chartInstance.getElementAtEvent(event)[0];
if (!element) {
chartClickHandler(event);
return;
}
if (event.shiftKey) {
const tempDatasetIndex = element['_datasetIndex'];
const tempIndex = element['_index'];
chartInstance.data.datasets[tempDatasetIndex].data = chartInstance
.data.datasets[tempDatasetIndex].data.filter((v, i) => i !== tempIndex);
chartInstance.update();
return;
}
scaleY = element['_yScale'].id;
scaleX = element['_xScale'].id;
}
function onDragStart() {
const event = d3.event.sourceEvent;
datasetIndex = element['_datasetIndex'];
index = element['_index'];
valueY = chartInstance.scales[scaleY].getValueForPixel(event.offsetY);
valueX = chartInstance.scales[scaleX].getValueForPixel(event.offsetX);
chartInstance.data.datasets[datasetIndex].data[index] = {
x: valueX,
y: valueY
};
chartInstance.update(0);
}
function onDragEnd() {
if (
chartInstance.data.datasets[datasetIndex] &&
chartInstance.data.datasets[datasetIndex].data) {
chartInstance.data.datasets[datasetIndex].data.sort((a, b) => a.x - b.x > 0 ? 1 : -1);
chartInstance.update(0);
}
element = null;
scaleY = null;
scaleX = null;
datasetIndex = null;
index = null;
valueY = null;
valueX = null;
}
d3.select(chartInstance.chart.canvas).call(
d3.drag().container(chartInstance.chart.canvas)
.on('start', onGetElement)
.on('drag', onDragStart)
.on('end', onDragEnd)
);
}
function chartClickHandler (event) {
let scaleRef;
let valueX = 0;
let valueY = 0;
Object.keys(chartInstance.scales).forEach((scaleKey) => {
scaleRef = chartInstance.scales[scaleKey];
if (scaleRef.isHorizontal() && scaleKey === 'x-axis-1') {
valueX = scaleRef.getValueForPixel(event.offsetX);
} else if (scaleKey === 'y-axis-1') {
valueY = scaleRef.getValueForPixel(event.offsetY);
}
});
const newPoint = {
x: valueX,
y: valueY
};
const dataSeries = chartInstance.data.datasets[0].data;
for (let i = 0; i < dataSeries.length; i++) {
if (dataSeries[i].x === newPoint.x) {
dataSeries.y = newPoint.y;
chartInstance.update();
return;
}
}
let inserted = false;
for (let j = dataSeries.length - 1; j >= 0; j--) {
if (dataSeries[j].x > newPoint.x) {
dataSeries[j + 1] = dataSeries[j];
} else {
dataSeries[j + 1] = newPoint;
inserted = true;
break;
}
}
if (!inserted) {
dataSeries.push(newPoint);
}
chartInstance.update();
}
chartElement = document.getElementById("chart");
createChart(chartElement);
})();
HTML
<body>
<div>Chart Drag and Click Test</div>
<div class="wrapper">
<canvas id="chart"></canvas>
</div>
</body>
CSS
.wrapper {
display: "block";
}
I want to control the scaling speed.
If a dragStart event occurs beyond the scale limits, the increment should be a fixed value to avoid the issue you mentioned. Also, ticks.min and ticks.max should be set for the same purpose. Below is a sample jsfiddle and code (you can control speed by step).
https://jsfiddle.net/oLrk3fb2/
function onDragStart() {
const event = d3.event.sourceEvent;
const scales = chartInstance.scales;
const scaleInstanceY = scales[scaleY];
const scaleInstanceX = scales[scaleX];
const scalesOpts = chartInstance.options.scales;
const ticksOptsX = scalesOpts.xAxes[0].ticks;
const ticksOptsY = scalesOpts.yAxes[0].ticks;
const step = 1;
datasetIndex = element['_datasetIndex'];
index = element['_index'];
valueY = scaleInstanceY.getValueForPixel(event.offsetY);
valueX = scaleInstanceX.getValueForPixel(event.offsetX);
if (valueY < scaleInstanceY.min) {
ticksOptsY.min = valueY = scaleInstanceY.min - step;
}
if (valueY > scaleInstanceY.max) {
ticksOptsY.max = valueY = scaleInstanceY.max + step;
}
if (valueX < scaleInstanceX.min) {
ticksOptsX.min = valueX = scaleInstanceX.min - step;
}
if (valueX > scaleInstanceX.max) {
ticksOptsX.max = valueX = scaleInstanceX.max + step;
}
chartInstance.data.datasets[datasetIndex].data[index] = {
x: valueX,
y: valueY
}
chartInstance.update(0);
}