How to resize rectangle in d3 js - d3.js

Right now I am able to resize a circle. I have created a rectangle using g.append('svg:rect') but I am not sure how to resize a rectangle in d3
This is what I have tried:
var boxWidth = 1300;
var boxHeight = 600;
var box = d3.select('body')
.append('svg')
.attr('class', 'box')
.attr('width', boxWidth)
.attr('height', boxHeight);
var drag = d3.behavior.drag()
.on('drag', function () {
g.selectAll('*')
.attr('cx', d3.event.x)
.attr('cy', d3.event.y);
});
var resize = d3.behavior.drag()
.on('drag', function () {
g.selectAll('.resizingContainer')
.attr('r', function (c) {
return Math.pow(Math.pow(this.attributes.cx.value - d3.event.x, 2) + Math.pow(this.attributes.cy.value - d3.event.y, 2), 0.5) + 6;
});
g.selectAll('.draggableCircle')
.attr('r', function (c) {
return Math.pow(Math.pow(this.attributes.cx.value - d3.event.x, 2) + Math.pow(this.attributes.cy.value - d3.event.y, 2), 0.5);
});
});
var g = box.selectAll('.draggableGroup')
.data([{
x: 65,
y: 55,
r: 25
}])
.enter()
.append('g');
g.append('svg:circle')
.attr('class', 'resizingContainer')
.attr('cx', function (d) {
return d.x;
})
.attr('cy', function (d) {
return d.y;
})
.attr('r', function (d) {
return d.r + 6;
})
.style('fill', '#999')
.call(resize);
g.append('svg:circle')
.attr('class', 'draggableCircle')
.attr('cx', function (d) {
return d.x;
})
.attr('cy', function (d) {
return d.y;
})
.attr('r', function (d) {
return d.r;
})
.call(drag)
.style('fill', 'black');
g.append('svg:rect')
.attr("width", 70)
.attr("height", 70)
.attr("x", 30)
.attr("y", 130)
.attr("rx", 6)
.attr("ry", 6)
.style("fill", d3.scale.category20c());
html:
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js" charset="utf-8"></script>
<script src='d3.js' charset='utf-8'></script>
<style>
.box {
border: 1px black;
border-radius: 10px;
}
.resizingContainer {
cursor: nesw-resize;
}
</style>
</head>
<body>
<script src='drag.js'></script>
<div id="checks">
X-axis:<input type="checkbox" id="xChecked" checked/>
Y-axis:<input type="checkbox" id="yChecked" checked/>
</div>
</body>
</html>
Here's the live demo: https://jsbin.com/dejewumali/edit?html,js,output

