I would like to draw rectangle on a barchart just like a animation. I only need to draw one rectangle one time (maybe keep it for 0.5 secs) then remove it and draw another rectangle.
Currently all rectangles will be draw on screen! I try to use exit pattern but not work!
barchart_hist()
function barchart_hist() {
var m = [10, 80, 25, 80]
var w = 600 - m[1] - m[3]
var h = 400 - m[0] - m[2]
var y_data = [2,1,5,6,2,3,0]
var graph = d3.select('body').append("svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")")
var x = d3.scaleBand()
.domain(d3.range(y_data.length))
.range([0, w])
.padding(0.05);
var y = d3.scaleLinear()
.domain([0, d3.max(y_data)])
.range([h,0])
var xAxis = d3.axisBottom(x)
.ticks(y_data.length)
var yAxisLeft = d3.axisLeft(y)
.ticks(d3.max(y_data))
graph.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (h) + ")")
.call(xAxis);
graph.append("g")
.attr("class", "y axis")
.attr("transform", "translate(-25,0)")
.call(yAxisLeft);
graph.selectAll("bar")
.data(y_data)
.enter().append("rect")
.style("fill", "none")
.attr('stroke','black')
.attr("x", (d,i) => x(i))
.attr("y", (d,i) => y(d))
.attr("width", x.bandwidth())
.attr("height", (d,i) => h - y(d));
var rdata = [[0,1],[3,4],[2,4],[5,6],[4,6],[1,6]]
var box = graph.selectAll('box')
.data(rdata)
var boxenter = box.enter()
.append('rect')
.attr('stroke','red')
.attr('stroke-width',2)
.attr('fill','none')
.attr('x',d => {
return x(d[0])})
.attr('y',d => y(y_data[d[0]]))
.attr('width',d => {
return x(d[1])-x(d[0])})
.attr('height',d => {
return y(0) - y(y_data[d[0]])
})
box.exit().transition().duration(500).remove()
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
There appear to be two key issues:
Enter/Update/Exit
The exit selection only contains elements when there are selected elements in the DOM which do not have corresponding items in the data array. In your case we have an empty selection with bound data:
// an empty selection (no elements with tag box exist):
var box = graph.selectAll('box')
.data(rdata) // bind data to the selection
Then we use the enter selection box.enter() to create elements so that every item in the data array as a corresponding element in the DOM. Since there are no matching elements in the DOM, we create a new element for every item in the DOM:
var boxenter = box.enter()
.append('rect')
...
The box exit selection, box.exit() contains any elements for which a corresponding item in the data array does not exist. selection.exit() does nothing other than return a selection. Since there was no pre-existing elements, no elements can be in the exit selection. So this:
box.exit().transition().duration(500).remove()
Does nothing as the exit selection is empty.
For reference, any existing elements that are not included in the exit selection are in the update selection (box).
If we want to do something with the newly entered boxes, we can use the boxenter selection.
Without using a key function with .data(), only one of the enter/exit selections can contain elements: either we have too many elements (exit selection contains elements) or not enough (enter selection contains elements).
Transitions
Your use of selection.transition().duration(500).remove() will remove a selection, but it will not transition anything. Transitions are used to change an attribute/style/property over time. You haven't specified what property you want to transition. selection.transition().duration(500).remove() will simply remove the selection from the DOM after 500 ms.
Instead, you need to transition an attribute/style/property, eg:
selection.transition() // return a transition (not a selection).
.attr('y',d => y(0))
.attr('height',0)
.duration(10000)
.remove();
Note, transitions have similar methods as selections - but there are methods for each which do not apply to the other. Here .attr(), .duration(), and .remove() are common to both transitions and selections, but .duration(), for example is only for transitions.
We can just chain the transition to the enter selection, since we want to remove it right after we add it.
Here's the above in action:
barchart_hist()
function barchart_hist() {
var m = [10, 80, 25, 80]
var w = 600 - m[1] - m[3]
var h = 400 - m[0] - m[2]
var y_data = [2,1,5,6,2,3,0]
var graph = d3.select('body').append("svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")")
var x = d3.scaleBand()
.domain(d3.range(y_data.length))
.range([0, w])
.padding(0.05);
var y = d3.scaleLinear()
.domain([0, d3.max(y_data)])
.range([h,0])
var xAxis = d3.axisBottom(x)
.ticks(y_data.length)
var yAxisLeft = d3.axisLeft(y)
.ticks(d3.max(y_data))
graph.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (h) + ")")
.call(xAxis);
graph.append("g")
.attr("class", "y axis")
.attr("transform", "translate(-25,0)")
.call(yAxisLeft);
graph.selectAll("bar")
.data(y_data)
.enter().append("rect")
.style("fill", "none")
.attr('stroke','black')
.attr("x", (d,i) => x(i))
.attr("y", (d,i) => y(d))
.attr("width", x.bandwidth())
.attr("height", (d,i) => h - y(d));
var rdata = [[0,1],[3,4],[2,4],[5,6],[4,6],[1,6]]
var box = graph.selectAll('box')
.data(rdata)
var boxenter = box.enter()
.append('rect')
.attr('stroke','red')
.attr('stroke-width',2)
.attr('fill','none')
.attr('x',d => {
return x(d[0])})
.attr('y',d => y(y_data[d[0]]))
.attr('width',d => {
return x(d[1])-x(d[0])})
.attr('height',d => {
return y(0) - y(y_data[d[0]])
})
.transition()
.attr('y',d => y(0))
.attr('height',0)
.duration(10000)
.remove();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
Consecutive Transitions
Ok, now we can look at putting the above together for consecutive transitions. This would be easiest if you had the same number of elements in each series: we could just use one data array that has properties representing the values in both arrays and transition from one to the other while also entering items only once. If desired, I can also expand on that approach.
But, as you have different sized arrays (or you might have dynamic data), we'll need a different approach. The below uses a standardized data structure for both arrays so that we can use the same update function for each.
I modified the last element in the first data array so it is visible in the chart for demonstration of the code (so you know it isn't errantly missing)
// The data:
var data1 = [[0,2],[1,1],[2,5],[3,6],[4,2],[5,3],[6,1]];
var data2 = [[0,1],[3,4],[2,4],[5,6],[4,6],[1,6]]
// The set up:
var m = [10, 80, 25, 80]
var w = 600 - m[1] - m[3]
var h = 300 - m[0] - m[2]
var graph = d3.select('body').append("svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")")
var xAxis = graph.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (h) + ")")
var yAxis = graph.append("g")
.attr("class", "y axis")
.attr("transform", "translate(-25,0)")
// (For demonstration):
var i = 0;
var datasets = [data1, data2];
// Update function:
function update(data) {
// set up scales:
var x = d3.scaleBand()
.domain(d3.range(data.length))
.range([0, w])
.padding(0.05);
var y = d3.scaleLinear()
.domain([0, d3.max(data, d=>d[1])])
.range([h,0])
// set up axis:
xAxis.transition().call(d3.axisBottom(x));
yAxis.transition().call(d3.axisLeft(y));
// Enter then remove:
graph.selectAll(".bar")
.data(data)
.enter()
.append("rect")
.style("fill", "none")
.attr('stroke','black')
.attr("x", d=>x(d[0]))
.attr("width", x.bandwidth())
// Start new rects with zero height:
.attr("y", y(0))
.attr("height", 0)
// Transition up:
.transition()
.attr("y", d=>y(d[1]))
.attr("height", d=>y(0)-y(d[1]))
// Transition down:
.transition()
.delay(1000) // wait a bit first
.attr("y", y(0))
.attr("height", 0)
.remove()
.end()
// get next dataset, and repeat:
.then(()=>update(datasets[++i%2]));
// example to get one more data set and stop:
//.then(function() {
// if (data === data1) update(data2)
// else console.log("end");
//});
}
update(data1);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
Note the use of transition.end() returns a promise once all transitions end whereas the use of transition.on("end", fn) will trigger on each element's transition end.
You've stated you want to remove the existing elements before updating with the new data. There are many questions on SO that ask how to remove all existing elements and then create new elements, this pattern omits update and exit selections and is frequently non-canonical. It may be that your second data array items don't represent any of the entities represented in the first data array - in which case this approach is fine. But, often this approach limits your functionality. Should you want to update the existing items with new data, we can alter the approach to make full use of updating existing data points (which may tell an interesting story, depending on your data and what it represents):
// The data:
var data1 = [[0,2],[1,1],[2,5],[3,6],[4,2],[5,3],[6,1]];
var data2 = [[0,1],[3,4],[2,4],[5,6],[4,6],[1,6]]
// The set up:
var m = [10, 80, 25, 80]
var w = 600 - m[1] - m[3]
var h = 300 - m[0] - m[2]
var graph = d3.select('body').append("svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")")
var xAxis = graph.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (h) + ")")
var yAxis = graph.append("g")
.attr("class", "y axis")
.attr("transform", "translate(-25,0)")
// (For demonstration):
var i = 0;
var datasets = [data1, data2];
// Update function:
function update(data) {
// set up scales:
var x = d3.scaleBand()
.domain(d3.range(data.length))
.range([0, w])
.padding(0.05);
var y = d3.scaleLinear()
.domain([0, d3.max(data, d=>d[1])])
.range([h,0])
// set up axis:
xAxis.transition().delay(500).call(d3.axisBottom(x));
yAxis.transition().delay(500).call(d3.axisLeft(y));
// Enter then remove:
var selection = graph.selectAll(".bar")
.data(data);
var enterTransition = selection.enter()
.append("rect")
.attr("class","bar")
.style("fill", "steelblue")
.attr("opacity",0)
.attr('stroke','black')
.attr('stroke-width',1)
.attr("x", d=>x(d[0]))
.attr("width", x.bandwidth())
// Start new rects with zero height:
.attr("y", y(0))
.attr("height", 0)
// Transition up and remove color:
.transition()
.delay(500)
.duration(1000)
.style("fill","white")
.attr("opacity",1)
.attr("y", d=>y(d[1]))
.attr("height", d=>y(0)-y(d[1]))
.end()
var updateTransition = selection.transition()
.delay(500)
.duration(1000)
.attr("y", d=>y(d[1]))
.attr("height", d=>y(0)-y(d[1]))
.attr("x", d=>x(d[0]))
.attr("width", x.bandwidth())
.end()
var exitTransition = selection.exit()
.transition()
.duration(500)
.attr("opacity",0)
.style("fill","crimson")
.remove()
.end()
Promise.allSettled([enterTransition,updateTransition,exitTransition])
.then(()=>update(datasets[++i%2]));
}
update(data1);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
I've added red for exit selection, blue for enter, with some opacity changes to highlight the changes better
You could consider this approach: when you create the box selection, append the rect but no other attribute thereby creating as many invisible rects as there are pairs in the rdata array.
You can then use selection.each() to iterate box. Since you know the index argument of each you can use that with selection.filter()
to identify the box to set styles and coordinates on; otherwise remove the stroke attribute to render the box invisible. Doing this within a setTimeout gives you the animated process.
I'd say the comment from #AndrewReid re omission of update/ exit being non-canonical applies to the approach below, but I feel it meets the brief.
// prep all the boxes (rect only - will be invisible)
var box = graph.selectAll('box')
.data(rdata)
.enter()
.append('rect');
// iterate boxes
box.each((d, i) => {
setTimeout(() => {
// otherBoxes are boxes not matching i argument in each
var otherBoxes = boxes.filter((d, i2) => i2 !== i);
otherBoxes.attr('stroke', 'none');
// thisbox is the currentbox per the i argument in each
var thisBox = boxes.filter((d, i2) => i2 === i);
// your original rendering code
thisBox
.attr('stroke','red')
.attr('stroke-width',2)
.attr('fill','none')
.attr('x',d => {
return x(d[0])})
.attr('y',d => y(y_data[d[0]]))
.attr('width',d => {
return x(d[1])-x(d[0])})
.attr('height',d => {
return y(0) - y(y_data[d[0]])
});
}, i * 1000); // 1s delay
});
Working example:
barchart_hist()
function barchart_hist() {
var m = [10, 80, 25, 80]
var w = 600 - m[1] - m[3]
var h = 200 - m[0] - m[2]
var y_data = [2,1,5,6,2,3,0]
var graph = d3.select('body').append("svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")")
var x = d3.scaleBand()
.domain(d3.range(y_data.length))
.range([0, w])
.padding(0.05);
var y = d3.scaleLinear()
.domain([0, d3.max(y_data)])
.range([h,0])
var xAxis = d3.axisBottom(x)
.ticks(y_data.length)
var yAxisLeft = d3.axisLeft(y)
.ticks(d3.max(y_data))
graph.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (h) + ")")
.call(xAxis);
graph.append("g")
.attr("class", "y axis")
.attr("transform", "translate(-25,0)")
.call(yAxisLeft);
graph.selectAll("bar")
.data(y_data)
.enter().append("rect")
.style("fill", "none")
.attr('stroke','black')
.attr("x", (d,i) => x(i))
.attr("y", (d,i) => y(d))
.attr("width", x.bandwidth())
.attr("height", (d,i) => h - y(d));
var rdata = [[0,1],[3,4],[2,4],[5,6],[4,6],[1,6]]
// prep all the boxes (id only)
var boxes = graph.selectAll('box')
.data(rdata)
.enter()
.append('rect');
// iterate boxes
boxes.each((d, i) => {
setTimeout(() => {
var otherBoxes = boxes.filter((d, i2) => i2 !== i);
otherBoxes.attr('stroke', 'none');
var thisBox = boxes.filter((d, i2) => i2 === i);
thisBox
.attr('stroke','red')
.attr('stroke-width',2)
.attr('fill','none')
.attr('x',d => {
return x(d[0])})
.attr('y',d => y(y_data[d[0]]))
.attr('width',d => {
return x(d[1])-x(d[0])})
.attr('height',d => {
return y(0) - y(y_data[d[0]])
});
}, i * 1000);
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
Related
Hey guys I created a time series line chart using publicly available stock data.
Where I got to is the following:
Looks like what it is doing is connecting the first datapoint with the last datapoint which is why it is creating a line across the entire chart.
I looked online and read that to create a non continuous line chart I can add
.defined(function(d) { return d; })
I did but it didn't help.
My code:
//Set dimensions and margins for the graph
var margin = {top: 20, right: 20, bottom: 100, left: 70},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
//Create canvas
var svg = d3.select("body").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 + ")");
//Parse date
var parseDate = d3.timeParse("%Y-%m-%d");
//Set the ranges
var x = d3.scaleTime()
.range([0, width]);
var y = d3.scaleLinear()
.range([height, 0]);
// Define the line
var valueLine = d3.line()
.defined(function(d) { return d; })
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.value); });
//Import data from api
d3.json("api_all.php", function(error, data) {
if (error) throw error;
data.forEach(e => {
e.date = parseDate(e.date);
e.value = +e.close;
e.stockName = e.stock_name;
e.stockSymbol = e.stock_symbol;
});
//Create nest variable
var nest = d3.nest()
.key(function(d){ return d.stockSymbol; })
.entries(data);
console.log(nest);
//Scale the range of the data
//x axis scale for entire dataset
x.domain(d3.extent(data, function(d) { return d.date; }));
//y.domain([0, d3.max(data, function(d) { return d.value; })]);
//Add the x axis
var xaxis = svg.append("g")
.attr("transform", "translate(0," + height + ")")
.attr("class", "x axis")
.call(d3.axisBottom(x));
//Add x axis label
svg.append("text")
.attr("transform", "translate(" + (width/2) + "," + (height + margin.top + 10) + ")")
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Date")
.attr("class", "x axis label");
//Create dropdown
var dropDown = d3.select("#dropDown")
dropDown
.append("select")
.selectAll("option")
.data(nest)
.enter()
.append("option")
.attr("value", function(d){ return d.key; })
.text(function(d){ return d.key; })
// Function to create the initial graph
var initialGraph = function(stock){
// Filter the data to include only stock of interest
var selectStock = nest.filter(function(d){
return d.key == stock;
})
console.log(selectStock)
//Unnest selectStock for y axis
var unnested = function(data, children){
var out = [];
data.forEach(function(d, i){
console.log(i, d);
d_keys = Object.keys(d);
console.log(i, d_keys)
values = d[children];
values.forEach(function(v){
d_keys.forEach(function(k){
if (k != children) { v[k] = d[k]}
})
out.push(v);
})
})
return out;
}
var selectStockUnnested = unnested(selectStock, "values");
//Scale y axis
var selectStockGroups = svg.selectAll(".stockGroups")
.data(selectStock, function(d){
return d ? d.key : this.key;
})
.enter()
.append("g")
.attr("class", "stockGroups")
.each(function(d){
y.domain([0, d3.max(selectStockUnnested, function(d) { return d.value; })])
console.log(selectStockUnnested);
});
var initialPath = selectStockGroups.selectAll(".line")
.data(selectStock)
.enter()
.append("path")
initialPath
.attr("d", function(d){ return valueLine(d.values) })
.attr("class", "line")
//Add the y axis
var yaxis = svg.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(y)
.ticks(5)
.tickSizeInner(0)
.tickPadding(6)
.tickSize(0, 0));
//Add y axis label
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - 60)
.attr("x", 0 - (height / 2))
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Price")
.attr("class", "y axis label");
}
// Create initial graph
initialGraph("1301.T")
// Update the data
var updateGraph = function(stock){
// Filter the data to include only stock of interest
var selectStock = nest.filter(function(d){
return d.key == stock;
})
console.log(selectStock);
//Unnest selectStock for y axis
var unnested = function(data, children){
var out = [];
data.forEach(function(d, i){
console.log(i, d);
d_keys = Object.keys(d);
console.log(i, d_keys)
values = d[children];
values.forEach(function(v){
d_keys.forEach(function(k){
if (k != children) { v[k] = d[k]}
})
out.push(v);
})
})
return out;
}
var selectStockUnnested = unnested(selectStock, "values");
// Select all of the grouped elements and update the data
var selectStockGroups = svg.selectAll(".stockGroups")
.data(selectStock)
.each(function(d){
y.domain([0, d3.max(selectStockUnnested, function(d) { return d.value; })])
});
// Select all the lines and transition to new positions
selectStockGroups.selectAll("path.line")
.data(selectStock)
.transition()
.duration(1000)
.attr("d", function(d){
return valueLine(d.values)
})
// Update the Y-axis
d3.select(".y")
.transition()
.duration(1500)
.call(d3.axisLeft(y)
.ticks(5)
.tickSizeInner(0)
.tickPadding(6)
.tickSize(0, 0));
}
// Run update function when dropdown selection changes
dropDown.on('change', function(){
// Find which stock was selected from the dropdown
var selectedStock = d3.select(this)
.select("select")
.property("value")
console.log(selectedStock);
// Run update function with the selected stock
updateGraph(selectedStock)
});
});
</script>
</body>
I'm new to D3 and I am trying to display a simple d3 bar chart that changes which data attribute it is visualizing based on a dropdown menu - the data remains the same and I am displaying the same labels (x-axis) with each dropdown selection, just the labels should transition/change their ordering and the bar values should transition/change based on which attribute they are showing.
When the dropdown menu changes however, the transition (update) selection isn't getting called - it is only called when the chart loads for the first time. Therefore, based on the code, the y-Axis is changing its numerical values, but the heights always remain the same as they are initiated so the bars don't animate at all despite the labels changing.
updateChart(menuSelection) { // called when dropdown selection changes, and initially upon page load with default menuSelection
// I sort the data by the attribute of the dropdown item selected
this.myData.sort(function(a,b){
if(menuSelection == 1) return b.count - a.count;
else if(menuSelection == 2) return b.positiveCount - a.positiveCount;
else return b.negativeCount - a.negativeCount;
});
var m = this.myData.length;
var svg = d3.select(".chart"),
margin = {top: 40, right: 25, bottom: 40, left: 25},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var g = svg.select("g.rectGroup").attr("transform", "translate(" + margin.left + "," + margin.top + ")").attr("class", "rectGroup");
if(g.empty()) {
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")").attr("class", "rectGroup");
}
var x = d3.scaleBand()
.domain(myData.map(function(d) { return d._id; }))
.range([0, width])
.padding(0.08);
var yMax = d3.max(this.myData, function(d) {
if(this.menuSelection == 1) return d.count;
else if(this.menuSelection == 2) return d.positiveCount;
else return d.negativeCount;
});
var y = d3.scaleLinear()
.domain([0, yMax])
.range([height, 0]);
var xAxis = d3.axisBottom()
.scale(x);
var yAxis = d3.axisLeft()
.scale(y);
var yz = getHeights(m, menuSelection, this.myData); // ARRAY OF HEIGHTS DEPENDING ON MENU DROP DOWN SELECTION
var bars = g.selectAll(".bar")
.data(this.myData, function(d) {
return d._id; // KEY IS ITEM NAME FOR OBJECT CONSTANCY; ALL ITEMS ARE DISPLAYED REGARDLESS OF ATTRIBUTE SELECTED, BUT ORDER/VALUES CHANGE FOR EACH ITEM
})
.enter().append("rect")
.attr("class", "bar")
.attr("height", 0)
.attr("y", height);
bars.transition().duration(700)
.attr("x", function(d) { return x(d._id); })
.attr("width", x.bandwidth())
.attr("y", function(d, i) { return y(yz[i])})
.attr("height", function(d, i) {
return height - y(yz[i])
});
bars.exit().remove();
svg.selectAll(".axis").remove();
var height_to_use = +svg.attr("height") - margin.bottom
var xAxis_g = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(" + margin.left + "," + height_to_use + ")")
.call(xAxis)
.selectAll(".tick text")
.call(wrap, x.bandwidth());
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(yAxis);
function getHeights(m, menuSelection, data) {
var values = [];
for (var i = 0; i < m; ++i) {
if(menuSelection == 1) {
values[i] = data[i].count;
} else if(menuSelection == 2) {
values[i] = data[i].positiveCount;
} else {
values[i] = data[i].negativeCount;
}
}
return values;
}
}
Actually, you don't have an update selection in your code.
For having an update selection, break up your "enter" selection, like this:
//this is the update selection
var bars = g.selectAll(".bar")
.data(data, function(d) {
return d._id;
});
//and the remainder is the enter selection
bars.enter().append("rect")
.attr("class", "bar")
.attr("height", 0)
.attr("y", height);
Also, it's worth mentioning that, since you don't have an update selection in your code, this...
bars.exit().remove();
... is useless.
I've modified nice AlainRo’s Block for my needs (unfortunately can't link to it, because have not enough reputation), and I can't remove old data chart after entering new data. There is my codepen. In another example I've added merge(), and the chart is well aligned but the old one is still visible and text values are missed.
I spent a lot of time on it, and I run out of ideas.
There's code
barData = [
{ index: _.uniqueId(), value: _.random(1, 20) },
{ index: _.uniqueId(), value: _.random(1, 20) },
{ index: _.uniqueId(), value: _.random(1, 20) }
];
var margin = {top: 20, right: 20, bottom: 50, left: 70},
width = 400 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom,
delim = 4;
var scale = d3.scaleLinear()
.domain([0, 21])
.rangeRound([height, 0]);
var x = d3.scaleLinear()
.domain([0, barData.length])
.rangeRound([0, width]);
var y = d3.scaleLinear()
.domain([0, 21])
.rangeRound([height, 0]);
var svg = d3.select('#chart')
.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 + ")");
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
svg.append("g")
.call(d3.axisLeft(y));
function draw() {
x.domain([0, barData.length]);
var brush = d3.brushY()
.extent(function (d, i) {
return [[x(i)+ delim/2, 0],
[x(i) + x(1) - delim/2, height]];})
.on("brush", brushmove);
var svgbrush = svg.selectAll('.brush')
.data(barData)
.enter()
.append('g')
.attr('class', 'brush')
.append('g')
.call(brush)
.call(brush.move, function (d){return [d.value, 0].map(scale);});
svgbrush
.append('text')
.attr('y', function (d){return scale(d.value) + 25;})
.attr('x', function (d, i){return x(i) + x(0.5);})
.attr('dx', '-.60em')
.attr('dy', -5)
.style('fill', 'white')
.text(function (d) {return d3.format('.2')(d.value);});
svgbrush
.exit()
.append('g')
.attr('class', 'brush')
.remove();
function brushmove() {
if (!d3.event.sourceEvent) return; // Only transition after input.
if (!d3.event.selection) return; // Ignore empty selections.
if (d3.event.sourceEvent.type === "brush") return;
var d0 = d3.event.selection.map(scale.invert);
var d = d3.select(this).select('.selection');;
var d1 =[d0[0], 0];
d.datum().value = d0[0]; // Change the value of the original data
d3.select(this).call(d3.event.target.move, d1.map(scale));
svgbrush
.selectAll('text')
.attr('y', function (d){return scale(d.value) + 25;})
.text(function (d) {return d3.format('.2')(d.value);});
}
}
draw();
function upadateChartData() {
var newBarsToAdd = document.getElementById('charBarsCount').value;
var newBarData = function() {
return { index: _.uniqueId(), value: _.random(1, 20) }
};
newBarData = _.times(newBarsToAdd, newBarData);
barData = _.concat(barData, newBarData)
draw();
};
Is it also possible to remove cross pointer and leave only resize, when I'm dragging top bar border?
You're appending g elements twice. This:
svgbrush.enter()
.append('g')
.attr('class', 'brush')
.merge(svgbrush)
.append('g')
.call(brush)
.call(brush.move, function (d){return [d.value, 0].map(scale);});
Should be:
svgbrush.enter()
.append('g')
.attr('class', 'brush')
.merge(svgbrush)
.call(brush)
.call(brush.move, function (d){return [d.value, 0].map(scale);});
Here is your updated Pen: http://codepen.io/anon/pen/VmavyX
PS: I also made other changes, declaring some new variables, just to organize your enter and update selections and solving the texts problem.
I have a barchart which is populated by values from a JSON variable. The chart is dynamic because the values are retrieved from an SQL query by doing a count. So my data gets fed back in like :
[{"Fruit":"Apple","COUNT( Fruit )":"12"},{"Fruit":"Orange","COUNT( Fruit )":"6"},{"Fruit":"Pear","COUNT( Fruit )":"1"},{"Fruit":"Blank","COUNT( Fruit )":"1"},{"Fruit":"Pineapple","COUNT( Fruit )":"1"},{"Fruit":"Kiwi","COUNT( Fruit )":"1"}]
For the most part my graphs seem to be displaying properly. However some are returning values that exceed the Y Axis, I dont think it's the values that are causing the issues I believe its the axis that isnt calculating the right height. For instance
If Orange count is 14, sometimes the Y axis stops at a number less than this and that column extends the graph.
By viewing it in google chrome developer console, I can see the height of the bar is
<rect id="Orange" y="-520px" x="94px" height="1040px" width="162"></rect>
which far extends my SVG height of 600px - Margins(top + bottom) of 80px!
Does anyone know why my Y Axis isn't getting the right Max value?
Code here:
var canv = document.getElementById("exportCanvas");
canv.width = screen.width;
var margin ={top:40, right:0, bottom:40, left:40},
width=screen.width - 250,
height=600-margin.top-margin.bottom;
var jsplit = jdata.split('"');
var keyX = jsplit[1];
var keyY = "";
var data = JSON.parse(jdata);
for (k in data[0]) {
if (k!=keyX) keyY=k;
}
console.log("keyX = " + keyX);
console.log(keyY);
console.log(data[0]);
// scale to ordinal because x axis is not numerical
var x = d3.scale.ordinal()
.domain(['Orange','Apple','Pear']) //Added this in temporarilly. this should be calculated from the data.
.rangeRoundBands([0, width], 0.25,0.25);
//scale to numerical value by height
// var y = d3.scale.linear().range([height, 0]);
var y = d3.scale.linear()
.range([height, 0]);
console.log(data);
x.domain(data.map(function(d){ return d[keyX]}));
y.domain([0, d3.max(data, function(d){return d[keyY]})]);
var chart = d3.select("#chart")
.append("svg") //append svg element inside #chart
.attr("width", width+ margin.left+margin.right) //set width
// .attr("width", width+(2*margin.left)+margin.right) //set width
.attr("height", height+margin.top+margin.bottom); //set height
// .attr("transform", "translate(" + Math.min(width,height) / 2 + "," + Math.min(width,height) / 2 + ")");
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom"); //orient bottom because x-axis will appear below the bars
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(10).tickFormat(function(d) {
if (d % 1 == 0) {
return d3.format('.f')(d)
} else {
return ""
}
});
var bar = chart.selectAll("g")
.data(data)
.enter()
.append("g");
//you're moving the group and then moving the rect below as well
//one or the other not both need to be moved.
//.attr("transform", function(d, i){
// return "translate("+x(d[keyX])+", 0)";
//});
bar.append("rect")
.attr("id", function(d) {
return d[keyX];
})
.attr("y", function(d) {
return y(d[keyY]) + "px";
})
.attr("x", function(d,i){
//AB - Adjusted this so it correcly places the bar along the X
//x.range is an array of x values for each bar
//calculated in the var x = line above , with the .rangeRoundBands([0, width], 0.25,0.25);
//setting the width of the bars (an equal division of width) with margins of 0.25 at the start
//and end of the graph and 0.25 between each bar.
return x.range()[i] + margin.left + "px";
})
.attr("height", function(d) {
return height - y(d[keyY]) +"px";
})
.attr("width", x.rangeBand()); //set width base on range on ordinal data
bar.append("text")
.attr("x",function(d,i){
//similar to above but adding half the width of the bar to the x position
//to roughly center it on the bar. only rough as doesnt take account of length of text.
return x.range()[i] + margin.left + (x.rangeBand()/2)+ "px";
})
.attr("y", function(d) { return y(d[keyY]) +20; })
.attr("dy", ".75em")
.style("fill","white")
.style("font-weight", "bold")
.text(function(d) { return d[keyY]; });
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate("+margin.left+","+ height+")")
.call(xAxis);
chart.append("g")
.attr("class", "y axis")
.attr("transform", "translate("+margin.left+",0)")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text(keyY);
Apologies for commented out code, I have been playing with it alot to try and suss this out.
You need to recalculate y.domain() when your dataset refreshes. So when you update your data, you can try something like:
y.domain([0, d3.max(data, function(d){return d[keyY]})]);
chart.select(".y.axis")
.call(yAxis.scale(y));
I am new to programming and new to D3, I am learning it using the book "Getting started with D3" and all the examples I can find around the internet.
To start practicing I am trying to create a column chart but am encountering two problems with my x-label:
The labels displayed below my x-axis are not in line with the columns. To be precise, I have numbers displayed before the labels start being displayed.
I am wondering how to rotate the labels. When I try the rotate attribute all of the x-axis is being rotated and I would only like to rotate the labels. I have found some questions related to this but haven't completely understood the answers.
My data are formatted this way:
{"Low_Income_Household_Internet_access":
[{"Country":"Chile","Value":9.69},
{"Country":"New Zealand","Value":47.8},
{"Country":"Greece","Value":25.39},
{"Country":"Italy","Value":33.26},
{"Country":"Israel","Value":40.31},
{"Country":"Portugal","Value":22.72},
{"Country":"United Kingdom","Value":56},...
],
"High_Income_Household_Internet_access":
[{"Country":"Chile","Value":59.78701735},
{"Country":"New Zealand","Value":78.3},
{"Country":"Greece","Value":81.14},
{"Country":"Italy","Value":84.44},
{"Country":"Israel","Value":86.59},
{"Country":"Portugal","Value":89.73},
{"Country":"United Kingdom","Value":90},...
]}
and my code is the following:
function draw(data) {
"use strict";
var w = 600;
var h = 300;
var barPadding = 3;
var margin=20;
var format = d3.format(",.0f");
var x = d3.scale.ordinal().rangeBands([0, w-margin], .1),
y = d3.scale.linear().range([h-margin, margin]);
var xAxis = d3.svg.axis().scale(x).orient("top").tickSize(0),
yAxis = d3.svg.axis().scale(y).orient("left").tickSize(-w);
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.append("g")
.attr("transform", "translate(" + margin + ",0)");
// Set the scale domain.
x.domain(d3.range(data.Low_Income_Household_Internet_access.length));
y.domain([0, d3.max(data.Low_Income_Household_Internet_access,function(d) { return d.Value; }) ]);
var bar = svg.selectAll("g.bar")
.data(data.Low_Income_Household_Internet_access)
.enter().append("g")
.attr("class", "bar")
.attr("transform", function(d) { return "translate(" + x(d.Country)+ "," + y(d.Value) + " ) "; });
bar.append("rect")
.attr("width", x.rangeBand())
.attr("height", function(d) { return h-margin- y(d.Value); })
.on("mouseover",function(d) {
d3.select(this)
.style("fill","red")
})
.on("mouseout",function(d) {
d3.select(this)
.style("fill","grey")
})
.append("svg:title")
.text(function(d) { return "" + d.Country + ": " + d.Value + " %"; });
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0, 295)")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);}