Transitioning a bar chart with negative values for the width - javascript

I am creating a horizontal bar chart using d3. And I am using an animation to "grow" the chart at startup. Here is the code.
// Create the svg element
d3.select("#chart-area")
.append("svg")
.attr("height", 800)
.attr("width", 800);
.data(dataValues) // This data is previously prepared
.enter().append("rect")
.style("fill", "blue")
.attr("x", function () { return xScale(0); }) // xScale is defined earlier
.attr("y", function (d) { return yScale(d); }) // yScale is defined earlier
.attr("height", yScale.bandwidth()) // yScale is defined earlier
// Initial value of "width" (before animation)
.attr("width", 0)
// Start of animation transition
.transition()
.duration(5000) // 5 seconds
.ease (d3.easeLinear);
// Final value of "width" (after animation)
.attr("width", function(d) { return Math.abs(xScale(d) - xScale(0)); })
The above code would work without any problem, and the lines would grow as intended, from 0 to whichever width, within 5 seconds.
Now, if we change the easing line to the following
// This line changed
.ease (d3.easeElasticIn);
Then, the ease would try to take the width to a negative value before going to a final positive value. As you can see here, d3.easeElasticIn returns negative values as time goes by, then back to positive, resulting in width being negative at certain points in the animation. So the bars do not render properly (because SVG specs state that if width is negative, then use 0)
I tried every solution to allow the bars to grow negatively then back out. But could not find any. How can I fix this problem?
Thanks.