Added rectangle resize for your code. Note that you need to use the bottom right corner for resizing (that was the easiest corner to add the resizing to :-))
var boxWidth = 1300;
var boxHeight = 600;
var box =
d3.select('body')
.append('svg')
.attr('class', 'box')
.attr('width', boxWidth)
.attr('height', boxHeight);
var drag = d3.behavior.drag()
.on('drag', function () {
g.selectAll('*')
.attr('cx', d3.event.x)
.attr('cy', d3.event.y);
});
var resize = d3.behavior.drag()
.on('drag', function () {
g.selectAll('.resizingContainer')
.attr('r', function (c) {
return Math.pow(Math.pow(this.attributes.cx.value - d3.event.x, 2) + Math.pow(this.attributes.cy.value - d3.event.y, 2), 0.5) + 6;
});
g.selectAll('.circle')
.attr('r', function (c) {
return Math.pow(Math.pow(this.attributes.cx.value - d3.event.x, 2) + Math.pow(this.attributes.cy.value - d3.event.y, 2), 0.5);
});
});
var g = box.selectAll('.draggableCircle')
.data([{
x: 65,
y: 55,
r: 25
}])
.enter()
.append('g')
.attr('class', 'draggableCircle');
g.append('svg:circle')
.attr('class', 'resizingContainer')
.attr('cx', function (d) {
return d.x;
})
.attr('cy', function (d) {
return d.y;
})
.attr('r', function (d) {
return d.r + 6;
})
.style('fill', '#999')
.call(resize);
g.append('svg:circle')
.attr('class', 'circle')
.attr('cx', function (d) {
return d.x;
})
.attr('cy', function (d) {
return d.y;
})
.attr('r', function (d) {
return d.r;
})
.call(drag)
.style('fill', 'black');
var distance = function (p1, p2) {
return Math.pow(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2), 0.5);
}
var resize2 = d3.behavior.drag()
.on('drag', function () {
var c = g2.selectAll('.resizingSquare');
var s = g2.selectAll('.square');
var e = d3.event;
var x = Number(this.attributes.x.value);
var y = Number(this.attributes.y.value);
var w = Number(this.attributes.width.value);
var h = Number(this.attributes.height.value);
var c1 = { x: x, y: y };
var c2 = { x: x + w, y: y };
var c3 = { x: x + w, y: y + h };
var c4 = { x: x, y: y + h };
// figure out which corner this is closest to
var d = []
var m1 = distance(e, c1)
var m2 = distance(e, c2)
var m3 = distance(e, c3)
var m4 = distance(e, c4)
switch (Math.min(m1, m2, m3, m4)) {
case m3:
c
.attr('width', function () { return w + (e.x - c3.x) + 12 })
.attr('height', function () { return h + (e.y - c3.y) + 12 })
s
.attr('width', function () { return w + (e.x - c3.x) })
.attr('height', function () { return h + (e.y - c3.y) })
break;
}
});
var g2 = box.selectAll('.draggableSquare')
.data([{
x: 65,
y: 155,
width: 70,
height: 70
}])
.enter()
.append('g')
.attr('class', 'draggableSquare');
g2
.append('svg:rect')
.attr('class', 'resizingSquare')
.attr("width", function (d) {
return d.width + 12;
})
.attr("height", function (d) {
return d.height + 12;
})
.attr("x", function (d) {
return d.x - 6;
})
.attr("y", function (d) {
return d.y - 6;
})
.attr("rx", 6)
.attr("ry", 6)
.style("fill", '#999')
.call(resize2);
g2
.append('svg:rect')
.attr('class', 'square')
.attr("width", function (d) {
return d.width;
})
.attr("height", function (d) {
return d.height;
})
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y;
})
.attr("rx", 6)
.attr("ry", 6)
.style("fill", d3.scale.category20c());
JS Bin - https://jsbin.com/zenomoziso/1/edit
That said, if you are looking to use this beyond a proof of concept it's going to be very difficult. There are several problems with the above that will manifest once you have more elements
The container (g or g2) being used is a global variable.
The code is clunky (I just tacked on most of it based on the watches - there could be far more efficient ways of doing the same thing - e.g. you could not the start position onDragStart and use that to calculate the change in dimensions)
The code could be cleaner (think objects, better naming conventions, etc.). You might just want do d3.data... squares({ resize: true, move: true }) in your main block instead of all the individual steps.
You'll be better off searching for some existing diagramming library (why do all the math, when it's already done and tested :-)) - I found a blog with the canvas variant here - http://simonsarris.com/blog/510-making-html5-canvas-useful

Rectangle size is based on the width and height attributes. Thus to resize the rectangle you'll want to use something of this ilk:
d3.selectAll('rect')
.attr('width', function(c){
return d3.event.x - this.attributes.x.value;
}).attr('height', function(c){
return d3.event.y - this.attributes.y.value;
});
Inserted into your resize behaviour. If you do this and click to drag the circle and move to the origin of the rectangle, it'll then expand linearly with your cursor. It will likely work with little modification if the drag event was attached to the rectangle. Here is a simple application that showcases dragging and re-sizing of a rectangle.
For more reliability you'd want to select by a class identifier rather than a global rectangle select, but this is the basic idea.

Related

Having trouble with D3v4 update pattern with 3 levels of nested data

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>

How to Drag line by its end points using rectangle as end points error?

