I am approaching the problem of appending a complex (two or more glyphs) symbol to some data. With D3.js, it seems that the right way to do so is appending the glyphs (in this example, circles) to groups (g) joined to data:
datum <=> g <=> (circle, circle)
Both the groups and the appended glyphs have properties depending on data, so that for example g is translated by .start and the position of the second circle is given by .end for each datum.
In order to achieve this, I wrote the following code (see the notebook), which however does not work as expected
function updatea (){
a[0].start += 10*Math.sin(t);
a[0].end += 10*Math.cos(t);
console.log(a[0].end - a[0].start);
t += 0.1;
var miao = svg.selectAll('g.aaa').data(a).join('g')
.classed('aaa',true)
.attr('transform',(d, i)=>('translate('+d.start+','+(i+1)*50+')'));
miao.append('circle').attr('r', 10).attr('fill','red');
miao.append('circle').attr('r', 10).attr('cx', d=>d.end).attr('fill','red');
}
The expected result would be as follows: two circles oscillate around their initial position, with a phase of period/4 between them. Instead, the second circle (to which I assigned an attribute cx, in order to give the position relative to the first one) is not refreshed, but instead all of its positions are drawn one after the other, oscillating with the translation in the attribute "transform".
I think that the problem is appending circles every time I update data; but how should I then append them? I tried something like this, following https://bost.ocks.org/mike/nest/:
var groups = svg.selectAll('g').data(a).enter().append('g').attr('transform',(d, i)=>('translate('+d.start+','+(i+1)*50+')'));
var circle_start = groups.selectAll('circle').data((d)=>{return d.start;}).enter().append('circle').attr('cx', d=>d).attr('cy', d=>100).attr('r', 10);
var circle_end = groups.selectAll('circle').data((d)=>{return d.end;}).enter().append('circle').attr('cx', d=>d).attr('cy', d=>100).attr('r', 10);
but it gives no output. Doing a bit of debug, for example assigning another dataset to one of the two circles, apparently the problem lies in .data(d)=>{return d.end;}).
Problem
On the pragmatic side, your update function doesn't work as expected because each update you append two new circles to each g entered or updated with selectAll().join():
function updatea (){
// join g's
var miao = svg.selectAll('g.aaa').data(a).join('g')
.classed('aaa',true)
.attr('transform',(d, i)=>('translate('+d.start+','+(i+1)*50+')'));
// append two circles to each g entered or updated:
miao.append('circle').attr('r', 10).attr('fill','red');
miao.append('circle').attr('r', 10).attr('cx', d=>d.end).attr('fill','red');
}
If you inspect the page you'll see two new circles appended each update. You're never updating the circles that are already on the page, just the translate on the g.
On the more theoretical side, you are unclear if your approach is most appropriate for binding data to complex symbols.
Solution
In proposing a solution let's consider the theoretical side first.
D3 was designed with data binding in mind. Normally in D3 you want to bind one datum per element (or one datum per element with a single child in each level of nesting). Where data is grouped and each datum is represented with multiple children, we would often see a second, nested, selectAll().data().join() statement.
However, if your visualization uses a symbol that is always comprised of the same set of child elements, then we don't need to do a nested join. In fact we do not need to in order to stay true to the data binding philosophy in D3: we'll bind one datum to one symbol (symbol in the data visualization sense).
This is the approach I'll propose here.
Rationale
This approach has advantages depending on situation, for example, there may be cases where the symbol's elements share parts of the datum (as in your case where d.start and d.end are both used to set the position of one of the sub-components) - splitting the datum into a new data array would be unnecessarily cumbersome. Changes in the symbol's representation/behavior/etc may require different parts of the datum as well, in which case it doesn't make sense to split the parent datum up.
Also, another reason why the proposed approach is attractive is that if you break the datum into smaller sub-components by using a nested selection:
svg.selectAll("g").data(data).enter().append("g")
.selectAll("circle").data(function(d) { return [d.start,d.end]; })
...
Or by flattening your array:
svg.selectAll("g").data([data[0].start,data[0].end,data[1].start,...])
...
It isn't as clear what child datum corresponds to what property when entering/updating your elements or what even what child datum corresponds to what parent datum. But also, say you dislike the symbol and now want a circle and rect, or two circles and a rect, then you need to substantially adjust the above approaches (perhaps by creating a fancy enter function that returns different types of shapes depending on index or on some identifier that tells you what symbol sub-component the datum corresponds to).
I believe attempting to create one unique datum per element is not ideal in this case, which is why I'm advocating for one datum per symbol.
Implementation
So, let's do one datum per symbol (where the symbols have child elements). This should be fairly easy to implement, I'll go over a simple method to do this here:
We can create the symbol in the join's enter function, and update it in the update function:
function updatea (){
a[0].start += 10*Math.sin(t);
a[0].end += 10*Math.cos(t);
t += 0.1;
var miao = svg.selectAll('g').data(a).join(
enter => {
// create the parent element to hold the symbol
let entered = enter.append('g')
.attr('transform', (d,i) =>'translate('+d.start+','+(i+1)*50+')')
.attr('class','symbol');
// append the sub-components of the symbol
entered.append('circle').attr('r', 10).attr('fill','red');
entered.append('circle').attr('class','end').attr('r', 15).attr('fill','yellow').attr('cx',d=>d.end);
},
update => {
// update overall positioning
update.attr('transform', (d,i) =>'translate('+d.start+','+(i+1)*50+')')
// update the sub-components
update.select('.end').attr('cx',d=>d.end);
return update
},
exit => exit.remove()
)
First, it's important to note, even though you've likely noticed, the parent datum is passed to child elements when using selection.append().
In the enter function passed to selection.join() We enter the g, style it as appropriate. Then we add the symbol sub-components and set their initial properties.
In the update function we update the overall position of the symbol and then the sub components.
Nothing occurs outside the join method in this case.
I cannot fork your observable without creating another account somewhere, so I'll just make a snippet of your example:
const svg = d3.select("body").append("svg")
.attr("width", 600)
.attr("height", 150);
var a = [{'start': 100, 'end': 200},{'start':100, 'end':200}];
var t = 0;
function updatea (){
a[0].start += 5*Math.sin(t);
a[0].end += 5*Math.cos(t);
a[1].start += 5*Math.cos(t);
a[1].end += 5*Math.sin(t);
t += 0.1;
var miao = svg.selectAll('g').data(a).join(
enter => {
// create the parent element to hold the symbol
let entered = enter.append('g')
.attr('transform', (d,i) =>'translate('+d.start+','+(i+1)*50+')')
.attr('class','symbol');
// append the sub-components of the symbol
entered.append('circle').attr('r', 10).attr('fill','red');
entered.append('circle').attr('class','end').attr('r', 15).attr('fill','yellow').attr('cx',d=>d.end);
},
update => {
// update overall positioning
update.attr('transform', (d,i) =>'translate('+d.start+','+(i+1)*50+')')
// update the sub-components
update.select('.end').attr('cx',d=>d.end);
return update
},
exit => exit.remove()
)
}
updatea();
setInterval(updatea, 100)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.0.0/d3.min.js"></script>
The idea is that since we have two circles, have two data with them. (Ignore what's in them for now:)
var a = [{...},
{...}];
Let's create a group:
var group = svg.append("g")
Then, d3 will "join" the data to DOM nodes. For each data, d3 creates a DOM node. In this case, since we're join()ing to circles, d3 will create a circle for each data. See this page for more details.
group
.selectAll('circle')
.data(a)
.join('circle')
.attr('r', 10)
.attr('fill','red')
.attr('transform', (d, i) => ('translate('+d.start+','+(i+1)*50+')'));
As for the actual logic, there's a couple things I changed.
Each circle now stores its own t and nothing else:
var a = [{t: 0},
{t: Math.PI/2}];
Then, the start and end attributes are set in order to have a representation independent of the current object's state. This allows us to have circles which have different t phases:
a.forEach((d, i) => {
d.start = 200 + 100*Math.sin(d.t);
d.end = 200 + 100*Math.cos(d.t);
d.t += 0.1;
})
Breaking it down:
(initial (range)
position)
200 + 100*Math.cos(d.t);
So it starts at position 200 and can either go to +100 or -100: the effective range is [100, 300].
You notice we do a lot of number crunching here. Basically, we're converting one domain of numbers (-1, 1) to a range (100, 300). This is a common use case for a scale, which can convert any domain to any range.
Observable notebook
I'm working on a crossfilter between a line chart and choropleth. I recently got a much better understanding of the reduce method in dc.js, so I want to pass through some more metadata about each data point to my line chart and my choropleth. This is working really well for the line chart, and I now have a tooltip showing lots of information about each point.
For my choropleth, however, the transition to using reduce instead of reduceSum has really messed up my data. For example:
The value getting passed to the tooltip isn't the data I would expect, and I have no idea where the calculation is coming from (it almost seems like it's from the SVG path, or even the color accessor?)
As I toggle between different renderings of the choropleth, my choropleth changes, but the value on the tooltip stays exactly the same
The initial render of the choropleth is showing a fully blue map, so it seems like the initial value might be incorrect, anyway.
I'm trying to understand how I can identify the data point that's coming from the group, use that to render the choropleth based on a specific value (total_sampled_sales) and then pass that data to the tooltip so that each state's value and metadata can be displayed.
Also, any tips for debugging this type of issue would be greatly appreciated. As you may be able to see from my console.logs, I'm having a hard time tracing the data through to the tooltip. This is presumably the problem block:
us1Chart.customUpdate = () => {
us1Chart.colorDomain(generateScale(returnGroup()))
us1Chart.group(returnGroup())
}
us1Chart.width(us1Width)
.height(us1Height)
.dimension(stateRegion)
.group(returnGroup())
.colors(d3.scaleQuantize().range(colorScales.blue))
.colorDomain(generateScale(returnGroup()))
.colorAccessor(d => {
// console.log('colorAccessor:', d)
return d ? d : 0
})
.overlayGeoJson(statesJson.features, "state", d => {
// console.log(`geojson`, d, d.properties.name)
return d.properties.name
})
.projection(d3.geoAlbersUsa()
.scale(Math.min(getWidth('us1-chart') * 2.5, getHeight('us1-chart') * 1.7))
.translate([getWidth('us1-chart') / 2.5, getHeight('us1-chart') / 2.5])
)
.valueAccessor(kv => {
// console.log(kv.value)
if (kv.value !== undefined) return kv.value
})
.renderTitle(false)
.on('pretransition', (chart) => {
chart.selectAll('path')
.call(mapTip)
.on('mouseover.mapTip', mapTip.show)
.on('mouseout.mapTip', mapTip.hide);
})
https://jsfiddle.net/ayyrickay/f1vLwhmq/19/
Note that the data's a little wonky because I removed half of the records just for size constraints
Because of the data binding with choropleths, I'm now using the data that's passed through (specifically, the US State that was selected) and then identifying the data point in the crossfilter group:
const selectedState = returnGroup().all().filter(item => item.key === d.properties.name)[0]
So I have a returnGroup method that selects the correct choropleth group (based on some app state), gets the list, and then I filter to see which item matches the name property passed to the tooltip. Because filter returns an array, I'm just being optimistic that it'll filter to one item and then use that item. Then I have access to the full data point, and can render it in the tooltip accordingly. Not elegant, but it works.
I trying to implement scatter chart by example.
In example we can see creating dimension:
runDimension = ndx.dimension(function(d) {return [+d.Expt, +d.Run]; });
Example's data:
Expt Run Speed
1 1 850
1 2 740
1 3 900
I want to use same chart, but I have my data in next format:
[
{
"Timestamp":"2016-12-15T17:29:53Z",
"idgame":"euro",
"users":{
"Jo": {
"energy":200,
"jump_height":0.5
},
"Bob": {
"energy":220,
"jump_height":0.35
}
}
},
{
"Timestamp":"2016-12-15T17:29:55Z",
"idgame":"euro",
"users":{
"Jo": {
"energy":120,
"jump_height":0.15
},
"Bob": {
"energy":240,
"jump_height":0.75
}
}
}
]
I need to build next chart, where x-axis is timestamp and y-axis is jump_height:
My data is allready in crossfilter, so I can't change it.
How can I create good dimension with current format?
I'm still not convinced this is worth the effort, versus biting the bullet and flattening your data and fixing up the other charts. If your data isn't flat you will be fighting crossfilter and dc.js every step of the way.
That said, as usual, it's possible!
We can't use the series chart, because that requires all the data to be present in one group. But since you want to produce multiple symbols from each row of data, an ordinary crossfilter group can't produce the data you need.
Maybe we could use a fake group, but that would be complicated. Instead, let's produce a dimension and group for each user, and then sew them together using a composite chart.
First, we need to parse those timestamps:
data.forEach(function(d) {
d.Timestamp = new Date(d.Timestamp);
});
Next, we'll retrieve a list of all users, by pulling the user keys from each event (timestamp), concatenating them, and then using a d3.set to unique them:
// retrieve all users from all events
var users = data.reduce(function(p, event) {
return p.concat(Object.keys(event.users));
}, []);
users = d3.set(users).values();
In the rest of the code, we'll assume there are the same users for each event. It's possible for them to be different, but it adds extra complexity, and this answer is complicated enough. Just ping me me if you need that feature!
We'll create the chart, crossfilter, and a scale which will assign symbols to users:
var chart = dc.compositeChart("#test");
var ndx = crossfilter(data);
var symbolScale = d3.scale.ordinal().range(d3.svg.symbolTypes);
Now we can create the composite chart. (We'll add the scatter subcharts in the next step.)
chart
.width(768)
.height(480)
.x(d3.time.scale())
.xUnits(d3.time.seconds)
.elasticX(true)
.elasticY(true)
.brushOn(false)
.clipPadding(10)
.shareTitle(false) // allow default scatter title to work
.shareColors(true) // but use different colors for subcharts
.legend(dc.legend().x(350).y(350).itemHeight(13)
.gap(5).horizontal(1).legendWidth(140).itemWidth(70));
We set up the X axis with a time scale, with a resolution of seconds. Both axes have elastic. We need to share colors so that each subchart will be assigned its own color. (The legend is perhaps overspecified - I copied this from another example.)
Finally we get to the meat of it. For each user, we'll create a subchart, and we'll tell the composite chart to compose all of them:
chart.compose(users.map(function(user) {
var userDimension = ndx.dimension(function(d) {
return [d.Timestamp, d.users[user].jump_height];
})
var jumpGroup = userDimension.group();
console.log(user, jumpGroup.all());
var scatter = dc.scatterPlot(chart)
.symbol(symbolScale(user))
.dimension(userDimension)
.group(jumpGroup)
.colorAccessor(function() { return user; })
.symbolSize(8)
.highlightedSize(10);
return scatter;
}))
We're creating a new dimension for each chart. This is because the dc.js scatter plot expects the keys to include both X and Y coordinates, and we can only access the Y coordinate (jump_height) once we know the user. Once we get past that, the group is simple.
The chart will assign the symbol and color based on the user key. These both work the same; an ordinal scale will assign a new value from the range for each new value it encounters in the domain. The only difference is that we're using the default color scale, whereas we had to specify our own symbol scale.
Here's a fiddle: https://jsfiddle.net/gordonwoodhull/3m4mv3xf/19/
I have a chart done in the D3. It works but I would like to add the ability of the axis scaling.
I have several sets of data that have a completely different range.
So I would like the Y axis range to be altered according to the selected series.
This is the entire code: https://plnkr.co/edit/89ERO3GuxREvYQ3DRX5L?p=preview
For the moment the example linked has 4 series: I would like that if for example the user selects "Kiwi" and "Apple" then the Y axis has range [0-1] and not [0-15] (or something like that).
The important thing is that the range is changed according to the selected data.
An example of what I mean is http://bl.ocks.org/andrewleith/3824854.
To do so I thought to save in an array the tags that have opacity 0.5 or 1 (the visible lines) and then according to that change the domain Y.
I created the array, and then I tried to identify the tag with opacity 0.5 (for simplicity) in this way:
var actives = new Array();
var activeElem = d3.select(".line").attr("opacity", 0.5);
console.log(activeElem);
or
var actives = new Array();
var activeElem = d3.selection.style({opacity: "0.5"});
console.log(activeElem);
or
var actives = new Array();
var activeElem = d3.selectAll("path[opacity='0.5']");
console.log(activeElem);
None of these works.
I also did other tests but I don't remember exactly what, and still did not work.
I am going crazy.
What is the right way to select items with opacity 0.5 and 1 and change the Y-axis based on the selected data?
Thank you
Hi the main issue here is that you need to make another calculation of the min and max values of your data and afterwards update your yAxis with the new domain with the min and max values.
// find the new max value of the selected series
var max = d3.max(filteredData, function(d) {
return d3.max(d.values, function(e) {
return e.value;
});
});
// find the new min value of the selected series
var min = d3.min(filteredData, function(d) {
return d3.min(d.values, function(e) {
return e.value;
});
});
// set the new y domain
y.domain([min, max]);
// update our new y axis with the new domain
svg.selectAll("g .y.axis").transition()
.duration(800).call(yAxis);
Since you want to filter your data by selecting certain lines I recommend using the data function which will allow you to join the specified array of data with the current selection (more info here). You are making a huge forEach loop when you could be using the d3 utilities. Something in the lines of:
// apend a group element which will contain our lines
svg.append('g')
.attr('class', 'line-container')
.selectAll('.normal-line-paths')
.data(dataNest) // set our data
.enter() // enter the data and start appending elements
.append('path')
.call(path); // calling our path function for later use when appending lines
I modified your code to use a more document driven approach, you can click on the labels and the y axis will be scaled accordingly t the new min and max values of the filtered dataset. I left the dashed group in blank so you can try and code the remaining parts and understand the logic behind the data join.
https://plnkr.co/edit/Wso0Gu4yHkdsHhl8NY8I?p=preview
EDIT:
Here is the plnkr with the dashed lines :)
https://plnkr.co/edit/yHGGtrLXZyyq0KpiVWYf?p=preview
I am struggling with D3. I have a dataset that I am pulling into D3, and populating a bar chart. The problem is there are too many bars! I want to only display the top 10 values.
My csv has two values, the key (column header) is "Name" and the value is "NumberOfTickets". I want to show only 10 names that have the most tickets. I assume i need to sort and splice my dataset, but I have tried everything and cant get it to work. It seems most tutorials are geared towards simple arrays and not a dataset that comes from a CSV with key value pairs. Any help would be appreciated. Here is my dataset portion of the D3 code. I assume this is where I need to manipulate the dataset:
var report = data.forEach(function(d){
d.Name= d.Name;
d.NumberOfTickets= +d.NumberOfTickets;
});
Try this:
var report = data.sort(function(a, b) {
return d3.descending(+a.NumberOfTickets, +b.NumberOfTickets);
}).slice( 0, 10);
If you want the names that get less tickets, just change for "ascending". If you want to change your slice, remember that the start number is inclusive, but the ending number is not.