As you already know, the use of d3.easeElasticIn in your specific code will create negative values for the rectangles' width, which is not allowed.
This basic demo reproduces the issue, the console (your browser's console, not the snippet's console) is populated with error messages, like this:
Error: Invalid negative value for attribute width="-85.90933910798789"
Have a look:
const svg = d3.select("svg");
const margin = 50;
const line = svg.append("line")
.attr("x1", margin)
.attr("x2", margin)
.attr("y1", 0)
.attr("y2", 150)
.style("stroke", "black")
const data = d3.range(10).map(function(d) {
return {
y: "bar" + d,
x: Math.random()
}
});
const yScale = d3.scaleBand()
.domain(data.map(function(d) {
return d.y
}))
.range([0, 150])
.padding(0.2);
const xScale = d3.scaleLinear()
.range([margin, 300]);
const bars = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
.attr("x", margin)
.attr("width", 0)
.style("fill", "steelblue")
.attr("y", function(d) {
return yScale(d.y)
})
.attr("height", yScale.bandwidth())
.transition()
.duration(2000)
.ease(d3.easeElasticIn)
.attr("width", function(d) {
return xScale(d.x) - margin
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
So, what's the solution?
One of them is catching those negative values as they are generated and, then, moving the rectangle to the left (using the x attribute) and converting those negative numbers to positive ones.
For that to work, we'll have to use attrTween instead of attr in the transition selection.
Like this:
.attrTween("width", function(d) {
return function(t){
return Math.abs(xScale(d.x) * t);
};
})
.attrTween("x", function(d) {
return function(t){
return xScale(d.x) * t < 0 ? margin + xScale(d.x) * t : margin;
};
})
In the snippet above, margin is just a margin that I created so you can see the bars going to the left of the axis.
And here is the demo:
const svg = d3.select("svg");
const margin = 100;
const line = svg.append("line")
.attr("x1", margin)
.attr("x2", margin)
.attr("y1", 0)
.attr("y2", 150)
.style("stroke", "black")
const data = d3.range(10).map(function(d) {
return {
y: "bar" + d,
x: Math.random()
}
});
const yScale = d3.scaleBand()
.domain(data.map(function(d) {
return d.y
}))
.range([0, 150])
.padding(0.2);
const xScale = d3.scaleLinear()
.range([0, 300 - margin]);
const bars = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
.attr("x", margin)
.attr("width", 0)
.style("fill", "steelblue")
.attr("y", function(d) {
return yScale(d.y)
})
.attr("height", yScale.bandwidth())
.transition()
.duration(2000)
.ease(d3.easeElasticIn)
.attrTween("width", function(d) {
return function(t) {
return Math.abs(xScale(d.x) * t);
};
})
.attrTween("x", function(d) {
return function(t) {
return xScale(d.x) * t < 0 ? margin + xScale(d.x) * t : margin;
};
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>

Related

How do I match up text labels in a legend created in d3

I am building a data visualization project utilizing the d3 library. I have created a legend and am trying to match up text labels with that legend.
To elaborate further, I have 10 rect objects created and colored per each line of my graph. I want text to appear adjacent to each rect object corresponding with the line's color.
My Problem
-Right now, an array containing all words that correspond to each line appears adjacent to the top rect object. And that's it.
I think it could be because I grouped my data using the d3.nest function. Also, I noticed only one text element is created in the HTML. Can anyone take a look and tell me what I'm doing wrong?
JS Code
const margin = { top: 20, right: 30, bottom: 30, left: 0 },
width = 1000 - margin.left - margin.right;
height = 600 - margin.top - margin.bottom;
// maybe a translate line
// document.body.append(svg);
const div_block = document.getElementById("main-div");
// console.log(div_block);
const svg = d3
.select("svg")
.attr("width", width + margin.left + margin.right) // viewport size
.attr("height", height + margin.top + margin.bottom) // viewport size
.append("g")
.attr("transform", "translate(40, 20)"); // center g in svg
// load csv
d3.csv("breitbartData.csv").then((data) => {
// convert Count column values to numbers
data.forEach((d) => {
d.Count = +d.Count;
d.Date = new Date(d.Date);
});
// group the data with the word as the key
const words = d3
.nest()
.key(function (d) {
return d.Word;
})
.entries(data);
// create x scale
const x = d3
.scaleTime() // creaters linear scale for time
.domain(
d3.extent(
data,
// d3.extent returns [min, max]
(d) => d.Date
)
)
.range([margin.left - -30, width - margin.right]);
// x axis
svg
.append("g")
.attr("class", "x-axis")
.style("transform", `translate(-3px, 522px)`)
.call(d3.axisBottom(x))
.append("text")
.attr("class", "axis-label-x")
.attr("x", "55%")
.attr("dy", "4em")
// .attr("dy", "20%")
.style("fill", "black")
.text("Months");
// create y scale
const y = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.Count)])
.range([height - margin.bottom, margin.top]);
// y axis
svg
.append("g")
.attr("class", "y-axis")
.style("transform", `translate(27px, 0px)`)
.call(d3.axisLeft(y));
// line colors
const line_colors = words.map(function (d) {
return d.key; // list of words
});
const color = d3
.scaleOrdinal()
.domain(line_colors)
.range([
"#e41a1c",
"#377eb8",
"#4daf4a",
"#984ea3",
"#ff7f00",
"#ffff33",
"#a65628",
"#f781bf",
"#999999",
"#872ff8",
]); //https://observablehq.com/#d3/d3-scaleordinal
// craete legend variable
const legend = svg
.append("g")
.attr("class", "legend")
.attr("height", 100)
.attr("width", 100)
.attr("transform", "translate(-20, 50)");
// create legend shapes and locations
legend
.selectAll("rect")
.data(words)
.enter()
.append("rect")
.attr("x", width + 65)
.attr("y", function (d, i) {
return i * 20;
})
.attr("width", 10)
.attr("height", 10)
.style("fill", function (d) {
return color(d.key);
});
// create legend labels
legend
.append("text")
.attr("x", width + 85)
.attr("y", function (d, i) {
return i * 20 + 9;
})
// .attr("dy", "0.32em")
.text(
words.map(function (d, i) {
return d.key; // list of words
})
);
// returning an array as text
// });
svg
.selectAll(".line")
.data(words)
.enter()
.append("path")
.attr("fill", "none")
.attr("stroke", function (d) {
return color(d.key);
})
.attr("stroke-width", 1.5)
.attr("d", function (d) {
return d3
.line()
.x(function (d) {
return x(d.Date);
})
.y(function (d) {
return y(d.Count);
})(d.values);
});
});
Image of the problem:
P.S. I cannot add a JSfiddle because I am hosting this page on a web server, as that is the only way chrome can read in my CSV containing the data.
My Temporary Solution
function leg_labels() {
let the_word = "";
let num = 0;
for (i = 0; i < words.length; i++) {
the_word = words[i].key;
num += 50;
d3.selectAll(".legend")
.append("text")
.attr("x", width + 85)
.attr("y", function (d, i) {
return i + num;
})
// .attr("dy", "0.32em")
.text(the_word);
}
}
leg_labels();
Problem
Your problem has to do with this code
legend
.append("text")
.attr("x", width + 85)
.attr("y", function (d, i) {
return i * 20 + 9;
})
// .attr("dy", "0.32em")
.text(
words.map(function (d, i) {
return d.key; // list of words
})
);
You are appending only a single text element and in the text function you are returning the complete array of words, which is why all words are shown.
Solution
Create a corresponding text element for each legend rectangle and provide the correct word. There are multiple ways to go about it.
You could use foreignObject to append HTML inside your SVG, which is very helpful for text, but for single words, plain SVG might be enough.
I advise to use a g element for each legend item. This makes positioning a lot easier, as you only need to position the rectangle and text relative to the group, not to the whole chart.
Here is my example:
let legendGroups = legend
.selectAll("g.legend-item")
.data(words)
.enter()
.append("g")
.attr("class", "legend-item")
.attr("transform", function(d, i) {
return `translate(${width + 65}px, ${i * 20}px)`;
});
legendGroups
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 10)
.attr("height", 10)
.style("fill", function (d) {
return color(d.key);
});
legendGroups
.append("text")
.attr("x", 20)
.attr("y", 9)
.text(function(d, i) { return words[i].key; });
This should work as expected.
Please note the use of groups for easier positioning.