I am new to d3.js and please forgive me if this sounds like a naive question. I have plotted a line (d3 v4) which can be draggable by its end points. The end points are rectangle.
The current output looks as below :
This is how it looks
The challenge that i am facing is - when i start dragging the point, the line seems to take its origin from the top left corner. When i drag the second point of the same line, the line drags / moves as expected.
The sample data looks as below :
The sample data
Requesting your suggestions / inputs on how to fix the above issue.
Below is the attached code that i am using :
var margin = { top: 0, right: 0, bottom: 0, left: 0 },
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
// Creating the colour Category
var color = d3.scaleOrdinal(d3.schemeCategory10);
var y = d3.scaleLinear().range([390, 0]);
// Scale the range of the data
y.domain([0, d3.max(data, function (d) { return Math.max(d.nonpromotedprice, d.promotedprice)*1.2; })]).nice();
// Line for the 1st Block
var lines = svg.selectAll("line")
.data(data)
.enter()
.append('line')// attach a line
.style("stroke", "#E6EAEE")
.style("stroke-width", 8) // colour the line
.attr("x1", function (d) { return d.x_value; }) // x position of the first end of the line
.attr("y1", function (d) { return y(d.nonpromotedprice); }) // y position of the first end of the line
.attr("x2", function (d) { return d.x_value; }) // x position of the second end of the line
.attr("y2", function (d) { return y(d.promotedprice); });
// Add the Y Axis
svg.append("g")
.attr("class", "grid")
.attr("fill", "lightgrey")
.attr("stroke-width", 0.7)
.attr("stroke-opacity", 0.2)
.call(d3.axisLeft(y)
.tickSize(-400)
.tickFormat(""));
var topEndPoints = data.map(function (line, i) {
return {
'x': line.x_value,
'y': line.nonpromotedprice,
'marker': 'marker-start',
'lineIndex': i
};
});
var bottomEndPoints = data.map(function (line, i) {
return {
'x': line.x_value,
'y': line.promotedprice,
'marker': 'marker-end',
'lineIndex': i
};
});
var MiddleEndPoints = data.map(function (line, i) {
return {
'x': line.x_value,
'y': line.avgprice,
'marker': 'marker-middle',
'lineIndex': i
};
});
var endPointsData = topEndPoints.concat(bottomEndPoints, MiddleEndPoints);
// Pointer to d3 rectangles
var endPoints = svg
.selectAll('rect')
.data(endPointsData)
.enter()
.append('rect')
.attr("width", 12)
.attr("height", 8)
.attr("x", function (d) { return d.x - 6; })
.attr("y", function (d) { return y(d.y); })
//.attr("cx", function (d) { return d.x; })
//.attr("cy", function (d) { return d.y; })
//.attr('r',7)
.attr("fill", function (d) { return color(d.x); })
.call(d3.drag()
//.origin(function(d) { return y(d.y); })
.subject(function() {
var t = d3.select(this);
return {x: t.attr("x"), y: t.attr("y")};
})
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// draw the logo
svg.selectAll("image")
.data(data)
.enter()
.append("svg:image")
.attr("xlink:href", function (d) { return d.logo; })
//.append("rect")
.attr("x", function (d) { return d.x_value - 13; })
.attr("y", function (d) { return y(d.nonpromotedprice + 35); })
.attr("height", 25)
.attr("width", 25);
function dragstarted() {
d3.select(this).classed("active", true).attr('y', d.y = y(d3.event.y));
}
function dragged(d, i) {
var marker = d3.select(this);
// Update the marker properties
marker
//.attr('cx', d.x = d3.event.x)
.attr('y', d.y = d3.event.y);
// Update the line properties
lines
.filter(function (lineData, lineIndex) {
return lineIndex === d.lineIndex;
})
.attr('x1', function (lineData) {
return d.marker === 'marker-start' ? lineData.x1 = d.x : lineData.x1;
})
.attr('y1', function (lineData) {
return d.marker === 'marker-start' ? lineData.y1 = d.y : lineData.y1;
})
.attr('x2', function (lineData) {
return d.marker === 'marker-end' ? lineData.x2 = d.x : lineData.x2;
})
.attr('y2', function (lineData) {
return d.marker === 'marker-end' ? lineData.y2 = d.y : lineData.y2;
});
}
function dragended() {
d3.select(this).classed("active", false);
Shiny.setInputValue("pricechanged",
{price: (d3.max(data, function (d) { return Math.max(d.nonpromotedprice, d.promotedprice); }) -(d3.event.y / 390)* d3.max(data, function (d) { return Math.max(d.nonpromotedprice, d.promotedprice); }))*1.19},
{priority: "event"}
);
}

How to add updating labels in D3 Vertical Bar Chart in an Ember Application

I have a vertical bar chart in my Ember application and I am struggling to attach text labels to the top of the bars.
The chart is broken up into the following functions:
Drawing the static elements of the chart:
didInsertElement() {
let svg = select(this.$('svg')[0]);
this.set('svg', svg);
let height = 325
let width = 800
let padding = {
top: 10,
bottom: 30,
left: 40,
right: 0
};
this.set('barsContainer', svg.append('g')
.attr('class', 'bars')
.attr('transform', `translate(${padding.left}, ${padding.top})`)
);
let barsHeight = height - padding.top - padding.bottom;
this.set('barsHeight', barsHeight);
let barsWidth = width - padding.left - padding.right;
// Y scale & axes
let yScale = scaleLinear().range([barsHeight, 0]);
this.set('yScale', yScale);
this.set('yAxis', axisLeft(yScale));
this.set('yAxisContainer', svg.append('g')
.attr('class', 'axis axis--y axisWhite')
.attr('transform', `translate(${padding.left}, ${padding.top})`)
);
// X scale & axes
let xScale = scaleBand().range([0, barsWidth]).paddingInner(0.15);
this.set('xScale', xScale);
this.set('xAxis', axisBottom(xScale));
this.set('xAxisContainer', svg.append('g')
.attr('class', 'axis axis--x axisWhite')
.attr('transform', `translate(${padding.left}, ${padding.top + barsHeight})`)
);
// Color scale
this.set('colorScale', scaleLinear().range(COLORS[this.get('color')]));
this.renderChart();
this.set('didRenderChart', true);
},
This re-draws the chart when the model changes:
didUpdateAttrs() {
this.renderChart();
},
This handles the drawing of the chart:
renderChart() {
let data = this.get('data');
let counts = data.map(data => data.count);
// Update the scales
this.get('yScale').domain([0, Math.max(...counts)]);
this.get('colorScale').domain([0, Math.max(...counts)]);
this.get('xScale').domain(data.map(data => data.label));
// Update the axes
this.get('xAxis').scale(this.get('xScale'));
this.get('xAxisContainer').call(this.get('xAxis')).selectAll('text').attr("y", 0)
.attr("x", 9)
.attr("dy", ".35em")
.attr("transform", "rotate(40)")
.style("text-anchor", "start");
this.get('yAxis').scale(this.get('yScale'));
this.get('yAxisContainer').call(this.get('yAxis'));
let barsUpdate = this.get('barsContainer').selectAll('rect').data(data, data => data.label);
// Enter
let barsEnter = barsUpdate.enter()
.append('rect')
.attr('opacity', 0);
let barsExit = barsUpdate.exit();
let div = select('body')
.append("div")
.attr("class", "vert-tooltip");
// Update
let rafId;
barsEnter
.merge(barsUpdate)
.transition()
.attr('width', `${this.get('xScale').bandwidth()}px`)
.attr('height', data => `${this.get('barsHeight') - this.get('yScale')(data.count)}px`)
.attr('x', data => `${this.get('xScale')(data.label)}px`)
.attr('y', data => `${this.get('yScale')(data.count)}px`)
.attr('fill', data => this.get('colorScale')(data.count))
.attr('opacity', data => {
let selected = this.get('selectedLabel');
return (selected && data.label !== selected) ? '0.5' : '1.0';
})
.on('start', (data, index) => {
if (index === 0) {
(function updateTether() {
Tether.position()
rafId = requestAnimationFrame(updateTether);
})();
}
})
.on('end interrupt', (data, index) => {
if (index === 0) {
cancelAnimationFrame(rafId);
}
});
// Exit
barsExit
.transition()
.attr('opacity', 0)
.remove();
}
I have stripped some tooltip and click events to maintain clarity.
To add the labels I have tried to add the following in the renderChart() function:
barsEnter.selectAll("text")
.data(data)
.enter()
.append("text")
.text(function (d) { return d.count; })
.attr("x", function (d) { return xScale(d.label) + xScale.bandwidth() / 2; })
.attr("y", function (d) { return yScale(d.count) + 12; })
.style("fill", "white");
with the above code I receive an error to say that xScale and yScale are not found because they are not within this functions scope. If I use:
.attr("x", function (d) { return this.get('xScale')(d.label) + this.get('xScale').bandwidth() / 2; })
.attr("y", function (d) { return this.get('yScale')(d.count) + 12; })
I generate 'this.get' is not a function errors and the context of 'this' becomes the an object with the value of (d).
If I add the X and Y scales as variables to this function like:
let xScale = this.get('xScale')
let yScale = this.get('ySCale')
...
.attr("x", function (d) { return xScale(d.label) + xScale.bandwidth() / 2; })
.attr("y", function (d) { return yScale(d.count) + 12; })
Then the x and y attrs are returned as undefined. Please let me know if I have missed anything out.
Converting the function() {} syntax into arrow functions will allow you to maintain the this.
So:
function(d) { return this.get('xScale'); }
becomes
(d) => this.get('xScale')
or
(d) => {
return this.get('xScale');
}
For more information on arrow functions:
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
- https://hackernoon.com/javascript-es6-arrow-functions-and-lexical-this-f2a3e2a5e8c4

d3.js: why doesn't the data update?

I've got a fairly simple reusable chart built in D3.js -- some circles and some text.
I'm struggling to figure out how to update the chart with new data, without redrawing the entire chart.
With the current script, I can see that the new data is bound to the svg element, but none of the data-driven text or attributes is updating. Why isn't the chart updating?
Here's a fiddle: http://jsfiddle.net/rolfsf/em5kL/1/
I'm calling the chart like this:
d3.select('#clusters')
.datum({
Name: 'Total Widgets',
Value: 224,
Clusters: [
['Other', 45],
['FooBars', 30],
['Foos', 50],
['Bars', 124],
['BarFoos', 0]
]
})
.call( clusterChart() );
When the button is clicked, I'm simply calling the chart again, with different data:
$("#doSomething").on("click", function(){
d3.select('#clusters')
.datum({
Name: 'Total Widgets',
Value: 122,
Clusters: [
['Other', 14],
['FooBars', 60],
['Foos', 22],
['Bars', 100],
['BarFoos', 5]
]
})
.call( clusterChart() );
});
The chart script:
function clusterChart() {
var width = 450,
margin = 0,
radiusAll = 72,
maxRadius = radiusAll - 5,
r = d3.scale.linear(),
padding = 1,
height = 3 * (radiusAll*2 + padding),
startAngle = Math.PI / 2,
onTotalMouseOver = null,
onTotalClick = null,
onClusterMouseOver = null,
onClusterClick = null;
val = function(d){return d};
function chart(selection) {
selection.each(function(data) {
var cx = width / 2,
cy = height / 2,
stepAngle = 2 * Math.PI / data.Clusters.length,
outerRadius = 2*radiusAll + padding;
r = d3.scale.linear()
.domain([0, d3.max(data.Clusters, function(d){return d[1];})])
.range([50, maxRadius]);
var svg = d3.select(this).selectAll("svg")
.data([data])
.enter().append("svg");
//enter
var totalCircle = svg.append("circle")
.attr("class", "total-cluster")
.attr('cx', cx)
.attr('cy', cy)
.attr('r', radiusAll)
.on('mouseover', onTotalMouseOver)
.on('click', onTotalClick);
var totalName = svg.append("text")
.attr("class", "total-name")
.attr('x', cx)
.attr('y', cy + 16);
var totalValue = svg.append("text")
.attr("class", "total-value")
.attr('x', cx)
.attr('y', cy + 4);
var clusters = svg.selectAll('circle.cluster')
.data(data.Clusters)
.enter().append('circle')
.attr("class", "cluster");
var clusterValues = svg.selectAll("text.cluster-value")
.data(data.Clusters)
.enter().append('text')
.attr('class', 'cluster-value');
var clusterNames = svg.selectAll("text.cluster-name")
.data(data.Clusters)
.enter().append('text')
.attr('class', 'cluster-name');
clusters .attr('cx', function(d, i) { return cx + Math.cos(startAngle + stepAngle * i) * outerRadius; })
.attr('cy', function(d, i) { return cy + Math.sin(startAngle + stepAngle * i) * outerRadius; })
.attr("r", "10")
.on('mouseover', function(d, i, j) {
if (onClusterMouseOver != null) onClusterMouseOver(d, i, j);
})
.on('mouseout', function() { /*do something*/ })
.on('click', function(d, i){ onClusterClick(d); });
clusterNames
.attr('x', function(d, i) { return cx + Math.cos(startAngle + stepAngle * i) * outerRadius; })
.attr('y', function(d, i) { return cy + Math.sin(startAngle + stepAngle * i) * outerRadius + 16; });
clusterValues
.attr('x', function(d, i) { return cx + Math.cos(startAngle + stepAngle * i) * outerRadius; })
.attr('y', function(d, i) { return cy + Math.sin(startAngle + stepAngle * i) * outerRadius - 4; });
//update with data
svg .selectAll('text.total-value')
.text(val(data.Value));
svg .selectAll('text.total-name')
.text(val(data.Name));
clusters
.attr('class', function(d, i) {
if(d[1] === 0){ return 'cluster empty'}
else {return 'cluster'}
})
.attr("r", function (d, i) { return r(d[1]); });
clusterValues
.text(function(d) { return d[1] });
clusterNames
.text(function(d, i) { return d[0] });
$(window).resize(function() {
var w = $('.cluster-chart').width(); //make this more generic
svg.attr("width", w);
svg.attr("height", w * height / width);
});
});
}
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.onClusterClick = function(_) {
if (!arguments.length) return onClusterClick;
onClusterClick = _;
return chart;
};
return chart;
}
I have applied the enter/update/exit pattern across all relevant svg elements (including the svg itself). Here is an example segment:
var clusterValues = svg.selectAll("text.cluster-value")
.data(data.Clusters,function(d){ return d[1];});
clusterValues.exit().remove();
clusterValues
.enter().append('text')
.attr('class', 'cluster-value');
...
Here is a complete FIDDLE with all parts working.
NOTE: I tried to touch your code as little as possible since you have carefully gone about applying a re-usable approach. This the reason why the enter/update/exit pattern is a bit different between the total circle (and text), and the other circles (and text). I might have gone about this using a svg:g element to group each circle and associated text.

nextAll selector, transform elements after clicked element

JSFiddle: http://jsfiddle.net/kKvtJ/2/
Right now the groups are 20px wide. When clicked, I want the selected group to expand to 40px wide, with the groups to the right shifting over 20px more.
Current:
Expected:
Can I can set a transform on all the groups like this? I couldn't figure this out.
var clicked_index = 3; // how to get currently clicked `g` index?
d3.selectAll('g')
.attr('transform',function(d,i){ return 'translate('+(i>clicked_index?40:0)+',0)' });
I have marked what I want to accomplish in the code below, in // pseudocode.
JSFiddle: http://jsfiddle.net/kKvtJ/2/
code
var data = [13, 11, 10, 8, 6];
var width = 200;
var height = 200;
var chart_svg = d3.select("#chart")
.append("svg")
.append("g");
y_scale = d3.scale.linear().domain([0, 15]).range([200, 0]);
h_scale = d3.scale.linear().domain([0, 15]).range([0,200]);
x_scale = d3.scale.linear().domain([0, 10]).range([0, 200]);
var nodes = chart_svg.selectAll('g').data(data);
var nodes_enter = nodes.enter().append('g')
.attr('transform', function (d, i) {
return 'translate(' + (i * 30) + ',0)'
})
.attr('fill', d3.rgb('#3f974e'));
nodes_enter.on('click', function() {
d3.selectAll('line')
.attr('opacity',0);
d3.selectAll('text')
.style('fill','white')
.attr('x',0);
d3.select(this).select('line')
.attr('opacity',1);
d3.select(this).selectAll('text')
.style('fill','black')
.attr('x',40);
// pseudocode
// d3.select(this).nextAll('g')
// .attr('transform','translate(20,0)');
});
nodes_enter.append('rect')
.attr('y', function (d) { return y_scale(d) })
.attr('height', function (d) { return h_scale(d) })
.attr('width', 20);
nodes_enter.append('text')
.text(function (d) { return d })
.attr('y', function (d) { return y_scale(d) + 16 })
.style('fill', 'white');
nodes_enter.append('line')
.attr('x1', 0)
.attr('y1', function(d) { return y_scale(d) })
.attr('x2', 40)
.attr('y2', function(d) { return y_scale(d) })
.attr('stroke-width', 1)
.attr('stroke','black')
.attr('opacity', 0);
You can do this by selecting all the g elements, shifting them if the respective index is larger than the one of the bar you clicked on, and selecting all the rect elements and adjusting the width depending on whether the index is the one you clicked on. Updated jsfiddle here, relevant code below. Note that I assigned the class "bar" to the relevant g elements to be able to distinguish them from the others.
nodes_enter.on('click', function(d, i) {
d3.selectAll("g.bar")
.attr('transform', function (e, j) {
return 'translate(' + (j * 30 + (j > i ? 20 : 0)) + ',0)';
});
d3.selectAll("g.bar > rect")
.attr("width", function(e, j) { return j == i ? 40 : 20; });
});

Resources