How can I make dragged text element in the right position? - javascript

Currently I'm working on a small project. At this stage I have several word cloud SVGs with the help of d3.js word cloud.
Now I want to make the SVG's 'text' element draggable with JQuery UI so that user can drag the text element and then drop it on certain area (say a droppable div).
But after several attempts, it seems that JQuery-UI is not that compatible with SVG. I've compared several similar questions and tried on my own code, but none of them worked. The most up-voted answer of this question is very close to what I want. The text element is draggable, but when I click the element, it doesn't appear in the right position and it would appear elsewhere. And also the element cannot be dragged out of its SVG container.
Here's my JS code:
var fill = d3.scale.category20();
function draw(words) {
d3.select('#wordCloud').append("svg")
.attr("width", 600)
.attr("height", 600)
.append("g")
.attr("transform", "translate(300,300)")
.selectAll("text")
.data(words)
.enter().append("text")
.style("font-size", function(d) { return d.size + "px"; })
.style("font-family", "Impact")
.style("fill", function(d, i) { return fill(i); })
.attr("text-anchor", "middle")
.attr("transform", function(d) {
return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
})
.text(function(d) { return d.text; });
}
wordCloudData.map((model) => {
let modelName = model.modelName;
let wordsArray = model.specArray;
var $title = $('<h4></h4>').text(modelName);
$('#wordCloud').append($title);
d3.layout.cloud().size([600, 600])
.words(wordsArray.map(function(d) {
return {text: d, size: 10 + Math.random() * 90};
}))
// .rotate(function() { return ~~(Math.random() * 2) * 90; })
.font("Impact")
.fontSize(function(d) { return d.size * 0.6; })
.on("end", draw)
.start();
});
$(document).ready(function() {
let wordCloudCount = $('#wordCloud svg').length;
let svgArray = $('#wordCloud svg').clone();
let titleArray = $('#wordCloud h4').clone();
for (let i = 0; i < wordCloudCount; i++) {
let $wordCloudDiv = $('<div></div>').attr('class', 'wordCloud');
let $title = titleArray[i];
let $svg = svgArray[i];
$wordCloudDiv.append($title);
$wordCloudDiv.append($svg);
$('#wordCloudFinal').append($wordCloudDiv);
}
$('#wordCloud').hide();
$('text')
.draggable()
.bind('mousedown', function(event, ui){
$(event.target.parentElement).append( event.target );
})
.bind('drag', function(event, ui){
event.target.setAttribute('x', ui.position.left);
event.target.setAttribute('y', ui.position.top);
});
});
Here is my HTML code:
<div class="container">
<div id="wordCloud"></div>
<div id="wordCloudFinal"></div>
</div>

Related

line break in d3 tag Cloud

I am using it to generate a heavy amount of words in my application and I grab data from a json file like below:
var a = [];
for (var i=0; i < a.length; i++){
a.push(a[i].word);
}
And this gives us an array.
a = ["gsad","sagsa","gsag","sagas","gsag","gsagas","yhff","gag"];
I have it display on the screen correctly but since the row is too long it got out from the border and I'd like to give it a link break instead of change the SVG size, How may i do this?
UPDATE:
The code below is how i insert my codes:
var PositiveArr = ["gsad","sagsa","gsag","sagas","gsag","gsagas","yhff","gag"]; //consider the NegativeArr,NeutralArr have the similar contents
var fill = d3.scale.category20();
d3.layout.cloud().size([600, 300])
.words([NegativeArr,NeutralArr,PositiveArr].map(function(d) {
return {text: d, size: 10 + Math.random() * 50};
}))
.rotate(function() { return ~~(Math.random() * 2) * 90; })
.font("Impact")
.fontSize(function(d) { return d.size; })
.on("end", draw)
.start();
function draw(words) {
d3.select("#pre-theme").append("svg")
.attr("width", 600)
.attr("height", 300)
.append("g")
.attr("transform", "translate(300,150)")
.selectAll("text")
.data(words)
.enter().append("text")
.style("font-size", function(d) { return d.size + "px"; })
.style("font-family", "Impact, Arial")
.style("fill", function(d, i) { return fill(i); })
.attr("text-anchor", "middle")
.attr("transform", function(d) {
return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
})
.text(function(d) { return d.text; });
}
SOLUTION:
I have found myself a solution, i just merge all my arrays into one using
var allResult = PersonsArr.concat(PlacesArr,PatternsArr,ProductsArr,CompaniesArr);
and insert the to the .map like
.words(entityResult.map(function(d) {
return {text: d, size: 10 + Math.random() * 50};
}))
SOLUTION: I have found myself a solution, i just merge all my arrays into one using
var allResult = PersonsArr.concat(PlacesArr,PatternsArr,ProductsArr,CompaniesArr);
and insert the to the .map like
.words(entityResult.map(function(d) {
return {text: d, size: 10 + Math.random() * 50};
}))