D3.js Combining Candlestick Chart with Line Graph

I am trying to plot a moving average on top of a candlestick chart but the "path" is not appearing completely on the svg canvas that I created.
I have tried looking at several post on how to put a line on top of a bar graph (because I figured it would be similar) but it has not worked.
A couple of the examples and post I have looked at are below:
https://bl.ocks.org/nanu146/f48ffc5ec10270f55c9e1fb3da8b38f0
d3.js How to add lines to a bar chart
D3.js combining bar and line chart
I have all the data in a array.
I am using the same x "scale" for both the candle stick graph and the moving average (line). I have tried using the same y "scale" for both the line and the candlestick but it did not work. Therefore i tried creating 2 scales for y, one for the moving average and one for the candlestick chart. That is what Im doing in my code below.
<script src="https://d3js.org/d3.v4.min.js"></script>
<script type="text/javascript">
var twoHundredDayCandleStickChart = [];
//pulling from 2 properties so must do this way
#for (int i = 0; i != 100; ++i)
{
#:twoHundredDayCandleStickChart.push({date: '#Model.DailyTimeSeriesData.Data.ElementAt(i).Key', high: '#Model.DailyTimeSeriesData.Data.ElementAt(i).Value.high', low: '#Model.DailyTimeSeriesData.Data.ElementAt(i).Value.low', open: '#Model.DailyTimeSeriesData.Data.ElementAt(i).Value.open', close: '#Model.DailyTimeSeriesData.Data.ElementAt(i).Value.close', sma: '#Model.TwoHundredDaySma.Data.ElementAt(i).Value.Sma'})
}
console.log(twoHundredDayCandleStickChart);
var width = 900;
var height = 500;
var margin = 50;
function min(a, b) { return a < b ? a : b; }
function max(a, b) { return a > b ? a : b; }
//y for the candlestick
var y = d3.scaleLinear().range([height - margin, margin]);
var x = d3.scaleTime().range([margin, width - margin]);
//y for the line
var y1 = d3.scaleLinear().range([height - margin, margin]);
//line for the sma
var line1 = d3.line()
.x(function (d) { return x(d["date"]); })
.y(function (d) { return y(d["sma"]); });
function buildChart(data) {
data.forEach(function (d) {
d.date = new Date(d.date);
d.high = +d.high;
d.low = +d.low;
d.open = +d.open;
d.close = +d.close;
d.sma = +d.sma;
});
var chart = d3.select("#twoHundredDaySmaWithCandleStickChart")
.append("svg")
.attr("class", "chart")
.attr("width", width)
.attr("height", height);
//map is going to create an array with all the lows and then d3.min will take the min out of all of them
y.domain([d3.min(data.map(function (x) { return x["low"]; })), d3.max(data.map(function (x) { return x["high"]; }))])
x.domain(d3.extent(data, function (d) { return d["date"]; }))
y1.domain(d3.extent(68, d3.max(data, function (d) { return d["sma"]; })))
//grid for the chart; x and y axis
chart.selectAll("line.x")
.data(x.ticks(10))
.enter().append("line")
.attr("class", "x")
//.text(String)
.attr("x1", x)
.attr("x2", x)
.attr("y1", margin)
.attr("y2", height - margin)
.attr("stroke", "#ccc");
chart.selectAll("line.y")
.data(y.ticks(10))
.enter().append("line")
.attr("class", "y")
.attr("x1", margin)
.attr("x2", width - margin)
.attr("y1", y)
.attr("y2", y)
.attr("stroke", "#ccc");
//x axis
chart.append("g")
.attr("transform", "translate(0," + 450 + ")") //need to change this 450 to a variable- it is how far down the axis will go
.attr("class", "xrule") // give it a class so it can be used to select only xaxis labels or change color
//the x axis
.call(d3.axisBottom(x))
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", function (d) {
return "rotate(-65)"
});
//the y axis
chart.selectAll("text.yrule")
.data(y.ticks(10))
.enter()
.append("text")
.attr("class", "yrule")
.attr("x", 0)
.attr("y", y)
.attr("dy", 0)
.attr("dx", 20)
.attr("text-anchor", "middle")
.text(String);
//add rectangles- if open higher then close then red
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("x", function (d) { return x(d["date"]); })
.attr("y", function (d) { return y(max(d["open"], d["close"])); })
.attr("height", function (d) { return y(min(d["open"], d["close"])) - y(max(d["open"], d["close"])); })
.attr("width", function (d) { return 0.5 * (width - 2 * margin) / data.length; })
.attr("fill", function (d) { return d["open"] > d["close"] ? "red" : "green"; });
//add a stem to the rectangle
chart.selectAll("line.stem")
.data(data)
.enter().append("line")
.attr("class", "stem")
.attr("x1", function (d) { return x(d["date"]) + 0.25 * (width - 2 * margin) / data.length; })
.attr("x2", function (d) { return x(d["date"]) + 0.25 * (width - 2 * margin) / data.length; })
.attr("y1", function (d) { return y(d["high"]); })
.attr("y2", function (d) { return y(d["low"]); })
.attr("stroke", function (d) { return d.open > d.close ? "red" : "green"; });
chart.append("path")
.data([data])
.attr("d", line1)
.attr("class", "line")
.style("stroke", "white")
.attr("fill", "none")
.attr("stroke-width", 2);
}
buildChart(twoHundredDayCandleStickChart);
</script>
The above code is giving me the image below:
The problem in the chart above was my scales! I was taking the domain for the candle stick data but the line data was a lot lower of a min. Therefore the whole graph was not showing up on the scale because the min of the domain had to be adjusted. MANY hours wasted but hopefully this can save someone else time!
d3.select("#twoHundredDaySmaWithCandleStickChart")
Try to change the above code like below
d3.select("svg") or give the div Id

