The bounty expires in 2 days. Answers to this question are eligible for a +50 reputation bounty.
Tom Rudge wants to draw more attention to this question.
I've created a D3 force directed graph in an Angular environment using typescript. I've followed this "brushing" example of a selectable force directed graph code for this example here. I've had to convert it into typescript and obviously fit it into my structure which is pretty standard D3. Brushing basically allows the selection of multiple nodes on a graph. This is triggered by keyup/keydown of the shift key.
Clicking on a node selects it and de-selects everything else.
Shift-clicking on a node toggles its selection status and leaves all
other nodes as they are.
Shift-dragging toggles the selection status of all nodes within the
selection area.
Dragging on a selected node drags all selected nodes.
Dragging an unselected node selects and drags it while de-selecting
everything else.
I've struggled to get this fully working.. here are my issues:
Shift toggles brushing on and off fine, but when I begin dragging when shift is active, when I keydown shift(let go) brushing remains on.
The brushing box does not appear until my second drag/selection attempt (as it remains on).
When I do brush/select nodes these don't get the selected class appended to them.
Here is a stackBlitz to my Angular D3 brushing attempt.
I'm relatively new to D3 so any assistance welcomed!
Brushing part of the code, for full version please see demo:
let gBrushHolder = svg.append('g');
let gBrush = null;
svg.style('cursor', 'grab');
let brushMode = false;
let brushing = false;
const brushstarted = () => {
// keep track of whether we're actively brushing so that we
// don't remove the brush on keyup in the middle of a selection
brushing = true;
node.each(function (d) {
d.previouslySelected = this.shiftKey && d.selected;
});
};
const brushed = () => {
if (!_d3.event.sourceEvent) return;
if (!_d3.event.selection) return;
console.log("during")
var extent = _d3.event.selection;
node.classed('selected', function (d) {
return (d.selected =
d.previouslySelected ^
(<any>(
(extent[0][0] <= d.x &&
d.x < extent[1][0] &&
extent[0][1] <= d.y &&
d.y < extent[1][1])
)));
});
};
const brushended = () => {
if (!_d3.event.sourceEvent) return;
if (!_d3.event.selection) return;
if (!gBrush) return;
gBrush.call(brush.move, null);
if (!brushMode) {
// the shift key has been release before we ended our brushing
gBrush.remove();
gBrush = null;
}
brushing = false;
console.log("end")
};
let brush = _d3
.brush()
.on('start', brushstarted)
.on('brush', brushed)
.on('end', brushended);
const keydown = () => {
this.shiftKey = _d3.event.shiftKey;
if (this.shiftKey) {
// if we already have a brush, don't do anything
if (gBrush) return;
brushMode = true;
if (!gBrush) {
gBrush = gBrushHolder.append('g');
gBrush.call(brush);
}
}
};
const keyup = () => {
this.shiftKey = false;
brushMode = false;
if (!gBrush) return;
if (!brushing) {
console.log("NOT BRUSHING")
// only remove the brush if we're not actively brushing
// otherwise it'll be removed when the brushing ends
gBrush.remove();
gBrush = null;
}
};
_d3.select('body').on('keydown', keydown);
_d3.select('body').on('keyup', keyup);
Related
I set up a grid 16x16 which a white background now i want the divs (grid items) to change color every time i press and hold my mouse and move over them, and to stop changing color when i stop the mousedown event, like in paint. in my script:
let gridContainer = document.getElementById('grid-container');
let gridArray = [];
for(let i = 0; i < 256; i++){
let div = document.createElement('div');
div.setAttribute('class', 'grid-item');
gridContainer.append(div);
gridArray[i] = div;
}
gridContainer.addEventListener("mousedown", draw)
gridContainer.addEventListener("mouseup",stopDraw)
function draw(){
gridArray.forEach(item => item.addEventListener("mouseover", () => {
(item.classList.add('hover'))
}));
}
function stopDraw(){
gridArray.forEach(item => item.removeEventListener("mouseover", () => {
(item.classList.add('hover'))
}));
}
The hover class is used to change the background color to blue.
I tried multiple other approaches but i always end up at the same place, the draw function not working unless i click which runs the function then it doesnt stop, it keeps running even after i leave the mouse.
Im using vanilla JS, I am still learning the basics.
Here is my code for better understanding my problem: https://codepen.io/ahmedmk11/pen/VwrmyGW
EDIT: I just realized that i didnt explain my problem in a clear way, so the draw function works properly, but my problem is that it doesnt stop working, i need in to only work when im pressing and holding only, like a pen.
Using event listeners is a good approach, to keep with this I have changed your code to use an object. Using an object allows the state of the mouse to be temporarily stored. Using the "canDraw()" method we can read from the object to determine if the mouse event is still occurring. It is still occurring if the key "mousedown" is still present.
The events now just add or remove the key from "mouseEvent" object.
let gridContainer = document.getElementById('grid-container');
let gridArray = [];
let mouseEvent = {};
for(let i = 0; i < 256; i++){
let div = document.createElement('div');
div.setAttribute('class', 'grid-item');
div.addEventListener("mouseover", draw);
gridContainer.append(div);
gridArray[i] = div;
}
gridContainer.addEventListener("mousedown", () => mouseEvent.mouseDown = true)
gridContainer.addEventListener("mouseup", () => delete mouseEvent.mouseDown)
function canDraw() {
return mouseEvent.mouseDown;
}
function draw(e){
if (canDraw()) {
e.fromElement.classList.add('hover');
}
}
Looks like you say that the div's should be paint when the mouse button is pressed, but you never really say that the divs should not be painted when the mouse button is not pressed, this happen in this specifc part:
function draw(){
gridArray.forEach(item => item.addEventListener("mouseover", () => {
(item.classList.add('hover'))
}));
}
function stopDraw(){
// right here!
gridArray.forEach(item => item.removeEventListener("mouseover", () => {
(item.classList.add('hover'))
}));
}
You could change your code to make something like "hey, when my mouse is up and over the divs, I don't want to insert the class that paint them", like this:
function draw(){
gridArray.forEach(item => item.addEventListener("mouseover", () => {
item.classList.add('hover');
}));
}
function stopDraw(){
// now we have a event that removes the class
gridArray.forEach(item => item.addEventListener("mouseover", () => {
item.classList.remove('hover');
}));
}
I also did some refactor (changed let by const when necessary, corrected the layout and moved the use of the functions draw and stopDraw after his declarations), I tried make less changes as I could, the final solution is:
const gridContainer = document.getElementById('grid-container');
let gridArray = [];
for(let i = 0; i < 256; i += 1){
const div = document.createElement('div');
div.setAttribute('class', 'grid-item');
gridContainer.append(div);
gridArray.push(div);
}
function draw(){
gridArray.forEach(item => item.addEventListener("mouseover", () => {
item.classList.add('hover');
}));
}
function stopDraw(){
gridArray.forEach(item => item.addEventListener("mouseover", () => {
item.classList.remove('hover');
}));
}
gridContainer.addEventListener("mousedown", draw);
gridContainer.addEventListener("mouseup", stopDraw);
I am a beginner in D3 and JavaScript development, i have succeed to generate a multi-line graph ( generated with an JS object) with interactivity. Every control work except that zoom alter line form.
I have tried to change data quantity and i don't know which part is guilty of that behavior. It appears after a given time, the line doesn't change and stay the same.
These pictures show this latter effect :
capture with the right line:
capture with altered line:
If anyone have any idea for resolve that.
[edit] i make another pass on my code and the problem seems linked to path update. i add these blocks of code if anyone see something wrong.
function line_gen(num){
graph['lineFunction'+num] = d3.line()
.defined(function(d) {return !isNaN(d[graph.dict[num]]) })
.x(function(d) { return graph.xScaleTemp(d[graph.dict[0]]);})
.y(function(d) { return graph.yScaleTemp(d[(graph.dict[num])])})
.curve(d3.curveMonotoneX);
}
function updateData() {
var t;
function data_join(num){
if(!(graph.oldFdata.length)){
graph.dataGroup.select(".data-line"+graph.opt.name+num)
.transition()
// .duration(graph.spd_transition!=-1?graph.spd_transition:0)
.attr("d", graph['lineFunction'+num]((graph.Fdata)));
}else{
let update = graph.dataGroup.select(".data-line"+graph.opt.name+num)
.data(graph.Fdata);
update.enter()
.append("path")
.attr("d",graph['lineFunction'+num]);
update.exit().remove();
// graph.dataGroup.select(".data-line"+graph.opt.name+num)
// .transition()
// .duration(graph.spd_transition!=-1?graph.spd_transition:0)
// .attrTween('d',function(d){
// var previous =d3.select(this).attr('d');
// var current =graph['lineFunction'+num]((graph.Fdata));
// return d3.interpolatePath(previous, current);
// });
}
}
for (var i = 1; i < graph["keys_list"].length; i++) {
if(graph[("lineFunction" + i)]==null){
indic_gen(i);
indicGrp(i);
}
data_join(i);
}
for (var i = 1; i < (Object.keys(graph.Fdata[0]).length); i++) {
/* update_tooltip(i); */
set_tooltip(i);
}
}
I have resolved my problem by rerendering it once more. Not the best solution but it's works.
I'm working with svg through Raphael.js and I need to collapse SVG group and show it on click.
I've tried to use 'visibility' attriburte of SVG group and change it on click, but it is ok with hiding but it's not appear back;
const GroupCollapse = function(paper) {
const $svg = paper.canvas; // #mysvg is SVG doc
const g = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
$svg.appendChild(g);
this.paper = paper;
this.node = g;
this.attrs = {};
this._ = {};
this.add = function (el) {
g.appendChild(el.node);
};
};
GroupCollapse.prototype = Raphael.el;
Raphael.fn.groupCollapse = function(... args) {
return new GroupCollapse(this, ... args)
};
This is my realisation of custom group.
Then I create it like this:
let gc = paper.groupCollapse();
I make it 'hidden' And I have a function to make it visible that didn't work.
function showCollapsed() {
gc.visibility = "visible";
}
I want to have SVG group of Raphael circles that collapsing to one circle and unfolds on click to this circle. Could you help me and show the best way to do that. Thank you!
I'm building a D3 treemap visualization, and I've run into a spot of trouble.
I've been able to populate my initial treemap, but I'd like to introduce modifications to it via click interactions. In order to do that, I need to recompute a new layout (and then do some things with it later) every click, using a subset of my previous (currently displayed) data to do that.
I've written a sum function that works as I want it to, and populates the value property of each Node in my new_root object. This is, as I understand it, the sole prerequisite for running d3.treemap on my new_root Here's a snippet of the code:
let treemap = d3.treemap()
.tile(d3.treemapResquarify)
.size([800, 400])
.round(true)
.paddingInner(1);
// later...
function object_is_direct_child(root, obj) {
return (root.children.filter(child => (child.id === obj.name)).length > 0);
}
// ...much later...
// Create and bind the click event.
let click = function(d) {
if (d.data.children.length > 0) {
// Use the hover ID to get the underlying name attr (e.g. "Noise-hover" -> "Noise")
let name = this.getAttribute("id").slice(0,-6);
// Select the new root node.
let new_root = root.children.find(c => c.id === name);
// Sum.
new_root = new_root.sum(node => ((object_is_direct_child(new_root, node)) ? node.n : 0));
// Here is the problem area. This doesn't work as expected.
treemap(new_root);
}
}
hover_rects.on("click", click);
This should populate the computed, liad-out x0, x1, y0, y1 values on new_node and all of its sub-children. Instead, new_node and all of its children are NaN.
I've uploaded what I have so far as a Gist, accessible here.
I was able to debug this using the follow MWE script, run in Node:
'use strict';
// Load data.
const tree = require('../threshold-tree');
const d3 = require('d3');
const assert = require('assert');
// Load in the data file as it would be loaded in the browser.
const fs = require('fs');
const csvString = fs.readFileSync('data/complaint_types.csv').toString();
const raw_data = d3.csvParse(csvString);
let tr = new tree.ThresholdTree(raw_data);
let hr = tr.as_hierarchy();
let root = d3.stratify().id(d => d.name).parentId(d => d.parent)(hr);
function object_is_direct_child(root, obj) {
return (root.children.filter(child => (child.id === obj.name)).length > 0);
}
// Now the sum function itself.
function nodal_summer(node) {
return ((object_is_direct_child(root, node)) ? node.n : 0);
}
root = root.sum(nodal_summer);
// Create our treemap layout factory function.
let treemap = d3.treemap()
.tile(d3.treemapResquarify)
.size([800, 400])
.round(true)
.paddingInner(1);
// Apply our treemap function to our data.
debugger;
treemap(root);
// Again...this is where it fails!
let new_root = root.children[0];
new_root.parent = null;
new_root = new_root.sum(node => ((object_is_direct_child(new_root, node)) ? node.n : 0));
debugger;
new_root = treemap(new_root);
console.log(treemap(new_root));
The culprit is the positionNode callback inside d3:
function positionNode(node) {
var p = paddingStack[node.depth],
...
paddingStack is initialized as a length-1 list, but node.depth is 1, so p is initialized as NaN, which gets propagated from there.
Two working fixes are:
Using node.copy().
new_root.eachBefore(function(node) { node.depth--; }).
I want to implement functionality on the svgElements that can be dragged with javascript how could I do this...
I have implemented this with mouse up
When mouse up occurs, I save the x and y position, object id, object type (circle, rect, etc.)
Can any one tell...is this good way to implement?
If you're asking how to implement undo/redo functionality in general, it's fairly simple: you have an array of actions and a counter. You push new elements onto the array when actions occur and step backwards when people hit undo.
Very basic implementation:
var history = {
stack : [],
counter : -1,
add : function(item){
this.stack[++this.counter] = item;
this.doSomethingWith(item);
// delete anything forward of the counter
this.stack.splice(this.counter+1);
},
undo : function(){
this.doSomethingWith(this.stack[--this.counter]);
},
redo : function(){
this.doSomethingWith(this.stack[++this.counter]);
},
doSomethingWith : function(item){
// show item
}
};
Note that there should be basic error checking to see that counter doesn't go beyond bounds and that you may want to pass 'undo' info into doSomethingWith in the case of an undo, but all that is app specific.
cwolves describes a good structure for the undo/redo functionality.
You are correct, that during mouse-up, you'll want to store the history, but you'll also want to store the original location of the object(s) being manipulated during mouse-down so you'll have have it when you undo the move.
If you end up taking it a step further, and allowing scaling, then you'll want to store the complete original transform e.g. "translate(10,10) scale(1.2,1.2) rotate(90)", for history, but also so that you'll have a baseline to apply the drag-scaling action to.
I found a better structure without counter, index shift or limit handling problems.
Simply 2 stack for "done" and "reverted" action that are balancing.
var history = function() {
this.done = this.reverted = [];
var self = this;
this.add = function(item) {
self.done.push(item);
// delete anything forward
self.reverted = [];
};
this.undo = function() {
var item = self.done.pop();
if (item) {
self.reverted.push(item);
}
return item;
};
this.redo = function() {
var item = self.reverted.pop();
if (item) {
self.done.push(item);
}
return item;
};
};
There are some issues with the above codes.
I attempted to use those and found out that I had to hit undo twice initially before it started going down the array.
So say I have done[0] done[1] done[2].
done[2] was just saved into the array. If I hit undo, it returns that. You dont want that. It is replacing what is already there. But hitting undo again, THEN you get your previous code.
Mine, as I have a drag and drop editor with different modes of edits. Drag and Drop Elements. Edit Elements HTML/Pictures. Sorting elements.
$("#neoContentContainer") contains all editor html.
And you can call editor_add on clicks, mousedowns ect... seeing it is a function you can easily call.
function editor_add(){
undo.push($("#neoContentContainer").html());
update_buttons();
}
function editor_undo(){
var item = undo.pop();
// prevent undo/redo from undoing to the same HTML currently shown.
if(item == $("#neoContentContainer").html()){
redo.push(item);
item = undo.pop();
}
if(item){
redo.push(item);
$("#neoContentContainer").html(item);
}
update_buttons();
}
function editor_redo(){
var item = redo.pop();
if(item == $("#neoContentContainer").html()){
undo.push(item);
item = redo.pop();
}
if(item){
undo.push(item);
$("#neoContentContainer").html(item);
}
update_buttons();
}
function update_buttons(){
if(undo.length == 0){
$('button[data-id="undo"]').attr('disabled',true);
} else {
$('button[data-id="undo"]').attr('disabled',false);
}
if(redo.length == 0){
$('button[data-id="redo"]').attr('disabled',true);
} else {
$('button[data-id="redo"]').attr('disabled',false);
}
}
https://jsfiddle.net/s1L6vv5y/
Not perfect, but get the jist of it. (Still wondering when we can get ONE LINE LINE BREAKS!!!! DEVS AT STACKOVERFLOW!! :)
So when my page loads, I run: editor_add();
Because when they do something, it needs to undo to something!
Now every time they drop something, sort things I run editor_add(); Took me all day, now realizing how simple it was this works very well for me.
So with this one, which is good....
var history = function() {
this.done = this.reverted = [];
var self = this;
this.add = function(item) {
self.done.push(item);
};
this.undo = function() {
if(done.length >= 3){
var undo_item = self.done.pop();
self.reverted.push(undo_item);
}
var item = self.done.pop();
if (item) {
self.reverted.push(item);
}
return item;
};
this.redo = function() {
if(reverted.length >= 3){
var revert_item = self.reverted.pop();
self.done.push(revert_item);
}
var item = self.reverted.pop();
if (item) {
self.done.push(item);
}
return item;
};
};
You do not want to clear the redos until you ran through the array.
(Guess logging in before editing helps!)