d3js enter().append after exit().remove()

I have a word cloud that can be filtered by date range and sentiment. Sometimes there will be more data sometimes there will be less. When I remove data, update the dom and then add data the elements that were removed won't come back. Using d3js version 3.4.13
var width = 600, height = 200;
var words = ["Hello", "world", "Wonderful"];
//inserting text
var wcwords = d3.select("#wordcloud")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.selectAll("text")
.data(words)
.enter()
.append("text");
wcwords
.attr("transform", function(d,i) {
return "translate(" + [5, 20*i] + ")";
})
.text(function(d) { return d; });
//changing data and updating dom (in this change there are less items)
wcwords.data(words.slice(0,2)).exit().remove();
//changing data and updating dom (in this change there are more items)
wcwords.data(words.concat(["No"])).enter().append('text')
.attr("transform", function(d,i) {
return "translate(" + [5, 20*i] + ")";
})
.text(function(d) { return d; });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg id='wordcloud'></svg>
EDIT
Original code did not work put updated my post with code that does what I need. New, deleted and updated items animate differently. I can change existing items, delete items and return items again.
The trick was to use the right selection parent.selectAll(children) and pass the update object (object returned by .data(newData))
Here is the "working" code, hope I did it correctly:
var width = 600;
var height = 200;
var words = ["Hello", "world", "Wonderful"];
var when=1000;
var step=1;
//this function sets the data and passes the update object
// to exit, update and enter
function change(data){
var update = d3.select('#wccontainer')
.selectAll('text')
.data(data);
exitWords(update);
updateWords(update);
enterWords(update);
}
//existing items move to the right
function updateWords(update){
update
//this is an existing item, no need for append
.text(function(d) { return d; })
.transition()
.duration(when-100)
.attr("transform", function(d,i) {
this.left=this.left+25;
return "translate(" + [this.left, 20*i] + ")";
})
.style('opacity',1);
}
//new items fade in
function enterWords(update){
update
.enter()
.append("text")
.attr("transform", function(d,i) {
this.left=0;
return "translate(" + [5, 20*i] + ")";
})
.text(function(d) { return d; })
.style('opacity',0)
.transition()
.duration(when-100)
.attr("transform", function(d,i) {
return "translate(" + [5, 20*i] + ")";
})
.style('opacity',1);
}
//removed words fade out
function exitWords(update){
var removeItems = update
.exit()
removeItems
.transition()
.duration(when-800)
.style('opacity',0)
.each('end',function(){
removeItems.remove();
});
}
function later(when,fn,parms){
setTimeout(function(){
fn.apply(null,parms);
},when);
}
//create the g container and set svg width/height
d3.select("#wordcloud")
.attr("width", width)
.attr("height", height)
.append("g")
.attr('id','wccontainer')
.attr("transform", "translate(" + width / 2
+ "," + height / 2 + ")")
//set the text labels
change(words);
//in 1000ms (value of when) set the text lables with changed data
later(when,change,[words.slice(0,2)]);
//in 2000ms set the text lables with changed data
later(when*++step,change,[["CHANGED"]
.concat(words.slice(1,2))
.concat(["ONE","TWO","THREE","FOUR"])]);
//in 3000ms set the text lables with the original values
later(when*++step,change,[words]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg id='wordcloud'></svg>
I'll first explain what's happening first...
var width = 600, height = 200;
var words = ["Hello", "world", "Wonderful"];
var wcwords = d3.select("#wordcloud")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.selectAll("text")
.data(words);
.enter()
.append("text");
wcwords is now an enter selection which happens to have the same structure as the update collection because all elements are new. Because selectAll is used, the selection is nested under the g node: this is the parent object of the selection.
wcwords
.attr("transform", function(d,i) {
return "translate(" + [5, 20*i] + ")";
})
.text(function(d) { return d; });
wcwords.data(words.slice(0,2)).exit().remove();
All this is doing is use the data method as a selector to remove one DOM element. The new selection (with only two elements) is not referenced in-scope and wcwords is unchanged, so in fact the DOM is now out of synch with the selection.
wcwords.data(words.concat(["No"])).enter().append('text')
.attr("transform", function(d,i) {
return "translate(" + [5, 20*i] + ")";
})
.text(function(d) { return d; });
A new selection is created and again, the wcwords object is unchanged. The node structure of wcwords (not the DOM structure) is compared to the new data structure and since there are 3 nodes in the former and 4 in the latter and because data preserves indexing, the enter selection will consist of a single group of 4 elements with the first three elements null and the final element being the datum object for the new node. A new text node is then added to the end of the parent node of wcwords (the g) by the append statement. Since the third element is not in the enetr selection, it is not re-inserted.
The basic principles are that
data does not change the object it is called on, it returns a reference to a new selection (which is ignored here)
the data statement compares the selection structure to the data structure when constructing the enter, update and exit selections. It is not compared to the DOM structure.
I'm guessing about the order you expect since you haven't shared that but maybe you were going for something like the following.
var width = 70, height = 100;
var words = ["Hello", "world", "Wonderful"];
var outputLog = OutputLog("#output-log");
var transitionLog = OutputLog("#transition-log");
var wcwords = d3.select("#wordcloud").style("display", "inline-block")
.attr("width", width)
.attr("height", height)
.append("g")
.style("font-size", "10px")
.attr("transform", "translate(" + 10 + "," + 20 + ")")
.selectAll("text")
.data(words)
.enter()
.append("text")
.style("opacity", 0);
wcwords
.text(function(d) { return d; })
.attr("transform", function(d,i) {
return "translate(" + [5, 20*i] + ")";
})
.call(step, 0, "in")
.call(log, "wcwords.data(words) enter");
// bind a new data set to the selection and return the update selection
var wcwords = wcwords.data(words.slice(0,2))
.call(log, "wcwords.data(words.slice(0,2)) update");
// merge the enter selection into the update selection and update the DOM
wcwords.enter()
.append("text")
.style("opacity", 0);
wcwords.exit().transition().call(step, 1, "out").remove()
.call(log, "exit");
// modify the selection by rebinding the original data
// but with an extra element concatenated
// and return the update selection
var wcwords = wcwords.data(words.concat(["No"]))
.call(log, "wcwords.data(words.concat(['No'])) update");
// update the DOM and merge the exit selection into the update selection
wcwords.enter().append('text')
.attr("transform", function(d,i) {
return "translate(" + [5, 20*i] + ")";
})
.text(function(d) { return d; })
.style("opacity", 0)
.call(step, 2, "in")
.call(log, "enter");
function datum(n){
return n ? d3.select(n).datum() : "";
}
function step (selection, s, type) {
var id = Date.now(),
opacity = {in: 1, out: 0},
t = 1000,
w = 0, b = "";
selection.each(function(d){w = Math.max(w, d.length) });
b = new Array(w+4).join('_')
this.transition(Date.now()).delay(s * t).duration(t)
.each("start." + id, function(d, i, j){
var n = this, node = d3.select(n),
DOM_node = d3.select(selection[0].parentNode)
.selectAll(this.nodeName).filter(function(d){return node.datum() === d});
DOM_node = DOM_node.length ? DOM_node[0][0] : null;
transitionLog.writeLine(["start ", (""+id).slice(-4), s, type, (d+b).slice(0,w), style(this, "opacity") || "null", DOM_node === n].join("\t"))
})
.each("interrupt." + id, function(d){
console.log(["\tinterrupt ", id, type, style(this, "opacity"), s].join("\t"))
})
.each("end." + id, function(d){
var n = this, node = d3.select(n),
DOM_node = d3.select(selection[0].parentNode)
.selectAll(this.nodeName).filter(function(d){return node.datum() === d});
DOM_node = DOM_node.length ? DOM_node[0][0] : null;
transitionLog.writeLine(["end", (""+id).slice(-4), s, type, (d+b).slice(0,w), style(this, "opacity") || "null", DOM_node === n].join("\t"))
})
.style("opacity", opacity[type]);
function style(n, a){return d3.select(n).style(a)}
}
function log(selection, title){
outputLog.writeLine(title);
outputLog.writeLine(this[0].map(datum), 1);
}
function OutputLog(selector) {
var outputLog = d3.select(selector)
.style({
"display" : "inline-block",
"font-size" : "10px",
"margin-left": "10px",
padding : "1em",
"white-space": "pre",
"background" : "#fd9801",
});
outputLog.writeLine = (function() {
var s = "";
return function(l, indent) {
this.text((s += ((indent ? " " : "") + l + "\n")));
}
})();
return outputLog
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="UTF-8"></script>
<svg id='wordcloud'></svg>
<div id="output-log"></div>
<div id="transition-log"></div>

Triggering two separate events on mouseover in D3

I have a D3 bar chart with the associated data points displayed as text on top of each bar. I want to display the text only on mouseover and also make the bar have a different fill color. So, essentially, on mouseover, the bar has to be styled to have a different fill color and the text opacity should go to 1 (from '0').
I am having trouble effecting two separate events on mouseover. I have given an index_value attribute to both elements in order to use d3.select(this).attr(index_value). But my mouseover function does not work. I have no idea why. Here's my relevant code section.
The bar chart
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr('data-value', function(d){return d[region]})
.attr("x", function(d) { return x(d.year); })
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d[region]); })
.attr("height", function(d) { return height - y(d[region]); })
.attr("fill", color)
.attr("index_year", function(d, i) { return "index-" + d.year; })
.attr("class", function(d){return "bar " + "bar-index-" + d.year;})
.attr("color_value", color)
.on('mouseover', synchronizedMouseOver)
.on("mouseout", synchronizedMouseOut);
The text overlay
svg.selectAll(".bartext")
.data(data)
.enter()
.append("text")
.attr("text-anchor", "middle")
.attr("x", function(d,i) {
return x(d.year)+x.rangeBand()/2;
})
.attr("y", function(d,i) {
return height - (height - y(d[region])) - yTextPadding;
})
.text(function(d){
return d3.format(prefix)(d3.round(d[region]));
})
.attr("index_year", function(d, i) { return "index-" + d.year; })
.attr("class", function(d){return "bartext " + "label-index-" + d.year;})
.on("mouseover", synchronizedMouseOver)
.on("mouseout", synchronizedMouseOut);
And the mouseover function
var synchronizedMouseOver = function() {
var bar = d3.select(this);
console.log(bar);
var indexValue = bar.attr("index_year");
var barSelector = "." + "bar " + "bar-" + indexValue;
var selectedBar = d3.selectAll(barSelector);
selectedBar.style("fill", "#f7fcb9");
var labelSelector = "." + "bartext " + "label-" + indexValue;
var selectedLabel = d3.selectAll(labelSelector);
selectedLabel.style("opacity", "1");
};
This can be achieved by simplifying your listeners. You don't need to add listeners to both rects and text. Just add them to the rects. Here are the simplified listeners:
function synchronizedMouseOver(d) {
var bar = d3.select(this)
.style("fill","red");
var text = d3.select(".label-index-" + d.year)
.style("opacity","1");
};
function synchronizedMouseOut(d) {
var bar = d3.select(this)
.style("fill",color);
var text = d3.select(".label-index-" + d.year)
.style("opacity","0");
};
Your two friends here are this and d, the DOM element for the rect and its data node, respectively.
Here is a FIDDLE with the behavior that you desire.