How Do I relate and combine bar chart with bubble chart in D3?

I want to use D3 for generating charts from JSON files. How do I combine/relate bar chart and bubble chart so when you click on either it should provide you details of both charts in a legend.
The each bubble must be below and center of the each bar. It must share x-axis of bar charts.
There are two different data sources for them.
No. of bars = No. of bubbles
I created the xscale
var xScale = d3.scale.ordinal() .domain( d3.range(dataset.length))
.rangePoints([0, w-50]);
This same scale is shared while drawing bar as well as circles.
I have changed your codepen code
Below is the complete code after change
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d,i) {
//return i * (w / dataset.length);
return xScale(i) ;
})
.attr("y", function(d) {
return h - (d * 10);
})
.attr("width", barWidth)
.attr("height", function(d) {
return d * 10;
})
.attr("fill","#d38e71");
var circles = svg.selectAll("circle")
//.data(dataset)
.data(num)
.enter()
.append("a")
.attr("xlink:href", "http://google.com")
.attr("xlink:title",function(d){
return "No. of campagins sent - "+d;
})
.attr("target","_blank")
.append("circle");
circles.attr("cx", function(d,i) {
/* return i* (wB / dataset.length)+(wB / dataset.length)/2-2;*/
return (xScale(i) + barWidth/2);
})
.attr("cy", function(d) {
return hB-35;
})
.attr("r", function(d) {
return rScale(d);
})
Note the following code for bar
.attr("x", function(d,i) {
//return i * (w / dataset.length);
return xScale(i) ;
})
Note the following code for circles.
circles.attr("cx", function(d,i) {
/* return i* (wB / dataset.length)+(wB / dataset.length)/2-2;*/
return (xScale(i) + barWidth/2);
})
One suggestion
Width of the bar is set via this statement (w / dataset.length - barPadding) This can be stored in a variable. So it will not calculate for each member in the chart.

D3 Javascript / SVG Part III ISsue

