Draw properly sized rectangle around text - javascript

I'm trying to draw a popup tooltip on hover when the user mouses over a pie chart. The following works, but the problem is the rectangle is a fixed size. How might one go about making the rectangle fit the size of the actual text?
// Draw rectangle
popup
.append("rect")
.attr("x", x + 5)
.attr("y", y - 5)
.attr("width", width)
.attr("height", height)
.attr("rx", 5)
.attr("ry", 5)
.style("fill", popupFillColor)
.style("stroke", stroke)
.style("stroke-width", 2);
// add text
popup
.append("text")
.attr("x", x + 10)
.attr("y", y + 10)
.text(e.seriesValue[0] + `: ${e.pValue} (${percent})`)
.style("font-family", "sans-serif")
.style("font-size", 14)
.style("fill", stroke);

Use getBBox:
const x = 50, y = 30;
const svg = d3.select('svg');
const rect = d3.select('svg')
.append('rect')
.style('fill', 'none')
.style('stroke', 'black');
const text = svg.append('text')
.text('This is my text')
.attr('x', x)
.attr('y', y)
const box = text.node().getBBox();
console.log(box)
const X_MARGIN = 20;
const Y_MARGIN = 10;
rect
.attr('x', box.x - X_MARGIN)
.attr('y', box.y - Y_MARGIN)
.attr('width', box.width + X_MARGIN * 2)
.attr('height', box.height + Y_MARGIN * 2)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>

Related

D3 Bar Chart Zoom Not Centered