change the word cloud when the onchange functions is triggered

I want to change the word cloud when the onchange functions is triggered.
in my current scriupt bellow, when I change the selection from the drop down list the other image shows up on my chrome windows, I think this might have something to do with this like d3.select("body").append("svg")
How can I show one wordcloud at a time and not append to the current windows?
I tried d3.select("body") = "svg" instead but didn't work, I tried to remove empty clear the screen body element and then shows up the word cloud didn't work too.
Any hint will be highly appreciated!
Thanks!
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="js/d3.js"></script>
<script src="js/d3.layout.cloud.js"></script>
<script>
function displayResult()
{
//location.reload();
//var e = document.getElementsByTagName('svg');
//e.removeChild(document.body.svg);
var client_name=document.getElementById("client_name");
var client_nameSelected = client_name.options[client_name.selectedIndex].value;
//alert(client_nameSelected);
var fill = d3.scale.category20();
//var ClientName = {"Hello":0.10 , "world":0.20, "normally cool!":0.25, "you":0.15, "want":0.60, "more":0.45, "words":0.90 };
var data = { 'name1':{"Hello":0.10 , "world":0.20, "normally cool!":0.65, "you":0.15, "want":0.60, "more":0.85, "words":0.90 }, 'name2':{"Hello":0.10 , "world":0.20, "normally cool!":0.25, "you you":0.15, "Hug":0.99, "more feedback":0.45, "words":0.90 }};
var ClientName = data[client_nameSelected];
var keysdic = Object.keys(ClientName);
//document.write(keysdic);
d3.layout.cloud().size([600, 600])
.words( [].concat(keysdic)
.map(function(d) {
var wordsize = 10 + ClientName[d] * 40 ;
result = {text: d, size: wordsize };
return result;
}))
.padding(5)
.rotate(function() { return ~~(Math.random() * 2) * 90; })
.font("Impact")
.fontSize(function(d) { return d.size; })
.on("end", draw)
.start();
function draw(words) {
d3.select("body").append("svg")
.attr("padding", 60)
.attr("width", 600)
.attr("height", 600)
.append("g")
.attr("transform", "translate(150,150)")
.selectAll("text")
.data(words)
.enter().append("text")
.style("font-size", function(d) { return d.size + "px"; })
.style("font-family", "Impact")
.style("fill", function(d, i) { return fill(i); })
.attr("text-anchor", "middle")
.attr("transform", function(d) {
return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
})
.text(function(d) { return d.text; });
}
}
</script>
<body>
Client Name List :
<select name = 'client_name' id = "client_name" onchange="displayResult();" >
<option value='name1'>name1</option>
<option value='name2'>name2</option>
</select><br />
<body>

