exit().remove() doesn't remove obsolete elements - d3.js

http://jsfiddle.net/brunoperel/z6qfttmr/
I'm trying to create elements on-the-fly, depending on the link which has been clicked on :
d3.select('#processes').selectAll('.processLaunch').data(helperProcessesData).enter()
.append('a')
.classed('processLaunch', true)
.attr('href', 'javascript:void(0)')
.text(function(d) { return d.text; })
.on('click', function(d) {
var currentProcess=d.name;
var filteredData = helperStepsData.filter(function(d) { return d.process === currentProcess; });
var helperStepsForProcess = d3.select('div#helperContainer').selectAll('.helperStep')
.data(filteredData);
console.log('Filtered data : '+JSON.stringify(filteredData));
console.log('Linked data : '+JSON.stringify(helperStepsForProcess.data()));
helperStepsForProcess.enter()
.append('div').text(function(d) { return 'Step '+d.step+' : '+d.title; });
helperStepsForProcess.exit().remove();
});
There are (at least !) two things that I don't understand here :
Each time I click on a link, related texts are added to the page, but obsolete ones are not removed even though I called .exit().remove() on the selection. Why ?
When I do a console.log of the data that is about to be bound to the selection's elements, it returns an array of objects, which is fine. But when, in the line after this, I retrieve the data which has been bound to the elements, I get an array of undefined objects. Why don't I get an array of objects as well ?

I think you just forgot to add the respective class on the steps:
helperStepsForProcess.enter()
.append('div')
.attr('class', 'helperStep');
helperStepsForProcess.text(function(d) {
return 'Step '+d.step+' : '+d.title; });
Adding this in your fiddle return the expected result.
I hope this helps!
Update: https://jsfiddle.net/chroth/z6qfttmr/2/

Related

Show/hide D3 data points dynamically

I'm using Svelte and D3 to create a scatter-point graph.
I need it to dynamically show and hide data points depending on the length of an array of years.
If the array is empty, I want to show all of the points.
If the array is populated, I want to show only the data points which occur on years which are present in the array.
The array is coming from a parent component, I've logged it with a reactive statement and the array is correctly updated in the child component as it needs to be.
My component is currently set up as follows:
onMount(() => {
drawGraph();
plotPoints();
});
$: filterYears.length,
destroyPoints();
plotPoints();
function drawGraph() {
logic to render the graph with axis and titles relevant to the data input.
this works fine.
}
function plotPoints() {
svg.selectAll("dot")
.data(dataArray2)
.enter()
.append("circle")
.attr("r", 3)
.attr("cx", function(d) { return x(d.year); })
.attr("cy", function(d) { return yRight(d.value); })
.style("stroke", "#f8ff2a")
.style('stroke-width', '1px')
.style("fill", "#f8ff2a55")
.style("display", d => (filterYears.length > 0 && !filterYears.includes(d.year)) ? "none" : "inline")
.on('mouseover', function (event, datum) {
d3.select(this).transition()
.duration('100')
.attr("r", 7);
div2.transition()
.duration(100)
.style("opacity", 1)
div2.html((datum.value).toFixed(2))
.style("left", (event.offsetX + 25) + "px")
.style("top", (event.offsetY - 10) + "px")
})
.on('mouseout', function (event, datum) {
d3.select(this).transition()
.duration('200')
.attr("r", 3);
div2.transition()
.duration('200')
.style("opacity", 0);
});
console.log('plot point');
}
I have 2 sets of data being rendered, the above is one of them.
function destroyPoints() {
console.log('destroying points');
svg.selectAll("dot").remove()
}
All variables are correctly scoped and all functions have access to what they need.
The reactive statement seems to be called initially on component render and was triggering destroyPoints() which was trying to access svg before initialisation, causing an error, I have gotten around this with a really hacky setTimeout. If somebody could shed some light on why this happens and a better workaround I would love to hear it.
The real issue is that on change of filterYears, the points are not updating in the graph.
Could somebody give me a hand here?
Thanks!
Regular component code, including reactive statements, is executed before anything it mounted at which point DOM elements will not exist yet. It is common to just add a condition to reactive statements that depend on DOM bindings:
$: if (svg) {
filterYears;
destroyPoints();
plotPoints();
}
There seems to be another error here which is obfuscated by deceptive formatting, the correct format of the original code is:
$: filterYears.length,
destroyPoints();
plotPoints();
plotPoints() is not part of the reactive statement due to the ;.
(Also, in case you modify filterYears using functions like push: An assignment has to be used to trigger reactivity.)
I am new to d3.js, but wouldnt a pre-refinement of the inputdata be more straight forward?
svg.selectAll("dot")
.remove()
.data(filteredDataArray.any() ? filteredDataArray : dataArray2)
.enter()
.append("circle")
or maybe even more outside. just filter the dataArray

