Related
I try to use dx/dy to add offset to group elems after drag, current code not group elements not following mouse move.
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 300);
var nodes = [
{
id:"A",
x:50,
y:50,
text:"hello"
}
]
add_box(svg,50,50,nodes)
var tooltip = d3.select('body')
.append('div')
.attr('id','tooltip')
.style('position','absolute')
.style('opacity',0)
.style('background','lightsteelblue')
function add_box(svg,x,y,nodes) {
var g = svg.selectAll('.node')
.data(nodes)
.join('g')
.attr('class','node')
g.call(d3.drag()
.on('start', dragStart)
.on('drag', dragging)
.on('end', dragEnd)
)
var txt = g.append('text')
.text(d => d.text)
.attr('x',d => d.x)
.attr('y',d => d.y)
var bbox = txt.node().getBBox()//getBoundingClientRect()
var m = 2
bbox.x -= m
bbox.y -= m
bbox.width += 2*m
bbox.height += 2*m
var rect = g.append('rect')
.attr('x',bbox.x)
.attr('y',bbox.y)
.attr('width',bbox.width)
.attr('height',bbox.height)
.attr('fill','none')
.attr('stroke','black')
}
function dragStart(event,d){
d3.select(this).raise()
.style("stroke", "")
d3.select('#tooltip')
.transition().duration(100)
.style('opacity', 1)
}
function dragging(event,d){
var x = event.x;
var y = event.y;
var dx = event.dx
var dy = event.dy
d3.select(this).select("text")
.attr("dx", dx)
.attr("dy", dy);
d3.select(this).select("rect")
.attr("dx", dx)
.attr("dy", dy);
var desc = "(" + x.toFixed(1) +"," + y.toFixed(1) + ")"
d3.select('#tooltip')
.style('left', (x+2) + 'px')
.style('top', (y-2) + 'px')
.text(desc)
}
function dragEnd(event,d){
d3.select(this)
.style("stroke", "black")
d3.select('#tooltip').style('opacity', 0)
}
<script src="https://d3js.org/d3.v7.min.js"></script>
I have data nested into 3 levels, which I need to dynamically update. The kicker is that the elements for the mid-level need to actually display on TOP of the elements for the low-level due to some hover behavior I need, so I'm having trouble with what the enter/update/exit/merge pattern should look like. (There don't need to be any elements displayed for the high-level).
The code I have right now updates the data successfully but is not rendering the rectangles at all, instead giving me an error, Uncaught TypeError: this.setAttribute is not a function.
How do I fix this problem, please?
Here's what it should look like before updating:
And here's what it should look like after updating:
Here's a CodePen with the code Below
```
let width = 0.9 * window.innerWidth,
height = 0.9 * window.innerHeight,
colors = ['darkviolet', 'steelblue', 'coral', 'Turquoise', 'firebrick', 'mediumslateblue', 'palevioletred', 'green', 'aqua'];
let data1 =
[{"group":"A","segment":"1","item":"1"},
{"group":"A","segment":"1","item":"2"},
{"group":"A","segment":"1","item":"3"},
{"group":"A","segment":"2","item":"4"},
{"group":"A","segment":"2","item":"5"},
{"group":"A","segment":"2","item":"6"},
{"group":"A","segment":"3","item":"7"},
{"group":"A","segment":"3","item":"8"},
{"group":"A","segment":"3","item":"9"},
{"group":"B","segment":"4","item":"1"},
{"group":"B","segment":"4","item":"2"},
{"group":"B","segment":"4","item":"3"},
{"group":"B","segment":"5","item":"4"},
{"group":"B","segment":"5","item":"5"},
{"group":"B","segment":"5","item":"6"},
{"group":"B","segment":"6","item":"7"},
{"group":"B","segment":"6","item":"8"},
{"group":"B","segment":"6","item":"9"},
{"group":"C","segment":"7","item":"1"},
{"group":"C","segment":"7","item":"2"},
{"group":"C","segment":"7","item":"3"},
{"group":"C","segment":"8","item":"4"},
{"group":"C","segment":"8","item":"5"},
{"group":"C","segment":"8","item":"6"},
{"group":"C","segment":"9","item":"7"},
{"group":"C","segment":"9","item":"8"},
{"group":"C","segment":"9","item":"9"}],
data2 =
[{"group":"A","segment":"1","item":"1"},
{"group":"A","segment":"8","item":"2"},
{"group":"A","segment":"9","item":"3"},
{"group":"A","segment":"2","item":"4"},
{"group":"A","segment":"2","item":"5"},
{"group":"A","segment":"2","item":"6"},
{"group":"A","segment":"5","item":"7"},
{"group":"A","segment":"3","item":"8"},
{"group":"A","segment":"3","item":"9"},
{"group":"B","segment":"4","item":"1"},
{"group":"B","segment":"4","item":"2"},
{"group":"B","segment":"7","item":"3"},
{"group":"B","segment":"5","item":"4"},
{"group":"B","segment":"5","item":"5"},
{"group":"B","segment":"5","item":"6"},
{"group":"B","segment":"5","item":"7"},
{"group":"B","segment":"6","item":"8"},
{"group":"B","segment":"6","item":"9"},
{"group":"C","segment":"7","item":"1"},
{"group":"C","segment":"7","item":"2"},
{"group":"C","segment":"3","item":"3"},
{"group":"C","segment":"8","item":"4"},
{"group":"C","segment":"8","item":"5"},
{"group":"C","segment":"8","item":"6"},
{"group":"C","segment":"9","item":"7"},
{"group":"C","segment":"6","item":"8"},
{"group":"C","segment":"1","item":"9"}];
let button = d3.select('body')
.append('button')
.attr('type', 'button')
.style('display', 'block')
.text('Update')
.on('click', function() { update(data2) });
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
.append('g');
let color = d3.scaleOrdinal().range(colors);
update(data1);
function getxy(data) {
let grouped = Array.from(d3.group(data, d=> d.group, d=> d.segment), ([key, value]) => ({key, value}));
grouped.forEach(function(s) {
s.value = Array.from(s.value, ([key, value]) => ({key, value}));
s.value.forEach(function(d) {
d.start = d3.min(d.value, function(t) { t.segment = +t.segment; t.item = +t.item; return +t.item });
d.end = d3.max(d.value, function(t) { return t.item });
d.key = +d.key;
d.group = s.key;
})
})
let x1 = d3.scaleBand()
.domain([1, 2, 3, 4, 5, 6, 7, 8, 9])
.range([width*0.05, width])
.padding(0.0);
let y1 = d3.scaleBand()
.domain(['A', 'B', 'C'])
.range([10, height])
.padding(0.1);
return [x1, y1, grouped];
}
function update(data) {
let xy = getxy(data);
let x = xy[0], y = xy[1], groupedData = xy[2];
let barsAll = svg
.selectAll('.bars')
.data(groupedData);
barsAll.exit().remove();
let barsEnter = barsAll
.enter()
.append('g')
.attr('class', 'bars');
barsEnter = barsEnter.merge(barsAll);
let segmentsAll = barsEnter
.selectAll('.segments')
.data(function(d) { return d.value });
segmentsAll.exit().remove();
let segmentsEnter = segmentsAll.enter();
let bitsAll = segmentsEnter
.selectAll('.bits')
.data(function(d) { return d.value });
bitsAll.exit().remove();
let bitsEnter = bitsAll
.enter()
.append('circle')
.attr('class', 'bits')
.attr('r', width*0.05)
.attr('stroke', 'none');
bitsEnter = bitsEnter.merge(bitsAll);
bitsEnter
.attr('cx', function(d) { return x(d.item) })
.attr('cy', function(d) { return y(d.group) + y.bandwidth()/2 })
.attr('fill', function(d) { return color(d.segment) });
segmentsEnter.append('rect')
.attr('stroke', 'black')
.attr('class', 'segments')
.style('fill-opacity', 0.2);
segmentsEnter = segmentsEnter.merge(segmentsAll);
segmentsEnter
.attr('fill', function(d) { return color(d.key) })
.attr('height', y.bandwidth()*0.75)
.attr('x', function(d) { return x(d.start) - width*0.05 })
.attr('y', function(d) { return y(d.group) + y.bandwidth()*0.125 })
.attr('width', function(d) { return x(d.end) - x(d.start) + width*0.1 });
}
```
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-array.v2.min.js"></script>
Well, by going around the “merge” step on the mid-level segments and low-level bits (but not on the top-level bars), I was able to hack a fix, finally. Working pen
Still open to help from others because I feel like I should really get the hang of the whole flow - update, enter, exit, merge - at some point.
let width = 0.9 * window.innerWidth,
height = 0.9 * window.innerHeight,
colors = ['darkviolet', 'steelblue', 'coral', 'Turquoise', 'firebrick', 'mediumslateblue', 'palevioletred', 'green', 'aqua'];
let data1 =
[{"group":"A","segment":"1","item":"1"},
{"group":"A","segment":"1","item":"2"},
{"group":"A","segment":"1","item":"3"},
{"group":"A","segment":"2","item":"4"},
{"group":"A","segment":"2","item":"5"},
{"group":"A","segment":"2","item":"6"},
{"group":"A","segment":"3","item":"7"},
{"group":"A","segment":"3","item":"8"},
{"group":"A","segment":"3","item":"9"},
{"group":"B","segment":"4","item":"1"},
{"group":"B","segment":"4","item":"2"},
{"group":"B","segment":"4","item":"3"},
{"group":"B","segment":"5","item":"4"},
{"group":"B","segment":"5","item":"5"},
{"group":"B","segment":"5","item":"6"},
{"group":"B","segment":"6","item":"7"},
{"group":"B","segment":"6","item":"8"},
{"group":"B","segment":"6","item":"9"},
{"group":"C","segment":"7","item":"1"},
{"group":"C","segment":"7","item":"2"},
{"group":"C","segment":"7","item":"3"},
{"group":"C","segment":"8","item":"4"},
{"group":"C","segment":"8","item":"5"},
{"group":"C","segment":"8","item":"6"},
{"group":"C","segment":"9","item":"7"},
{"group":"C","segment":"9","item":"8"},
{"group":"C","segment":"9","item":"9"}],
data2 =
[{"group":"A","segment":"1","item":"1"},
{"group":"A","segment":"8","item":"2"},
{"group":"A","segment":"9","item":"3"},
{"group":"A","segment":"2","item":"4"},
{"group":"A","segment":"2","item":"5"},
{"group":"A","segment":"2","item":"6"},
{"group":"A","segment":"5","item":"7"},
{"group":"A","segment":"3","item":"8"},
{"group":"A","segment":"3","item":"9"},
{"group":"B","segment":"4","item":"1"},
{"group":"B","segment":"4","item":"2"},
{"group":"B","segment":"7","item":"3"},
{"group":"B","segment":"5","item":"4"},
{"group":"B","segment":"5","item":"5"},
{"group":"B","segment":"5","item":"6"},
{"group":"B","segment":"5","item":"7"},
{"group":"B","segment":"6","item":"8"},
{"group":"B","segment":"6","item":"9"},
{"group":"C","segment":"7","item":"1"},
{"group":"C","segment":"7","item":"2"},
{"group":"C","segment":"3","item":"3"},
{"group":"C","segment":"8","item":"4"},
{"group":"C","segment":"8","item":"5"},
{"group":"C","segment":"8","item":"6"},
{"group":"C","segment":"9","item":"7"},
{"group":"C","segment":"6","item":"8"},
{"group":"C","segment":"1","item":"9"}];
let button = d3.select('body')
.append('button')
.attr('type', 'button')
.style('display', 'block')
.text('Update')
.on('click', function() { update(data2) });
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
.append('g');
let color = d3.scaleOrdinal().range(colors);
function getxy(data) {
let grouped = Array.from(d3.group(data, d=> d.group, d=> d.segment), ([key, value]) => ({key, value}));
grouped.forEach(function(s) {
s.value = Array.from(s.value, ([key, value]) => ({key, value}));
s.value.forEach(function(d) {
d.start = d3.min(d.value, function(t) { t.segment = +t.segment; t.item = +t.item; return +t.item });
d.end = d3.max(d.value, function(t) { return t.item });
d.key = +d.key;
d.group = s.key;
})
})
let x1 = d3.scaleBand()
.domain([1, 2, 3, 4, 5, 6, 7, 8, 9])
.range([width*0.05, width])
.padding(0.0);
let y1 = d3.scaleBand()
.domain(['A', 'B', 'C'])
.range([10, height])
.padding(0.1);
return [x1, y1, grouped];
}
function update(data) {
let xy = getxy(data);
let x = xy[0], y = xy[1], groupedData = xy[2];
// update
let barsAll = svg
.selectAll('.bars')
.data(groupedData);
// exit
barsAll.exit().remove();
// enter
let barsEnter = barsAll
.enter();
barsEnter = barsEnter.merge(barsAll).append('g');
barsEnter.selectAll('.segments').remove();
d3.selectAll('.segments').remove();
let segmentsAll = barsEnter
.selectAll('.segments')
.data(function(d) { return d.value });
segmentsAll.exit().remove();
let segmentsEnter = segmentsAll
.enter();
let bitsAll = segmentsEnter
.selectAll('.bits')
.data(function(d) { return d.value });
bitsAll.exit().remove();
bitsAll
.enter()
.append('circle')
.attr('class', 'bits')
.attr('r', width*0.05)
.attr('stroke', 'none')
.attr('cx', function(d) { return x(d.item) })
.attr('cy', function(d) { return y(d.group) + y.bandwidth()/2 })
.attr('fill', function(d) { return color(d.segment) });
// bitsEnter = bitsEnter.merge(bitsAll);
bitsAll
.attr('cx', function(d) { return x(d.item) })
.attr('cy', function(d) { return y(d.group) + y.bandwidth()/2 })
.attr('fill', function(d) { return color(d.segment) });
segmentsEnter
.append('rect')
.attr('class', 'segments')
.attr('stroke', 'black')
.style('fill-opacity', 0.2)
.attr('fill', function(d) { return color(d.key) })
.attr('height', function() { return y.bandwidth()*0.75 })
.attr('x', function(d) { return x(d.start) - width*0.05 })
.attr('y', function(d) { return y(d.group) + y.bandwidth()*0.125 })
.attr('width', function(d) { return x(d.end) - x(d.start) + width*0.1 });
segmentsAll
.attr('fill', function(d) { return color(d.key) })
.attr('height', function() { return y.bandwidth()*0.75 })
.attr('x', function(d) { return x(d.start) - width*0.05 })
.attr('y', function(d) { return y(d.group) + y.bandwidth()*0.125 })
.attr('width', function(d) { return x(d.end) - x(d.start) + width*0.1 });
//segmentsAll = segmentsEnter.merge(segmentsAll);
// segmentsEnter
// .attr('fill', function(d) { return color(d.key) })
// .attr('height', function() { return y.bandwidth()*0.75 })
// .attr('x', function(d) { return x(d.start) - width*0.05 })
// .attr('y', function(d) { return y(d.group) + y.bandwidth()*0.125 })
// .attr('width', function(d) { return x(d.end) - x(d.start) + width*0.1 });
}
update(data1);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js"></script>
<script src="https://d3js.org/d3-array.v2.min.js"></script>
I have a data legend SVG and I want to set the height and width of this SVG to be equal to the number of g which is based on the data. But how do I achieve this? My SVG height and width is always not according to the my G. I tried node().getBBox() but it is still not giving me the right height and width.
Here is my code :
var legend = d3.select(".svgLegend")
.append('svg')
.attr("id", "legend")
.append('g')
.attr("class", "mainGroup")
.attr('legend', true)
var itemEnter = legend.selectAll('g.legendItem')
.data(legendData)
.enter()
.append('g')
.attr('class', function (d) {
return 'legendItem ' + safe_name(d.name);
})
itemEnter.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', '10')
.attr('height', '10')
.style('fill', function (d) {
return color(d.name);
})
.attr('transform', 'translate(10,6)')
.attr('class', function (d) {
return 'legendRect ' + safe_name(d.name);
})
itemEnter.append('text')
.attr('x', 0)
.attr('y', 0)
.attr('class', 'legendText')
.text(function (d) { return d.name })
.attr('transform', 'translate(25, 15)')
itemEnter.selectAll("text").each(function () {
var textLength = this.getComputedTextLength();
itemEnter.attr("transform", function (d, i) { return "translate(" + i % 8 * (textLength + 60) + "," + Math.floor(i / 8) * itemHeight + ")"; })
})
Legend Data :
[
{
"name":"Malaysia",
"value":350,
"percentage":"48.61"
},
{
"name":"England",
"value":300,
"percentage":"41.67"
},
{
"name":"China",
"value":400,
"percentage":"55.56"
},
{
"name":"South Korea",
"value":600,
"percentage":"83.33"
}
]
What I want to achieve is that the svg's height and width is exact same as itemEnter's height and width.
You can use the values from getClientBoundingRect() to set the width and height of your SVG:
var bRect = legend.node().getBoundingClientRect()
svg.attr('width', bRect.width + 10)
.attr('height', bRect.height)
(adding in an extra 10px to the width for safety)
Demo:
var legendData = [
{
"name":"Malaysia",
"value":350,
"percentage":"48.61"
},
{
"name":"England",
"value":300,
"percentage":"41.67"
},
{
"name":"China",
"value":400,
"percentage":"55.56"
},
{
"name":"South Korea",
"value":600,
"percentage":"83.33"
}
]
function safe_name (t) {
return t.replace(/\W/g, '_')
}
function color (d) {
var colors = {
China: 'deepskyblue',
'South Korea': 'deeppink',
England: 'red',
Malaysia: 'goldenrod'
}
return colors[d]
}
var svg = d3.select(".svgLegend")
.append('svg')
.attr("id", "legend")
var legend = svg
.append('g')
.attr("class", "mainGroup")
.attr('legend', true)
var itemEnter = legend.selectAll('g.legendItem')
.data(legendData)
.enter()
.append('g')
.attr('class', function (d) {
return 'legendItem ' + safe_name(d.name);
})
itemEnter.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', '10')
.attr('height', '10')
.style('fill', function (d) {
return color(d.name);
})
.attr('transform', 'translate(10,6)')
.attr('class', function (d) {
return 'legendRect ' + safe_name(d.name);
})
itemEnter.append('text')
.attr('x', 0)
.attr('y', 0)
.attr('class', 'legendText')
.text(function (d) { return d.name })
.attr('transform', 'translate(25, 15)')
var itemHeight = 25
itemEnter.selectAll("text")
.each(function () {
var textLength = this.getComputedTextLength();
itemEnter.attr("transform", function (d, i) { return "translate(" + i % 8 * (textLength + 60) + "," + Math.floor(i / 8) * itemHeight + ")"; })
})
var bRect = legend.node().getBoundingClientRect()
svg.attr('width', bRect.width + 10)
.attr('height', bRect.height)
<script src="http://d3js.org/d3.v5.js"></script>
<div class="svgLegend"></div>
Using getBBox()is a good idea. This is how I would do it.
let bbox = test.getBBox();
//console.log(bbox)
svg.setAttributeNS(null, "viewBox", `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height} `)
svg.setAttributeNS(null, "width", bbox.width);
svg.setAttributeNS(null, "height", bbox.height);
<svg id="svg" >
<g id="gtest">
<path id="test" d="M187.476,214.443c-2.566,11.574-4.541,22.658-7.542,33.456
c-3.558,12.8-7.14,25.713-12.242,37.938c-10.223,24.495-41.321,29.239-58.824,9.548c-9.592-10.792-11.295-26.9-3.539-40.556
c11.233-19.778,25.391-37.46,40.447-54.438c1.07-1.207,2.116-2.436,3.893-4.484c-7.212,0.9-13.349,1.988-19.529,2.374
c-16.283,1.018-32.578,2.21-48.881,2.437c-18.686,0.261-32.846-10.154-37.071-26.055c-6.762-25.449,15.666-48.973,41.418-43.338
c23.645,5.175,46.447,12.901,68.424,23.051c1.033,0.478,2.083,0.918,3.933,1.731c-0.83-1.947-1.341-3.225-1.911-4.475
c-9.896-21.701-18.159-43.986-23.192-67.337c-4.587-21.28,8.933-40.56,29.946-43.257c20.134-2.585,38.124,12.991,39.091,34.294
c1.029,22.682-0.049,45.292-3.58,67.755c-0.17,1.079-0.152,2.188-0.246,3.659c8.05-6.831,15.471-13.737,23.52-19.811
c11.147-8.412,22.398-16.795,34.27-24.113c18.35-11.312,40.821-4.481,50.028,14.385c9.091,18.628,0.131,40.586-20.065,48.198
c-11.034,4.158-22.248,7.944-33.594,11.143c-11.321,3.191-22.908,5.438-34.866,8.212c1.189,0.81,2.19,1.504,3.205,2.18
c18.402,12.261,37.157,24.032,55.101,36.932c14.769,10.616,18.619,29.317,10.675,44.578c-7.537,14.477-25.151,22.136-40.767,17.583
c-7.583-2.212-14.022-6.469-18.523-12.919c-12.463-17.86-24.638-35.924-36.898-53.925
C189.24,217.849,188.547,216.357,187.476,214.443z"/>
</g>
</svg>
I've made a scatterplot and a choropleth map in the same web page. Data is stored in a .CSV and .json, and elements are linked with a "name" field.
I've made a tooltip on mouseover on both. I want now some interactivity beetween them: when mouse is over an element on scatterplot, this element on choropleth react and when mouse is over choropleth map scatterplot react.
Scatterplot and choropleth are in differents div with specifics ID and I don't how can I refeer from one to an other. I tried d3.select("this#scatterplot"); like this example but it doesn't work for me.
How can I select elements in differents DIV and differents functions ?
I want something like this :
function handleMouseOverMap(d, i) {
d3.select('this#choropleth').style('stroke-width', 3);
d3.select('this#scatterplot').attr('r', 8);
}
function handleMouseOverGraph(d, i) {
d3.select('this#scatterplot').attr('r', 8);
d3.select('this#choropleth').style('stroke-width', 3);
}
Code
<div id="scatterplot"></div>
<div id="choropleth"></div>
<script>
d3.queue()
.defer(d3.csv, 'data.csv', function (d) {
return {
name: d.name,
sau: +d.sau,
uta: +d.uta
}
})
.defer(d3.json, 'dept.json')
.awaitAll(initialize)
var color = d3.scaleThreshold()
.domain([150000, 300000, 450000])
.range(['#5cc567', '#e7dc2b', '#e59231', '#cb0000'])
function initialize(error, results) {
if (error) { throw error }
var data = results[0]
var features = results[1].features
var components = [
choropleth(features),
scatterplot(onBrush)
]
function update() {
components.forEach(function (component) { component(data) })
}
function onBrush(x0, x1, y0, y1) {
var clear = x0 === x1 || y0 === y1
data.forEach(function (d) {
d.filtered = clear ? false
: d.uta < x0 || d.uta > x1 || d.sau < y0 || d.sau > y1
})
update()
}
update()
}
/* Graphique */
function scatterplot(onBrush) {
var margin = { top: 10, right: 15, bottom: 40, left: 75 }
var width = 680 - margin.left - margin.right
var height = 550 - margin.top - margin.bottom
var x = d3.scaleLinear()
.range([0, width])
var y = d3.scaleLinear()
.range([height, 0])
// Tooltip
var xValue = function(d) { return d.sau;};
var yValue = function(d) { return d.uta;};
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var xAxis = d3.axisBottom()
.scale(x)
.tickFormat(d3.format(''))
var yAxis = d3.axisLeft()
.scale(y)
.tickFormat(d3.format(''))
// Selection
var brush = d3.brush()
.extent([[0, 0], [width, height]])
.on('start brush', function () {
var selection = d3.event.selection
var x0 = x.invert(selection[0][0])
var x1 = x.invert(selection[1][0])
var y0 = y.invert(selection[1][1])
var y1 = y.invert(selection[0][1])
onBrush(x0, x1, y0, y1)
})
var svg = d3.select('#scatterplot')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
var bg = svg.append('g')
var gx = svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
var gy = svg.append('g')
.attr('class', 'y axis')
gx.append('text')
.attr('x', width)
.attr('y', 35)
.style('text-anchor', 'end')
.style('fill', '#000')
.style('font-weight', 'bold')
.text('UTA')
gy.append('text')
.attr('transform', 'rotate(-90)')
.attr('x', 0)
.attr('y', -55)
.style('text-anchor', 'end')
.style('fill', '#000')
.style('font-weight', 'bold')
.text('SAU - ha')
svg.append('g')
.attr('class', 'brush')
.call(brush)
return function update(data) {
x.domain(d3.extent(data, function (d) { return d.uta })).nice()
y.domain(d3.extent(data, function (d) { return d.sau })).nice()
gx.call(xAxis)
gy.call(yAxis)
var bgRect = bg.selectAll('rect')
.data(d3.pairs(d3.merge([[y.domain()[0]], color.domain(), [y.domain()[1]]])))
bgRect.exit().remove()
bgRect.enter().append('rect')
.attr('x', 0)
.attr('width', width)
.merge(bgRect)
.attr('y', function (d) { return y(d[1]) })
.attr('height', function (d) { return y(d[0]) - y(d[1]) })
.style('fill', function (d) { return color(d[0]) })
var circle = svg.selectAll('circle')
.data(data, function (d) { return d.name })
circle.exit().remove()
circle.enter().append('circle')
.attr('r', 4)
.style('stroke', '#fff')
.merge(circle)
.attr('cx', function (d) { return x(d.uta) })
.attr('cy', function (d) { return y(d.sau) })
.style('fill', function (d) { return color(d.sau) })
.style('opacity', function (d) { return d.filtered ? 0.5 : 1 })
.style('stroke-width', function (d) { return d.filtered ? 1 : 2 })
// Event
.on("mouseover", function(d) {
tooltipOverGraph.call(this, d);
handleMouseOverGraph.call(this, d);
})
.on("mouseout", function(d) {
tooltipOutGraph.call(this, d);
handleMouseOutGraph.call(this, d);
})
}
// Tooltip
function tooltipOverGraph(d) {
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(d["name"] + "<br>" + xValue(d)
+ " ha" +", " + yValue(d) + " UTA" )
.style("left", (d3.event.pageX + 5) + "px")
.style("top", (d3.event.pageY - 28) + "px");
}
function tooltipOutGraph(d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
}
}
// Create Event Handlers for mouse
function handleMouseOverGraph(d, i) {
d3.select(this).attr('r', 8);
}
function handleMouseOutGraph(d, i) {
d3.select(this).attr('r', 4);
}
/* Carte */
function choropleth(features) {
var width = 680
var height = 550
// Tooltip
var xValue = function(d) { return d.sau;};
var yValue = function(d) { return d.uta;};
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// Projection et centrage de la carte
var projection = d3.geoMercator()
.center([ 3, 46.5 ])
.scale([width * 3.1])
.translate([width / 2, height / 2])
var path = d3.geoPath().projection(projection)
var svg = d3.select('#choropleth')
.append('svg')
.attr('width', width)
.attr('height', height)
svg.selectAll('path')
.data(features)
.enter()
.append('path')
.attr('d', path)
.style('stroke', '#fff')
.style('stroke-width', 1)
// Event
.on("mouseover", function(d) {
tooltipOverMap.call(this, d);
handleMouseOverMap.call(this, d);
})
.on("mouseout", function(d) {
tooltipOutMap.call(this, d);
handleMouseOutMap.call(this, d);
})
// Tooltip
function tooltipOverMap(d) {
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(d["name"] + "<br>" + xValue(d)
+ " ha" +", " + yValue(d) + " UTA" )
.style("left", (d3.event.pageX + 5) + "px")
.style("top", (d3.event.pageY - 28) + "px");
}
function tooltipOutMap(d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
}
return function update(data) {
svg.selectAll('path')
.data(data, function (d) { return d.name || d.properties.name })
.style('fill', function (d) { return d.filtered ? '#ddd' : color(d.sau) })
}
}
// Create Event Handlers for mouse
function handleMouseOverMap(d, i) {
d3.select(this).style('stroke-width', 3);
}
function handleMouseOutMap(d, i) {
d3.select(this).style('stroke-width', 1);
}
</script>
Example
First, add distinct class attributes to the rect and circle items when you enter + append them:
bgRect.enter().append('rect')
.attr('class', function(d,i) { return 'classRect' + i; })
circle.enter().append('circle')
.attr('class', function(d,i) { return 'classCircle' + i; })
Then, update your mouse over functions:
function handleMouseOverMap(d) {
// update the choropleth //
d3.select(d).style('stroke-width', 3);
// update the scatterplot //
// capture the number contained in class (e.g. "1" for "classRect1")
var i = d3.select(d).class.substr(-1);
// select corresponding circle in scatter and update
d3.select('circle.classCircle'+i).attr('r', 8);
}
function handleMouseOverGraph(d) {
// update the scatter //
d3.select(d).attr('r', 8);
// update the choropleth //
// capture the number contained in class (e.g. "1" for "classCircle1")
var i = d3.select(d).class.substr(-1);
// select corresponding rect in the choropleth and update
d3.select('rect.classRect'+i).style('stroke-width', 3);
}
Continuing to try and master the enter-update-exit pattern...
I've got a relatively simple reusable d3.js chart, and I want to be able to update the chart between two data sets. I'm getting close, but the chart is not updating properly.
You can see a fiddle here: http://jsfiddle.net/rolfsf/vba6n4sh/2/
Where did I mess up the enter-update-exit pattern?
The chart code looks like this:
function relativeSizeChart() {
var width = 1200,
margin = 0,
padding = 16,
r = d3.scale.linear(),
onTotalMouseOver = null,
onTotalClick = null,
onClusterMouseOver = null,
onClusterClick = null,
val = function(d){return d;};
totalFormat = function(d){return d;};
clusterFormat = function(d){return d;};
clusterFormat2 = function(d){return d;};
function chart(selection) {
selection.each(function(data) {
//console.log(data);
var clusterCount = data.Clusters.length,
totalColWidth = 0.3*width,
colWidth = (width - totalColWidth)/clusterCount,
height = colWidth + 2*padding,
maxRadius = (colWidth - 10)/2;
var svg = d3.select(this).selectAll("svg")
.data([data]);
var svgEnter = svg
.enter().append("svg")
.attr('class', function(d){
if( onTotalMouseOver !== null || onTotalClick !== null ||onClusterMouseOver !== null || onClusterClick !== null){
return 'clickable';
}else{
return 'static';
}
})
.attr("width", width)
.attr("height", height);
var background, clusterLines;
background = svgEnter.append("g")
.attr('class', 'background');
var headers = svgEnter.append("g")
.attr('class', 'headers')
.selectAll("text.header")
.data(data.Headers, function(d){return d;});
var total = svgEnter.append("g")
.attr('class', 'total');
var cluster = svgEnter.selectAll('g.cluster')
.data(data.Clusters,function(d){ return d;});
var clusterEnter = cluster
.enter().append("g")
.attr('class', 'cluster')
.attr('transform', function (d, i) {
return 'translate(' + (totalColWidth + i*colWidth) + ',0)';
});
var clusters = svg.selectAll('g.cluster');
r = d3.scale.linear()
.domain([0, d3.max(data.Clusters, function(d){return d[1];})])
.range([40, maxRadius]);
background .append("rect")
.attr("class", "chart-bg")
.attr('x', 0)
.attr('y', padding)
.attr('height', (height-padding))
.attr('width', width)
.attr('class', 'chart-bg');
background .append("g")
.attr('class', 'cluster-lines');
background .append("line")
.attr("class", "centerline")
.attr('x1', (totalColWidth - padding))
.attr('x2', width - (colWidth/2))
.attr('y1', (height+padding)/2)
.attr('y2', (height+padding)/2);
clusterLines = background.select('g.cluster-lines')
.selectAll("line")
.data(data.Clusters,function(d){ return d;})
.enter().append('line')
.attr('class', 'cluster-line');
headers .enter().append('text')
.attr('class', 'header');
total .append("rect")
.attr("class", "total-cluster")
.attr('x', padding)
.attr('y', 0.2*(height+(4*padding)))
.attr('height', 0.5*(height))
.attr('width', totalColWidth-(2*padding))
.attr('rx', 4)
.attr('ry', 4)
.on('mouseover', onTotalMouseOver)
.on('click', onTotalClick);
total .append("text")
.attr("class", "total-name")
.attr('x', totalColWidth/2 )
.attr('y', function(d, i) { return ((height+padding)/2) + (padding + 10); });
total .append("text")
.attr("class", "total-value")
.attr('x', totalColWidth/2 )
.attr('y', function(d, i) { return ((height+padding)/2); })
.text(totalFormat(0));
clusterEnter.append('circle')
.attr('class', 'bubble')
.attr('cx', function(d, i) { return colWidth/2; })
.attr('cy', function(d, i) { return (height+padding)/2;})
.attr("r", "50")
.on('mouseover', function(d, i, j) {
if (onClusterMouseOver != null) onClusterMouseOver(d, i, j);
})
.on('mouseout', function() { /*do something*/ })
.on('click', function(d, i){
onClusterClick(this, d, i);
});
clusterEnter.append('text')
.attr('class', 'cluster-value')
.attr('x', function(d, i) { return colWidth/2; })
.attr('y', function(d, i) { return ((height+padding)/2); })
.text(clusterFormat(0));
clusterEnter.append('text')
.attr('class', 'cluster-value-2')
.attr('x', function(d, i) { return colWidth/2; })
.attr('y', function(d, i) { return ((height+padding)/2) + (padding + 10); })
.text(clusterFormat2(0));
//update attributes
clusterLines.attr('x1', function(d, i) { return totalColWidth + i*colWidth })
.attr('x2', function(d, i) { return totalColWidth + i*colWidth })
.attr('y1', function(d, i) { return padding })
.attr('y2', function(d, i) { return (height); });
headers .attr('x', function(d, i) {
if(i === 0){
return (totalColWidth/2);
}else{
return (totalColWidth + (i*colWidth) - (colWidth/2))
}
})
.attr('y', 12);
//clean up old
svg .exit().remove();
cluster .exit().selectAll('circle.bubble')
.style("opacity", 1)
.style("fill", "#DDD")
.style("stroke", "#DDD")
.transition()
.duration(500)
.style("opacity", 0);
cluster .exit().remove();
headers .exit().remove();
function update(data) {
//update with data
svg .selectAll('text.total-value')
.transition()
.delay(400)
.duration(1000)
.tween( 'text', function(d, i) {
var currentValue = +this.textContent.replace(/\D/g,'');
var interpolator = d3.interpolateRound( currentValue, d.Total[1] );
return function( t ) {
this.textContent = totalFormat(interpolator(t));
};
});
svg .selectAll('text.total-name')
.text(val(data.Total[0]));
svg .selectAll('circle')
.attr('class', function(d, i) {
if(d[1] === 0){ return 'bubble empty';}
else {return 'bubble';}
})
.transition()
.duration(1000)
.delay(function(d, i) { return 500 + (i * 100); })
.ease('elastic')
.attr("r", function (d, i) { return r(d[1]); });
svg .selectAll('text.cluster-value')
.transition()
.delay(function(d, i) { return 500 + (i * 100); })
.duration(1000)
.tween( 'text', function(d, i) {
var currentValue = +this.textContent.replace(/\D/g,'');
var interpolator = d3.interpolateRound( currentValue, d[1] );
return function( t ) {
this.textContent = clusterFormat(interpolator(t));
};
});
svg .selectAll('text.cluster-value-2')
.transition()
.delay(function(d, i) { return 500 + (i * 100); })
.duration(1000)
.tween( 'text', function(d, i) {
var currentValue = +this.textContent.replace(/\D/g,'');
var interpolator = d3.interpolateRound( currentValue, d[0] );
return function( t ) {
this.textContent = clusterFormat2(interpolator(t));
};
});
headers .text(function(d, i){return d});
}
update(data);
});
}
chart.totalFormat = function(_) {
if (!arguments.length) return totalFormat;
totalFormat = _;
return chart;
};
chart.clusterFormat = function(_) {
if (!arguments.length) return clusterFormat;
clusterFormat = _;
return chart;
};
chart.clusterFormat2 = function(_) {
if (!arguments.length) return clusterFormat2;
clusterFormat2 = _;
return chart;
};
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.onTotalClick = function(_) {
if (!arguments.length) return onTotalClick;
onTotalClick = _;
return chart;
};
chart.onTotalMouseOver = function(_) {
if (!arguments.length) return onTotalMouseOver;
onTotalMouseOver = _;
return chart;
};
chart.onClusterClick = function(_) {
if (!arguments.length) return onClusterClick;
onClusterClick = _;
return chart;
};
chart.onClusterMouseOver = function(_) {
if (!arguments.length) return onClusterMouseOver;
onClusterMouseOver = _;
return chart;
};
return chart;
}
my sample data looks like this
var data = {
"data1": {
Headers: ["Total", "Col 1A", "Col 2A", "Col 3A", "Col 4A"],
Total: ["Total # of Widgets", 1200],
Clusters: [
[100, 1200],
[67, 800],
[42, 500],
[17, 198]
]
},
"data2": {
Headers: ["Total", "Col 1B", "Col 2B", "Col 3B", "Col 4B"],
Total: ["Total # of Widgets", 1200],
Clusters: [
[20, 245],
[31, 371],
[32, 386],
[12, 145]
]
}
}
Thanks!!
There are a couple of issues with your enter/update/exit pattern:
background = svgEnter.append("g")
.attr('class', 'background');
var headers = svgEnter.append("g")
.attr('class', 'headers')
.selectAll("text.header")
.data(data.Headers, function(d){return d;});
var total = svgEnter.append("g")
.attr('class', 'total');
This code is referencing svgEnter, which is OK the first time through, as svgEnter has a non-empty selection (it contains the svg you create a little earlier).
On subsequent calls to this function, svgEnter will contain an empty selection, as the svg element already exists. So, I have modified this part of your code to:
svgEnter.append('g')
.attr('class', 'background');
var background = svg.selectAll('g.background');
svgEnter.append('g')
.attr('class', 'headers')
var headers = svg.selectAll('g.headers').selectAll('text.header')
.data(data.Headers, function(d) { return d; });
svgEnter.append('g')
.attr('class', 'total');
var total = svg.selectAll('g.total');
This will create the g elements if we also have to create the svg element. It will then create the variables similar to your existing code using selections from the svg element.
I think that they're the only changes I made, the rest of your code works as expected.
An updated fiddle is at http://jsfiddle.net/vba6n4sh/9/