How can I prevent nodes, that are set with automove to get moved by layouter? - cytoscape.js

I have a cytoscape graph with these extensions:
Automove
Expand-Collapse
In my graph I have normal nodes with smaller nodes beside them (here in yellow). These smaller nodes are set with automove to move along with their linked node.
This happens with this function:
private addCircle(nodeId: string, circleText: string): void {
var parentNode = this.graph.getElementById(nodeId)
if (parentNode.data('isCircle') || parentNode.data('circleId')) return
var px = parentNode.position('x') + 10
var py = parentNode.position('y') - 10
var circleId = (this.graph.nodes().size() + 1).toString()
parentNode.data('circleId', circleId)
this.graph
.add({
group: 'nodes',
data: { id: circleId, name: circleText },
position: { x: px, y: py },
classes: 'XorGroup_' + circleText,
})
.unselectify()
.ungrabify()
this.graph.automove({
nodesMatching: this.graph.getElementById(circleId),
reposition: 'drag',
dragWith: parentNode,
})
}
If I move the "parent" its works totally fine, but if I use the collapse function of the "Expand-Collapse" extension, the nodes are seperated from their "parent" and get placed in the upper left corner.
This collapse event is used on totally different nodes.
I tried to use this function:
cy.nodes().on("expandcollapse.aftercollapse", function(event) { var node = this; ... })
Unfortunatelly it fires before the layout is calculated by the "Expand-Collapse" extension.
Does anybody have an idea how to manage, that the smaller nodes are ALWAYS on the same relative position to their "parent"?

Related

D3 linkHorizontal() update on mouse position