dc.js table -- Select row(s), highlight onclick and apply filter for Crossfilter [duplicate]

I love the DC.JS library and have been trying to create a clickable aggregated table in DC.js with partial success. I want to highlight the rows on click event(multiple selections allowed) similar to the row chart or an ordinal bar chart in dc js. Like a row chart, when multiple selections are made, multiple table rows should be highlighted.
I am not able to select the row that I have clicked on, rather, my css selects the first row irrespective of which row I click. I tried to use 'this' keyword to select the current row that was clicked but to no avail.
Here's the js fiddle: https://jsfiddle.net/yashnigam/kvt9xnbs/83/
Here's my code for the click event that makes the css selection:
marketTable.on("renderlet", function(chart){
chart.selectAll('tr.dc-table-row').on('click',function(d){
if(filterKeys.includes(d.key)){
chart.select('tr.dc-table-row').datum(d.key).style('background-color','gray');
}
})
});
Kindly share a way to highlight rows of data table on click, the same way it works on a row chart.
#Hassan has the right idea. I would suggest selecting the trs rather than the tds, and instead of changing the classes on click (which wouldn't survive a redraw), apply the classes also during the pretransition event.
So:
tr.dc-table-row.sel-rows {
background-color: lightblue;
}
marketTable.on('pretransition', function (table) {
table.selectAll('td.dc-table-column')
.on('click', /* ... */)
table.selectAll('tr.dc-table-row')
.classed('sel-rows', d => filterKeys.indexOf(d.key) !== -1)
});
We apply the class based on whether the row's key is in the array. Straightforward and simple!
Fork of your fiddle.
using built-in filters
#vbernal pointed out that the list doesn't get reset when you click the reset link. To better integrate this, we can hook into the built-in filters that the table inherits from the base mixin (but doesn't ordinarily use):
marketTable.on('pretransition', function (table) {
table.selectAll('td.dc-table-column')
.on('click',function(d){
let filters = table.filters().slice();
if(filters.indexOf(d.key)===-1)
filters.push(d.key);
else
filters = filters.filter(k => k != d.key);
table.replaceFilter([filters]);
dc.redrawAll();
});
let filters = table.filters();
table.selectAll('tr.dc-table-row')
.classed('sel-rows', d => filters.indexOf(d.key) !== -1);
});
Instead of setting dimension.filter() ourselves, we fetch the existing table.filters(), toggle as needed, and then set the filters using
table.replaceFilter([filters])
(Note the extra brackets.)
When the reset link is clicked, we reset the filter on the table rather than the crossfilter dimension. (It's always better to manipulate filters through the chart, because charts are not able to read the selection state from the crossfilter dimension.)
$('#resetTable').on('click', function() {
marketTable.filter(null);
dc.redrawAll();
});
New version of fiddle.
In your onclick event, add (toggle) a class similar .sel-rows to clicked item (instead of change back color of it). Now in your css add this:
.sel-rows td{
background-color: gray;
}
Background color for table rows not work in some browsers.
As I said before, the changes that you (#Gordon) indicated worked, when I click the button, the table is redefined without any kind of color.
However, the problem was inversed, now the numbers remain the same.
I mixed it with the code you created and the solution I found was as follows:
marketTable.on('pretransition', function(table) {
table.selectAll('td.dc-table-column')
.on('click', function(d) {
let filters = table.filters().slice();
if (filters.indexOf(d.key) === -1)
filters.push(d.key);
else
filters = filters.filter(k => k != d.key);
if (filters.length === 0)
marketDim.filter(null);
else
marketDim.filterFunction(function(d) {
return filters.indexOf(d) !== -1;
})
table.replaceFilter([filters]);
dc.redrawAll();
});
let filters = table.filters();
table.selectAll('tr.dc-table-row')
.classed('sel-rows', d => filters.indexOf(d.key) !== -1);
});
$('#reset').on('click', function() {
marketTable.filter(null);
marketDim.filter(null)
vendorDim.filter(null)
CategoryDim.filter(null)
dc.redrawAll();
});
$('#resetTable').on('click', function() {
marketTable.filter(null);
marketDim.filter(null)
dc.redrawAll();
});
I don't know if it's the most elegant way to do this, I'm still a beginner in D3, DC and Crossfilter

How to use d3.js to select an svg element and modify its attr?

I'm a bit new to d3.js and I guess my question is not about any detailed code or data.
here it is:
function selectNodesWithProperty(container, key, value){
var nodes = d3.selectAll(container).select(function(d, i) {
if (value == d[key]) {
console.log(this); // this works correctly, the element is the one I want.
this.attr('fill', 'red')
}
});
}
selectNodesWithProperty(".selector", "id", "Blue");
I have two questions:
Is my way the proper way to select the element by data?
Is there any way that I can use d3's attr for the element?
Thanks,
The most obvious solution is to use selection.filter() to get a subset of the selection containing all the nodes for which the filter is true. You can then use .attr() on the selection returned by .filter() to apply changes to the attribute values of the new subset only.
function selectNodesWithProperty(container, key, value){
d3.selectAll(container)
.filter(d => value === d[key])
.attr('fill', 'red')
}

dc.js group.top(5) not working in chart

i am trying to display top 5 country based on revenue
i tried feeding in chart its not working
.group(countryGrp.top(10))
but when i tried console.log(countryGrp.top(10)) i can see the values though
var country = data.dimension(function(d){return d.PName});
var countryGrp = lead.group().reduceSum(function(d) {
return d.Amount;
});
var leadBarChart = dc.barChart("#country-chart")
leadBarChart.width(500).height(450).transitionDuration(750)
.margins({
top : 20,
right : 10,
bottom : 80,
left : 50
})
.dimension(country)
.group(countryGrp.top(10))
.ordinalColors([ "#1F77B4" ]).elasticY(true).centerBar(true)
.title(function(d) {
return d.Amount;
}).gap(6).xUnits(function() {
return 2;
})
.x(d3.scale.linear())
.renderHorizontalGridLines(true).xAxis().ticks(5)
.tickFormat(d3.format("d"));
i am getting this error uncaught TypeError: undefined is not a function can any one help me out here. Thanks in advance
So group.top(N) doesn't return a group object; it returns an array. You need to supply a group for the group argument.
Unfortunately the bar chart does not currently support capping but what you can do is prefilter the data as explained here:
https://github.com/dc-js/dc.js/wiki/FAQ#filter-the-data-before-its-charted
Basically you will create a "fake group" which has an .all() function which returns only the items you want.
I am planning to fix .data() so that it works for bar charts:
https://github.com/dc-js/dc.js/issues/584
(and it would also be nice to support capping) but for now I think you are stuck with the "fake group" workaround.
Please comment here if you can't get this to work, or add you example to the wiki linked above if you do get it to work!
You'll want to use something like the following:
function getTops(source_group) {
return {
all: function () {
return source_group.top(5);
}
};
}
var fakeGroup = getTops(groupToFindTopsFor);
You can then use .group(fakeGroup) to properly chart your data.

How to get data of parent node in d3.js

I am doing nesting in D3 and in a nested element, I need to reach data object on its parent.
Right now I am doing
d3.select(this).node().parentNode.__data__;
Is there a better way?
d3.select(this).node() is the same as just this in the context of a function passed to a D3 selection. You could rework it like this d3.select(this.parentNode).datum() and get the correct value back without having to use the ugly double-underscore property.
The other method I'm familiar with is to use .each() on the parent, and then deal with children within the closure:
d3.selectAll('.parent').each(function(parentDatum) {
d3.select(this).selectAll('.child').each(function(childDatum) {
// do stuff with parentDatum and childDatum here
});
});
I found this post when I had exactly the same issue. My solution, was to change the selection to pass on the data that I will need in the child, which I think might be a valid approach if you can live with the data redundancy in the node.
[
{
title: "Google",
link: "www.google.com",
languagePath : "en,fr,it"
},
...
]
To help explain, I've used this in the context of a table, with two columns. The first column has the title and the second column has an a for each of the accept-language items.
So I did a sub selections of the split of languagePath and for each enter call, I would create a a with the text being the language.
So at this point I also need the link, so that the a elements look like:
EN
FR
But the link is not part of the data passed to this child, so when I did the selection instead of doing:
var langLinks = tableRow
.append("td")
.selectAll("span")
.data(function(d) {
return d.languagePath.split(",")
})
I did
var langLinks = tableRow
.append("td")
.selectAll("span")
.data(function(d) {
return d.languagePath.split(",").map(function(item) {
return {
lang : item,
link : d.link
})
})
And so, when I'm processing data on the enter() of this selection I do have the parent's data.
Hope this is a valid alternative for some of you, it did help me.

Resources