I'm following the part III tutorial of "Let' Make Some Charts" as an introduction to D3. Part of the tutorial calls for data insertion via TSV. Given I don't see this being an eventual use case for me, I'm attempting to modify the tutorial with the code below using a simple javascript array. However, nothing shows up on the page when I render in the browser. Can anyone shed some light on this?
Here's the tutorial link for some reference to the original code: http://bost.ocks.org/mike/bar/3/
My JS code:
<script>
var data = [4,8,15,16,23,42,57,89,100,160];
var width = 960,
height = 500; // have to make sure variables are case sensitive
var y = d3.scale.linear()
.domain([0, d3.max(data)]) // scaling based on max value
.range([height, 0]);
var chart = d3.select(".chart")
.attr("width", width)
.attr("height", height);
var barWidth = width / data.length;
var bar = chart.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d,i) { return "translate(" + i * barWidth + ",0)";});
bar.append("rect")
.attr("y", function(d) { return y(d.value); })
.attr("width", barWidth - 1)
.attr("height", function(d) { return height - y(d.value); });
bar.append("text")
.attr("x", barWidth / 2)
.attr("y", function(d) { return y(d.value) + 3; })
.attr("dy", ".75em")
.text(function(d) { return d.value; });
function type(d) {
d.value = +d.value;
return d;
}
</script>
The code you've copied references a named attribute value to determine what to draw. The data you've created doesn't have this but just the data. So everywhere you have d.value, you need to reference just d.
Complete demo here.
Your problem stems from the fact that you're using an Array of numbers for your data, while in Mike Bostock's example he was using an Array of Objects (for example, var data = [{value: 30}, ...]). Thus you need to change all cases of d.value to d in your code, since your data is not longer an Object but just a number.
bar.append("rect")
.attr("y", function(d) { return y(d); }) // <---- delete .value
.attr("width", barWidth - 1)
.attr("height", function(d) { return height - y(d); }); // <---- delete .value
bar.append("text")
.attr("x", barWidth / 2)
.attr("y", function(d) { return y(d) + 3; }) // <---- delete .value
.attr("dy", ".75em")
.text(function(d) { return d; }); // <---- delete .value
Making these changes produces the following bar chart:

Chart not updating properly

I am trying to follow Mike Bostock's tutorial on d3js (http://mbostock.github.io/d3/tutorial/bar-2.html) to understand how to update charts dynamically but I am facing some hurdles.
In my chart, my bars on the left, rather than being simply removed, are sent behind my chart and I can't figure out why:
JS:
var t = 1297110663, // start time (seconds since epoch)
v = 70, // start value (subscribers)
data = d3.range(33).map(next); // starting dataset
function next() {
return {
time: ++t,
value: v = ~~Math.max(10, Math.min(90, v + 10 * (Math.random() - .5)))
};
}
setInterval(function(){
data.shift();
data.push(next());
console.log(data);
redraw();
}, 1000);
var w = 20,
h =80;
var x = d3.scale.linear()
.domain([0, 1])
.range([0, w]);
var y = d3.scale.linear()
.domain([0, 100])
.rangeRound([0, h]);
var chart = d3.select(".container").append("svg")
.attr("class", "chart")
.attr("width", w * data.length - 1)
.attr("height", h);
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("x", function(d, i){ return x(i) - 0.5; })
.attr("y", function(d) { return h - y(d.value) - .5; })
.attr("width", w)
.attr("height", function(d) { return y(d.value); });
function redraw(){
console.log(data);
var rect = chart.selectAll('rect')
.data(data, function(d){ return d.time; });
rect.enter().insert("rect")
.attr("x", function(d, i) { return x(i + 1) - .5; })
.attr("y", function(d) { return h - y(d.value) - .5; })
.attr("width", w)
.attr("height", function(d) { return y(d.value); })
rect.transition() // Shouldn't I use .update() here?
.duration(1000)
.attr("x", function(d, i) { return x(i) - .5; });
rect.exit().transition()
.duration(1000)
.attr('x', function(d, i) { return x(i - 1) - .5})
.remove();
}
Here is the jsfiddle: http://jsfiddle.net/kkMR4/
Another thing I don't understand is why we dont use .update()? If I understand correctly .enter() is used to create the DOM element where data didnt find any match in the DOM and .exit() is used to find the DOM elements which are not in data, so shouldn't I use update() to move all the other column to the left?
Many thanks
Best
The problem is in this block:
rect.exit().transition()
.duration(1000)
.attr('x', function(d, i) { return x(i - 1) - .5})
.remove();
The third line (.attr), reassigns the coordinates. If you want them to truly exit, you can remove this line.
rect.exit().transition()
.duration(1000)
.remove();

Categories