I’m trying to implement a drag and drop functionality with a connection line. The connection line has a starting point ([x, y]), which is gathered when the mouse is clicked and a target point ([x, y]) which follows the mouse position and is continuously updated while dragging the element.
The project uses Vue.JS with VUEX store and for the connection line D3.js (linkHorizontal method https://bl.ocks.org/shivasj/b3fb218a556bc15e36ae3152d1c7ec25).
In the main component I have a div where the SVG is inserted:
<div id="svg_lines"></div>
In the main.js File I watch the current mouse position (targetPos), get the start position from the VUEX store (sourcePos) and pass it on to connectTheDots(sourcePos, targetPos).
new Vue({
router,
store,
render: (h) => h(App),
async created() {
window.document.addEventListener('dragover', (e) => {
e = e || window.event;
var dragX = e.pageX, dragY = e.pageY;
store.commit('setConnMousePos', {"x": dragX, "y": dragY});
let sourcePos = this.$store.getters.getConnStartPos;
let targetPos = this.$store.getters.getConnMousePos;
// draw the SVG line
connectTheDots(sourcePos, targetPos);
}, false)
},
}).$mount('#app');
The connectTheDots() function receives sourcePos and targetPos as arguments and should update the target position of D3 linkHorizontal. Here is what I have:
function connectTheDots(sourcePos, targetPos) {
const offset = 2;
const shapeCoords = [{
source: [sourcePos.y + offset, sourcePos.x + offset],
target: [targetPos.y + offset, targetPos.x + offset],
}];
let svg = d3.select('#svg_lines')
.append('svg')
.attr('class', 'test_svgs');
let link = d3.linkHorizontal()
.source((d) => [d.source[1], d.source[0]])
.target((d) => [d.target[1], d.target[0]]);
function render() {
let path = svg.selectAll('path').data(shapeCoords)
path.attr('d', function (d) {
return link(d) + 'Z'
})
path.enter().append('svg:path').attr('d', function (d) {
return link(d) + 'Z'
})
path.exit().remove()
}
render();
}
I stole/modified the code from this post How to update an svg path with d3.js, but can’t get it to work properly.
Instead of updating the path, the function just keeps adding SVGs. See attached images:
Web app: Multiple SVGs are drawn
Console: Multiple SVGs are added to element
What am I missing here?
#BKalra helped me solve this.
This line keeps appending new SVGs:
let svg = d3.select('#svg_lines') .append('svg') .attr('class', 'test_svgs');
So I removed it from the connectTheDots() function.
Here is my solution:
In the main component I added an SVG with a path:
<div id="svg_line_wrapper">
<svg class="svg_line_style">
<path
d="M574,520C574,520,574,520,574,520Z"
></path>
</svg>
</div>
In the connectTheDots() function I don't need to append anymore, I just grap the SVG and update its path:
function connectTheDots(sourcePos, targetPos) {
const offset = 2;
const data = [{
source: [sourcePos.y + offset, sourcePos.x + offset],
target: [targetPos.y + offset, targetPos.x + offset],
}];
let link = d3.linkHorizontal()
.source((d) => [d.source[1], d.source[0]])
.target((d) => [d.target[1], d.target[0]]);
d3.select('#svg_line_wrapper')
.selectAll('path')
.data(data)
.join('path')
.attr('d', link)
.classed('link', true)
}

html2pdf fit image to pdf

I finally got my html2pdf to work showing my web page just how I want it in the pdf(Any other size was not showing right so I kept adjusting the format size until it all fit properly), and the end result is exactly what I want it to look like... EXCEPT even though my aspect ratio is correct for a landscape, it is still using a very large image and the pdf is not standard letter size (Or a4 for that matter), it is the size I set. This makes for a larger pdf than necessary and does not print well unless we adjust it for the printer. I basically want this exact image just converted to a a4 or letter size to make a smaller pdf. If I don't use the size I set though things are cut off.
Anyway to take this pdf that is generated and resize to be an a4 size(Still fitting the image on it). Everything I try is not working, and I feel like I am missing something simple.
const el = document.getElementById("test);
var opt = {
margin: [10, 10, 10, 10],
filename: label,
image: { type: "jpeg", quality: 0.98 },
//pagebreak: { mode: ["avoid-all", "css"], after: ".newPage" },
pagebreak: {
mode: ["css"],
avoid: ["tr"],
// mode: ["legacy"],
after: ".newPage",
before: ".newPrior"
},
/*pagebreak: {
before: ".newPage",
avoid: ["h2", "tr", "h3", "h4", ".field"]
},*/
html2canvas: {
scale: 2,
logging: true,
dpi: 192,
letterRendering: true
},
jsPDF: {
unit: "mm",
format: [463, 600],
orientation: "landscape"
}
};
var doc = html2pdf()
.from(el)
.set(opt)
.toContainer()
.toCanvas()
.toImg()
.toPdf()
.save()
I have been struggling with this a lot as well. In the end I was able to resolve the issue for me. What did the trick for me was setting the width-property in html2canvas. My application has a fixed width, and setting the width of html2canvas to the width of my application, scaled the PDF to fit on an A4 paper.
html2canvas: { width: element_width},
Try adding the above option to see if it works. Try to find out the width of your print area in pixels and replace element_width with that width.
For completeness: I am using Plotly Dash to create web user interfaces. On my interface I include a button that when clicked generates a PDF report of my dashboard. Below I added the code that I used for this, in case anybody is looking for a Dash solution. To get this working in Dash, download html2pdf.bundlemin.js and copy it to the assets/ folder. The PDF file will be downloaded to the browsers default downloads folder (it might give a download prompt, however that wasn't how it worked for me).
from dash import html, clientside_callback
import dash_bootstrap_components as dbc
# Define your Dash app in the regular way
# In the layout define a component that will trigger the download of the
# PDF report. In this example a button will be responsible.
app.layout = html.Div(
id='main_container',
children = [
dbc.Button(
id='button_download_report',
children='Download PDF report',
className='me-1')
])
# Clientside callbacks allow you to directly insert Javascript code in your
# dashboards. There are also other ways, like including your own js files
# in the assets/ directory.
clientside_callback(
'''
function (button_clicked) {
if (button_clicked > 0) {
// Get the element that you want to print. In this example the
// whole dashboard is printed
var element = document.getElementById("main_container")
// create a date-time string to use for the filename
const d = new Date();
var month = (d.getMonth() + 1).toString()
if (month.length == 1) {
month = "0" + month
}
let text = d.getFullYear().toString() + month + d.getDay() + '-' + d.getHours() + d.getMinutes();
// Set the options to be used when printing the PDF
var main_container_width = element.style.width;
var opt = {
margin: 10,
filename: text + '_my-dashboard.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 3, width: main_container_width, dpi: 300 },
jsPDF: { unit: 'mm', format: 'A4', orientation: 'p' },
// Set pagebreaks if you like. It didn't work out well for me.
// pagebreak: { mode: ['avoid-all'] }
};
// Execute the save command.
html2pdf().from(element).set(opt).save();
}
}
''',
Output(component_id='button_download_report', component_property='n_clicks'),
Input(component_id='button_download_report', component_property='n_clicks')
)

Forge Data Visualization not working on Revit rooms [ITA]

I followed the tutorials from the Forge Data Visualization extension documentation: https://forge.autodesk.com/en/docs/dataviz/v1/developers_guide/quickstart/ on a Revit file. I used the generateMasterViews option to translate the model and I can see the Rooms on the viewer, however I have problems coloring the surfaces of the floors: it seems that the ModelStructureInfo has no rooms.
The result of the ModelStructureInfo on the viewer.model is:
t {model: d, rooms: null}
Here is my code, I added the ITA localized versions of Rooms as 3rd parameter ("Locali"):
const dataVizExtn = await this.viewer.loadExtension("Autodesk.DataVisualization");
// Model Structure Info
let viewerDocument = this.viewer.model.getDocumentNode().getDocument();
const aecModelData = await viewerDocument.downloadAecModelData();
let levelsExt;
if (aecModelData) {
levelsExt = await viewer.loadExtension("Autodesk.AEC.LevelsExtension", {
doNotCreateUI: true
});
}
// get FloorInfo
const floorData = levelsExt.floorSelector.floorData;
const floor = floorData[2];
levelsExt.floorSelector.selectFloor(floor.index, true);
const model = this.viewer.model;
const structureInfo = new Autodesk.DataVisualization.Core.ModelStructureInfo(model);
let levelRoomsMap = await structureInfo.getLevelRoomsMap();
let rooms = levelRoomsMap.getRoomsOnLevel("2 - P2", false);
// Generates `SurfaceShadingData` after assigning each device to a room (Rooms--> Locali).
const shadingData = await structureInfo.generateSurfaceShadingData(devices, undefined, "Locali");
// Use the resulting shading data to generate heatmap from.
await dataVizExtn.setupSurfaceShading(model, shadingData, {
type: "PlanarHeatmap",
placePosition: "min",
usingSlicing: true,
});
// Register a few color stops for sensor values in range [0.0, 1.0]
const sensorType = "Temperature";
const sensorColors = [0x0000ff, 0x00ff00, 0xffff00, 0xff0000];
dataVizExtn.registerSurfaceShadingColors(sensorType, sensorColors);
// Function that provides a [0,1] value for the planar heatmap
function getSensorValue(surfaceShadingPoint, sensorType, pointData) {
const { x, y } = pointData;
const sensorValue = computeSensorValue(x, y);
return clamp(sensorValue, 0.0, 1.0);
}
const sensorType = "Temperature";
dataVizExtn.renderSurfaceShading(floor.name, sensorType, getSensorValue);
How can I solve this issue? Is there something else to do when using a different localization?
Here is a snapshot of what I get from the console:
Which viewer version you're using? There was an issue causing ModelStructureInfo cannot produce the correct LevelRoomsMap, but it gets fixed now. Please use v7.43.0 and try again. Here is the snapshot of my test:
BTW, if you see t {model: d, rooms: null} while constructing the ModelStructureInfo, it's alright, since the room data will be produced after you called ModelStructureInfo#getLevelRoomsMap or ModelStructureInfo#getRoomList.

Vue.Js draw lines between components problem

I am using SVGs to draw lines between components that are related in my app. Currently I am grabbing those elements and getting their position info with document.getElementById() and then using getClientBoundingRect.
This generally works, but there is occasional render wonkiness.
Is there a better way to do this? Perhaps an already existing library that works with VueJs?
Changing the class puts a CSS filter on the shape because you have
.isSelected {
filter: brightness(50%);
}
Now the W3C Filter Effects spec says
The application of the ‘filter’ property to an element formatted with the CSS box model establishes a new stacking context the same way that CSS ‘opacity’ does, and all the element's descendants are rendered together as a group with the filter effect applied to the group as a whole.
So browsers are doing the correct thing per that specification. The new stacking context puts the shape in front of the line.
See also this wontfixed Chromium bug
The problem ended up being because of document.getElementById and z-order
I ended up changing my timer to get the element by ref, and iterate through the children of my component to find it.
Here is the code:
drawLines: function () {
let children = this.$children
let lines = this.lines
lines.splice(0, lines.length)
let scheduleContainer = this.$refs.scheduleContainer
let scheduleContainerRect = scheduleContainer.getBoundingClientRect();
for (let i = 0; i < children.length; i++) {
let child = children[i]
if (child.$props.assignment) {
if (child.$props.assignment.assignmentRequestId != "00000000-0000-0000-0000-000000000000") {
for (let ii = 0; ii < children.length; ii++) {
let child2 = children[ii]
if (child2.$props.assignmentRequest) {
if (child2.$props.assignmentRequest.id == child.$props.assignment.assignmentRequestId) {
let assignmentRect = child.$refs.theContainer.getBoundingClientRect()
let requestRect = child2.$refs.theContainer.getBoundingClientRect()
let x1 = ((assignmentRect.left - scheduleContainerRect.left) + 12.5) + 'px'
let y1 = ((assignmentRect.top - scheduleContainerRect.top) + 12.5) + 'px'
let x2 = ((requestRect.left - scheduleContainerRect.left) + 12.5) + 'px'
let y2 = ((requestRect.top - scheduleContainerRect.top) + 12.5) + 'px'
let line = { 'x1': x1, 'y1': y1, 'x2': x2, 'y2': y2 }
lines.push(line)
}
}
}
}
}
}
},

Adding extra information above the node in cytoscape.js

Using cytoscape.js to draw a graph. I need to add circle above the node at top-right position. After drawing the graph, is there any API wherein we can get positions of nodes.
Also, I have used the code as follows:
elements : {
nodes : [ {
data : {
id : '1',
name : 'A'
}
}
]
}
var pos = cy.nodes("#1").position();
'#1' is the ID of the node in the data attribute. However, we are not able to add the node/circle at that exact position.
If you want to get something like:
then this code adds the circle to a node knowing it's id:
function addCircle(nodeId, circleText){
var parentNode = cy.$('#' + nodeId);
if (parentNode.data('isCircle') || parentNode.data('circleId'))
return;
parentNode.lock();
var px = parentNode.position('x') + 10;
var py = parentNode.position('y') - 10;
var circleId = (cy.nodes().size() + 1).toString();
parentNode.data('circleId', circleId);
cy.add({
group: 'nodes',
data: { weight: 75, id: circleId, name: circleText, isCircle: true },
position: { x: px, y: py },
locked: true
}).css({
'background-color': 'yellow',
'shape': 'ellipse',
'background-opacity': 0.5
}).unselectify();
}
// ...
addCircle('1', 'Bubble A');
but it must be called after the node's positions are known, when the layout settles down.
The locking is there to prevent that node and it's circle get apart.
jsFiddle demo: http://jsfiddle.net/xmojmr/wvznb9pf/
Using compound node which would keep the node and it's circle together would be probably better, once the support for positioning child nodes is implemented.
Disclaimer: I'm cytoscape.js newbie, use at your own risk