I have a bar chart with zoom function. The issue is, the zooming isn't actually centered. If, I place the cursor, on a bar and zoom, the bar underneath the cursor moves away as opposed to staying there, However, if I set the MARGIN.LEFT = 0, then the issue is rectified and No matter what bar I have my cursor on, when I zoom the bar stays there, right underneath. Could anyone help me with this?
Working Code Here: https://codesandbox.io/s/d3-zoom-not-centered-sfziyk
D3 Code:
const MARGIN = {
LEFT: 60,
RIGHT: 40,
TOP: 10,
BOTTOM: 130
};
// total width incl margin
const VIEWPORT_WIDTH = 1140;
// total height incl margin
const VIEWPORT_HEIGHT = 400;
const WIDTH = VIEWPORT_WIDTH - MARGIN.LEFT - MARGIN.RIGHT;
const HEIGHT = VIEWPORT_HEIGHT - MARGIN.TOP - MARGIN.BOTTOM;
const svg = d3
.select(".chart-container")
.append("svg")
.attr("width", WIDTH + MARGIN.LEFT + MARGIN.RIGHT)
.attr("height", HEIGHT + MARGIN.TOP + MARGIN.BOTTOM);
const g = svg
.append("g")
.attr("transform", `translate(${MARGIN.LEFT}, ${MARGIN.TOP})`);
g.append("text")
.attr("class", "x axis-label")
.attr("x", WIDTH / 2)
.attr("y", HEIGHT + 110)
.attr("font-size", "20px")
.attr("text-anchor", "middle")
.text("Month");
g.append("text")
.attr("class", "y axis-label")
.attr("x", -(HEIGHT / 2))
.attr("y", -60)
.attr("font-size", "20px")
.attr("text-anchor", "middle")
.attr("transform", "rotate(-90)")
.text("");
const zoom = d3.zoom().scaleExtent([0.5, 10]).on("zoom", zoomed);
svg.call(zoom);
function zoomed(event) {
x.range([0, WIDTH].map((d) => event.transform.applyX(d)));
barsGroup
.selectAll("rect.profit")
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth());
barsGroup
.selectAll("rect.revenue")
.attr("x", (d) => x(d.month) + 0.5 * x.bandwidth())
.attr("width", 0.5 * x.bandwidth());
xAxisGroup.call(xAxisCall);
}
const x = d3.scaleBand().range([0, WIDTH]).paddingInner(0.3).paddingOuter(0.2);
const y = d3.scaleLinear().range([HEIGHT, 0]);
const xAxisGroup = g
.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0, ${HEIGHT})`);
const yAxisGroup = g.append("g").attr("class", "y axis");
const xAxisCall = d3.axisBottom(x);
const yAxisCall = d3
.axisLeft(y)
.ticks(3)
.tickFormat((d) => "$" + d);
const defs = svg.append("defs");
const barsClipPath = defs
.append("clipPath")
.attr("id", "bars-clip-path")
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", WIDTH)
.attr("height", 400);
const barsGroup = g.append("g");
const zoomGroup = barsGroup.append("g");
barsGroup.attr("class", "bars");
zoomGroup.attr("class", "zoom");
barsGroup.attr("clip-path", "url(#bars-clip-path)");
xAxisGroup.attr("clip-path", "url(#bars-clip-path)");
d3.csv("data.csv").then((data) => {
data.forEach((d) => {
d.profit = Number(d.profit);
d.revenue = Number(d.revenue);
d.month = d.month;
});
var y0 = d3.max(data, (d) => d.profit);
var y1 = d3.max(data, (d) => d.revenue);
var maxdomain = y1;
if (y0 > y1) var maxdomain = y0;
x.domain(data.map((d) => d.month));
y.domain([0, maxdomain]);
xAxisGroup
.call(xAxisCall)
.selectAll("text")
.attr("y", "10")
.attr("x", "-5")
.attr("text-anchor", "end")
.attr("transform", "rotate(-40)");
yAxisGroup.call(yAxisCall);
const rects = zoomGroup.selectAll("rect").data(data);
rects.exit().remove();
rects
.attr("y", (d) => y(d.profit))
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.profit));
rects
.enter()
.append("rect")
.attr("class", "profit")
.attr("y", (d) => y(d.profit))
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.profit))
.attr("fill", "grey");
const rects_revenue = zoomGroup.selectAll("rect.revenue").data(data);
rects_revenue.exit().remove();
rects_revenue
.attr("y", (d) => y(d.revenue))
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.revenue));
rects_revenue
.enter()
.append("rect")
.attr("class", "revenue")
.style("fill", "red")
.attr("y", (d) => y(d.revenue))
.attr("x", (d) => x(d.month) + 0.5 * x.bandwidth())
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.revenue))
.attr("fill", "grey");
});
When you call the zoom on the svg, all zoom behaviour is relative to the svg.
Imagine that your x-axis is at initial zoom level of length 100 representing the domain [0, 100]. So the x-scale has range([0, 100]) and domain([0, 100]). Add a left margin of 10.
If you zoom by scale 2 at the midpoint of your axis at x=50 you would expect to get the following behaviour after the zoom:
The midpoint does not move.
The interval [25, 75] is visible.
However, since the zoom is called on the svg you have to account for the left margin of 10. The zoom does not occur at the midpoint but at x = 10 + 50 = 60. The transform is thus x -> x * k + t with k = 2 and t = -60. This results in
x = 50 -> 2 * 50 - 60 = 40,
x = 80 -> 2 * 80 - 60 = 100,
x = 30 -> 2 * 30 - 60 = 0.
Visible after the zoom is the interval [30, 80] and the point x = 50 is shifted to the left.
This is what you observe in your chart.
In order to get the expected behaviour, you can do two things:
a. Follow the bar chart example where the range of the x-scale does not start at 0 but at the left margin. The g which is translated by margin.left and margin.top is also omitted here. Instead, the ranges of the axes incorporate the margins directly.
b. Add a rect with fill: none; pointer-events: all; to the svg that is of the size of the chart without the margins. Then call the zoom on that rectangle, as done in this example.
Note that all the new examples on ObservableHQ follow the pattern "a" that needs fewer markup.

How to create different colour shades to column bar in d3 [duplicate]

I have created a D3 bar chart, which takes in my data, applies a scale and then draws my rectangles. Here's my current output:
I would like to make it more visual. The values are temperatures, so I'd like to create something like the first bar here:
Basically I want to make a bar that is made up of many rectangles of different colors.
After considering several options, I decided to try to inject a pattern into my bars. I tried making my pattern from paths, and then I attempted rectangles, but no success.
I'm not tied to the 'pattern' approach, so an different approach, or how I can make this pattern, is what I'm after. Here's my code:
// Set variables.
var dataset = // an array of objects, each with two properties: location and temperature
w = 500,
h = 800,
xScale = d3.scale.linear()
.domain([-30, 40])
.range([5, 350]),
yScale = d3.scale.ordinal();
// Create SVG element.
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
/* svg.append("defs")
.append("pattern")
.attr("id", "heatHatch")
.attr("patternUnits", "userSpaceOnUse")
.attr("width", 350)
.attr("height", 25);
.append("path")
.attr("d", "M10 0 L10 20 L0 20 L0 0 Z M22 0 L22 20 L12 20 L12 0 Z")
.attr("fill", "pink")
.append("path")
.attr("d", "M22 0 L22 20 L12 20 L12 0 Z")
.attr("fill", "red");
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 10)
.attr("height", 20)
.attr("fill", "pink"); */
// Create bars.
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", 0)
.attr("y", function(d, i) {
return i * 25;
})
.attr("width", function(d) {
return xScale(d[1]);
})
.attr("height", 20);
/*.attr("fill", "url(#heatHatch)"); */
Thanks.
If you want pattern that scales to the bar size, use a gradient pattern:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#3.5.3" data-semver="3.5.3" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
</head>
<body>
<script>
var w = 500,
h = 500;
var svg = d3.select("body").append("svg:svg")
.attr("width", w)
.attr("height", h);
var color = d3.scale.category10();
var gradient = svg.append("svg:defs")
.append("svg:linearGradient")
.attr("id", "gradient")
.attr("x1", "0%")
.attr("y1", "50%")
.attr("x2", "100%")
.attr("y2", "50%")
.attr("spreadMethod", "pad");
d3.range(10).forEach(function(d, i) {
var c = color(i);
gradient.append("svg:stop")
.attr("offset", (i * 10) + "%")
.attr("stop-color", c)
.attr("stop-opacity", 1);
gradient.append("svg:stop")
.attr("offset", ((i + 1) * 10) + "%")
.attr("stop-color", c)
.attr("stop-opacity", 1);
});
svg.selectAll("bar")
.data(d3.range(10))
.enter()
.append("rect")
.attr("class", "bar")
.attr("width", function(d,i){
return (w / 10) * i;
})
.attr("height", (h / 10) - 10)
.attr("y", function(d,i){
return (h / 10) * i;
})
.style("fill", "url(#gradient)");
</script>
</body>
</html>
If you want a pattern with statically sized colors, create a pattern of rects:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#3.5.3" data-semver="3.5.3" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
</head>
<body>
<script>
var w = 500,
h = 500;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h);
var color = d3.scale.category10();
var pattern = svg.append("svg:defs")
.append("svg:pattern")
.attr("width", w)
.attr("height", 4)
.attr("id", "myPattern")
.attr("patternUnits","userSpaceOnUse");
d3.range(10).forEach(function(d, i) {
var c = color(i);
pattern.append("rect")
.attr("height", "4px")
.attr("width", w / 10)
.attr("x", (w / 10) * i)
.attr("fill", c);
});
svg.selectAll("bar")
.data(d3.range(10))
.enter()
.append("rect")
.attr("class","bar")
.attr("width", function(d,i){
return (w / 10) * i;
})
.attr("height", (h / 10) - 10)
.attr("y", function(d,i){
return (h / 10) * i;
})
.style("fill", "url(#myPattern)");
</script>
</body>
</html>

D3.JS add zoom and listitems to rect

I want to achieve followings,
1 - Create a given column matrix with rectangles with provided colours
Done
2 - Make this matrix zoom able
3 - Add list items to each rectangle which will only show numbers of list items in it if completely zoomed out and on zoom in, it will show the list items e.g. there Titles.
Now I want to achieve Number 2 here, this is what I am trying,
http://jsfiddle.net/nhe613kt/25/
When I add code for zooming it fails,
var svgContainer = d3.select("body").append("svg")
.attr("width", 300)
.attr("height", 300)
.style("background-color", "black");
var zoomed = function () {
svgContainer.attr("transform", "translate("+ d3.event.translate + ")
scale(" + d3.event.scale + ")");
};
var zoom = d3.behavior.zoom()
.scaleExtent([1, 8])
.on("zoom", zoomed);
.size([width, height]);
var rectangle1 = svgContainer.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 100)
.attr("height", 100)
.attr("fill", "red")
.call(zoom);;
var rectangle2 = svgContainer.append("rect")
.attr("x", 100)
.attr("y", 0)
.attr("width", 100)
.attr("height", 100)
.attr("fill", "yellow");
var rectangle3 = svgContainer.append("rect")
.attr("x", 200)
.attr("y", 0)
.attr("width", 100)
.attr("height", 100)
.attr("fill", "red");
var rectangle4 = svgContainer.append("rect")
.attr("x", 0)
.attr("y", 100)
.attr("width", 100)
.attr("height", 100)
.attr("fill", "yellow");
var rectangle5 = svgContainer.append("rect")
.attr("x", 100)
.attr("y", 100)
.attr("width", 100)
.attr("height", 100)
.attr("fill", "red");
var rectangle6 = svgContainer.append("rect")
.attr("x", 200)
.attr("y", 100)
.attr("width", 100)
.attr("height", 100)
.attr("fill", "yellow");
var rectangle7 = svgContainer.append("rect")
.attr("x", 0)
.attr("y", 200)
.attr("width", 100)
.attr("height", 100)
.attr("fill", "red");
var rectangle8 = svgContainer.append("rect")
.attr("x", 100)
.attr("y", 200)
.attr("width", 100)
.attr("height", 100)
.attr("fill", "yellow");
var rectangle9 = svgContainer.append("rect")
.attr("x", 200)
.attr("y", 200)
.attr("width", 100)
.attr("height", 100)
.attr("fill", "red");
My desired result will be this,
http://bl.ocks.org/mbostock/3680957
The code you provided has several problems:
1. There is a syntax error in definition of zoom (.on("zoom", zoomed);)
2. You haven't defined width and height.
3. zoomed function possibly couldn't be parsed because of wrong line breaks (notice point where you define scale of transformation).
Here is JSFiddle, where zoom works correctly for first element of matrix. Main points is:
// don't forget about width and height
var width = 960,
height = 500;
// make sure that string defining transform attribute is correct. scale isn't a method, but part of string
var zoomed = function () {
svgContainer.attr("transform", "translate("+ d3.event.translate + ")scale(" + d3.event.scale + ")");
};
// don't place semicolon after on("zoom", zoomed)
var zoom = d3.behavior.zoom()
.scaleExtent([1, 8])
.on("zoom", zoomed)
.size([width, height]);
// add zoom behaviour to desired rectangle
var rectangle1 = svgContainer.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 100)
.attr("height", 100)
.attr("fill", "red")
.call(zoom);

Chart not updating with new data with transitions in D3

Trying to do a simple replace data/transition with D3 but it's not updating. I'm not getting an error and don't see anything odd when debugging. I feel like i'm missing something super simple and just overlooking. The more eyes, the better!
Here's my D3 code:
var labels = ['Opens', 'Clicks', 'Unsubscribe'],
data = [4, 8, 15],
chart,
x,
y,
gap = 2,
width = 450,
leftWidth = 100,
barHeight = 40,
barHeightInner = (barHeight / 2),
barTopMargin = (barHeight - barHeightInner) / 2,
height = (barHeight + gap * 2) * labels.length + 30;
/* SET SVG + LEFT MARGIN */
chart = d3.select($("#graphArea")[0])
.append('svg')
.attr('class', 'chart')
.attr('width', leftWidth + width + 20)
.attr('height', height)
.append("g")
.attr("transform", "translate(10, 20");
x = d3.scale.linear()
.domain([0, d3.max(data)])
.range([0, width]);
y = d3.scale.ordinal()
.domain(data)
.rangeBands([0, (barHeight + 2 * gap) * labels.length], 0.05);
/* CREATE BARS */
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("x", leftWidth)
.attr("y", y)
.attr("width", x)
.attr("height", y.rangeBand())
.attr('class', 'bar-outer');
/* INNER BARS */
chart.selectAll("rect.inner")
.data(data)
.enter().append("rect")
.attr("x", leftWidth)
.attr("y", function(d){return y(d) + barTopMargin} )
.attr("width", function(d){return x(d)/2;})
.attr("height", barHeightInner)
.attr('class', 'bar-inner');
/* SET NAMED LABELS */
chart.selectAll("text.label")
.data(labels)
.enter().append("text")
.attr("x", leftWidth / 2 + 40)
.attr("y", function(d,i) {return i * y.rangeBand() + 20;})
.attr("dy", ".35em")
.attr("text-anchor", "end")
.attr('class', 'label')
.text(String);
function update(data) {
data = [20, 10, 3];
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("x", leftWidth)
.attr("y", y)
.attr("width", x)
.attr("height", y.rangeBand())
.attr('class', 'bar-outer');
chart.selectAll("rect")
.data(data)
.exit()
.transition()
.duration(300)
.ease("exp")
.attr('width', 0)
.remove();
chart.selectAll("rect")
.data(data)
.transition()
.duration(300)
.ease("quad")
.attr("width", x)
.attr("height", y.rangeBand())
.attr("transform", function(d,i) { return "translate(" + [0, y(i)] + ")"});
It's kind of messy, sorry... but should be readable at least. I'm not sure why when update(data) it's not changing. Any advice would be greatly appreciated!
The main problems were with the application of the enter/update/exit pattern and also with forgetting to re-set the scale domains according to the new data. Here is the segment of interest:
function update() {
data = [20, 10, 3];
// must re-set scale domains with the new data
x.domain([0, d3.max(data)]);
y.domain(data);
var outer = chart.selectAll(".bar-outer")
.data(data);
// exit selection
outer
.exit()
.remove();
// enter selection
outer
.enter()
.append("rect")
.attr('class', 'bar-outer');
// update selection
outer
.transition()
.duration(500)
.ease("quad")
.attr("x", leftWidth)
.attr("y", y)
.attr("width", x)
.attr("height", y.rangeBand());
var inner = chart.selectAll(".bar-inner")
.data(data);
// exit selection
inner
.exit()
.remove();
// enter selection
inner
.enter()
.append("rect")
.attr('class', 'bar-inner');
// update selection
inner
.transition()
.duration(500)
.ease("quad")
.attr("x", leftWidth)
.attr("y", function (d) {
return y(d) + barTopMargin
})
.attr("width", function (d) {
return x(d) / 2;
})
.attr("height", barHeightInner);
};
Here is the complete FIDDLE.

Text blocking circle click method d3js

To start I'm sure there is a much simpler way to do this then what I'm trying to do. I'm trying to zoom in on specific circles using d3js and have text overlaying the circle. The problem is that since the text is ontop of the circle the text blocks the onclick even that is fired when you click on the circle. Here is my code so far:
js
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height)
.append("g");
var node1 = svg.append("g")
.append("circle")
.data([offset[0]])
.text(function(d){return d.label})
.attr("r", 25)
.style("fill","white")
.on("click", clicked);
node1.attr("cx", 530)
.attr("cy", 310)
.transition()
.delay(500)
.duration(1000)
.attr("r", 55)
.attr("cx", 530)
.attr("cy", 205);
d3.select('g').append('text')
.attr("id","orient")
.attr("dx", 510)
.attr("dy", 210)
.attr("width", 90)
.attr("height", 90)
.text(function(d){return offset[0].label});
var node2 = svg.append("g")
.append("circle")
.data([offset[1]])
.attr("r", 25)
.style("fill","white")
.on("click", clicked);
node2.attr("cx", 530)
.attr("cy", 310)
.transition()
.delay(500)
.duration(1000)
.attr("r", 55)
.attr("cx", 620)
.attr("cy", 310);
d3.select('g').append('text')
.attr("id","seperate")
.attr("dx", 590)
.attr("dy", 315)
.attr("width", 90)
.attr("height", 90)
.text(function(d){return offset[1].label});
function
function clicked(d) {
var imageSelected = this;
console.log("clicked");
var cx, cy, k, offset;
var setClass = d.swipe_class;
cx = d3.select(this).attr("cx");
cy = d3.select(this).attr("cy");
k = 2;
cx= cx - d.xoff;
cy= cy - d.yoff;
console.log("cy="+d.yoff +"cx="+ d.xoff);
svg.transition()
.duration(750)
.attr("transform", "translate(" + width/2 + "," + height/2 + ")scale(" + k + ")translate(" + -cx + "," + -cy + ")");
}
Is there a way to trigger the circles click event when I click the text ontop of it? Or maybe just a better way of doing this that would allow it?
You can set the text to ignore pointer events:
...
.append("text")
.attr("pointer-events", "none")
...

Categories