How do I hide the text labels in d3 when the nodes are too small?

I am creating a zoomable sunburst similar to this example. The problem is that I've got a lot of data in my sunburst, so the text labels are mushed together and hardly readable. Therefore I would like to hide the labels when they are too small, like in this d3.partition.layout example. How do I go about implementing this feature?
I just got this working by adding the following:
var kx = width / root.dx, ky = height / 1;
Then in the text declaration section do the following:
var text = g.append("text")
.attr("transform", function(d) { return "rotate(" + computeTextRotation(d) + ")"; })
.attr("x", function(d) { return y(d.y); })
.attr("dx", "6") // margin
.attr("dy", ".35em") // vertical-align
.attr("opacity", function(d) { return d.dx * ky > 10 ? 1 : 0; })
.text(function(d) { return d.name; });
The key part of the above is this line:
.attr("opacity", function(d) { return d.dx * ky > 10 ? 1 : 0; })
This sets the opacity to 0 if it is not big enough. Then in the click function you need to do the same thing, as so:
function click(d) {
// fade out all text elements
text.transition().attr("opacity", 0);
kx = (d.y ? width - 40 : width) / (1 - d.y);
ky = height / d.dx;
path.transition()
.duration(750)
.attrTween("d", arcTween(d))
.each("end", function(e, i) {
// check if the animated element's data e lies within the visible angle span given in d
if (e.x >= d.x && e.x < (d.x + d.dx)) {
// get a selection of the associated text element
var arcText = d3.select(this.parentNode).select("text");
// fade in the text element and recalculate positions
arcText.transition().duration(750)
.attr("opacity", 1)
.text(function(d) { return d.name; })
.attr("opacity", function(d) { return e.dx * ky > 10 ? 1 : 0; })
.attr("transform", function() { return "rotate(" + computeTextRotation(e) + ")" })
.attr("x", function(d) { return y(d.y); });
}
});
}
To implement this in general, you will need to draw the text element, get its actual size using getBBox() and depending on that size, either show it or not. The code would look something like this.
svg.append("text")
.style("opacity", function() {
var box = this.getBBox();
if(box.width <= available.width && box.height <= available.height) {
return 1; // fits, show the text
} else {
return 0; // does not fit, make transparent
}
});
You can of course also remove the text element completely, but that would require a separate pass.

Categories