Newline in tickFormat in D3 - d3.js

I have code that I've created and I'm trying to format the Y data in a chart to show it in two lines.
var xScale = d3.scaleLinear().domain([0, 12]).range([0, width]),
yScale = d3.scaleLinear().domain([0, 3]).range([height, 0]);
var x_axis = d3
.axisBottom()
.scale(xScale)
.tickValues([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
.tickFormat((d, i) => [`Styczeń 2023`, "Luty 2023", "Marzec 2023", "Kwiecień 2023", "Maj 2023", "Czerwiec 2023", "Lipiec 2023", "Sierpień 2023", "Wrzesień 2023", "Październik 2023", "Listopad 2023", "Grudzień 2023"][i]);
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(x_axis);
I am working on version v5 and here it is rather not advisable for me to update to a higher one.
I need the year to be displayed below the month.

Related

d3 - Bar Chart overlapping my text, how can I reduce the domain so it doesn't?

const bar3 = d3.select('#bar3').append('svg').attr('width', 800).attr('height', 225);
var bar1Data = [{
position: 1,
label: '70k',
value: 100
},
{
position: 2,
label: '71K - 149K',
value: 200
},
{
position: 3,
label: '71K - 149K',
value: 300
},
{
position: 4,
label: '71K - 149K',
value: 100
},
];
var colorScale = d3.scaleOrdinal()
.range(["#FFC828", "#FFAC27", '#FF8F26', '#FF7125']);
const max = d3.sum(bar1Data, d => d.value)
console.log(max)
const maximum = d3.scaleLinear()
.domain([0, d3.max(bar1Data, d => d.value)])
.range([200, 0])
//Join data to rects
const rects = bar3.selectAll('rect')
.data(bar1Data)
rects
.attr('width', 196)
.attr('height', d => 225 - maximum(d.value))
.attr('x', 20)
.attr('y', 0)
rects.enter()
.append('rect')
.attr('width', 196)
.attr('height', d => 225 - maximum(d.value))
.attr('x', (d, i) => i * 199)
.attr('y', d => maximum(d.value))
.style("fill", (d, i) => colorScale(i));
function sum(array, start, end) {
var total = 0;
for (var i = start; i < end; i++) total += array[i];
return total;
}
bar3.append("text")
.attr("x", (400))
.attr("y", 80)
.attr("text-anchor", "middle")
.style('font-family', 'marsBook')
.style("font-size", "24px")
.text("Generation of Head of Household")
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="bars">
<div id="bar3"></div>
</div>
Hi,
I am struggling to get my bar chart to be lower than the text field. I'm not sure what I've done, but the chart keeps overlapping the title and I'd like that to not be the case. Preferably, keeping all things in the #bar3 container and lowering the domain. But it hasn't seemed to work for me.
You need to change not the domain (which is the set of values the data can take), but the range. Specifically, the 200 in maximum denotes 200 pixels from the top, and 0 denotes the very top. So changing that fixes it:
const bar3 = d3.select('#bar3').append('svg').attr('width', 800).attr('height', 225);
var bar1Data = [{
position: 1,
label: '70k',
value: 100
},
{
position: 2,
label: '71K - 149K',
value: 200
},
{
position: 3,
label: '71K - 149K',
value: 300
},
{
position: 4,
label: '71K - 149K',
value: 100
},
];
var colorScale = d3.scaleOrdinal()
.range(["#FFC828", "#FFAC27", '#FF8F26', '#FF7125']);
const max = d3.sum(bar1Data, d => d.value)
console.log(max)
const maximum = d3.scaleLinear()
.domain([0, d3.max(bar1Data, d => d.value)])
.range([200, 50])
//Join data to rects
const rects = bar3.selectAll('rect')
.data(bar1Data)
rects
.attr('width', 196)
.attr('height', d => 225 - maximum(d.value))
.attr('x', 20)
.attr('y', 0)
rects.enter()
.append('rect')
.attr('width', 196)
.attr('height', d => 225 - maximum(d.value))
.attr('x', (d, i) => i * 199)
.attr('y', d => maximum(d.value))
.style("fill", (d, i) => colorScale(i));
function sum(array, start, end) {
var total = 0;
for (var i = start; i < end; i++) total += array[i];
return total;
}
bar3.append("text")
.attr("x", 400)
.attr("y", 40)
.attr("text-anchor", "middle")
.style('font-family', 'marsBook')
.style("font-size", "24px")
.text("Generation of Head of Household")
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="bars">
<div id="bar3"></div>
</div>
Another option is to plot the bars not directly on the svg, but to use a g-child element instead, and give that some margin at the top:
const barContainer = bar3.append('g').attr('transform', 'translate(20, 40)');
And then append the bars to the barContainer and the text to the svg. That allows you to use 0 in the range again.

Enter/update/exit on two-levels, without nesting

To perform enter/update/exit actions on the corresponding sets, we can write the following code, paraphrased from an example by Bostock.
Here each number appears in a p node.
function update(data) {
var text = d3.select('body')
.selectAll('p')
.data(data);
text.enter()
.append('p')
.merge(text)
.text(d => d);
text.exit().remove();
}
update([1, 2]);
update([3, 4, 5, 6]);
update([7, 8, 9]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Now suppose that we want each p node to be itself inside a div node. This ought to be a simple extension since we need no nesting. But the following obvious modification is flawed.
function update(data) {
var text = d3.select('body')
.selectAll('div')
.data(data);
text.enter()
.append('div')
.append('p')
.text(d => d)
.merge(text);
text.exit()
.remove();
}
update([1, 2]);
update([3, 4, 5, 6]);
update([7, 8, 9]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Why?
Since your update selection refers to <div> elements, not to <p> elements, remove the merge() method. Of course, this has the undesired effect of requiring duplicate code (specifically .text(d => d)):
text.enter()
.append('div')
.append('p')
.text(d => d);
text.select('p')
.text(d => d);
Here is your code with those changes:
function update(data) {
var text = d3.select('body')
.selectAll('div')
.data(data);
text.enter()
.append('div')
.append('p')
.text(d => d);
text.select('p')
.text(d => d);
text.exit()
.remove();
}
update([1, 2]);
setTimeout(function() {
update([3, 4, 5, 6])
}, 1000);
setTimeout(function() {
update([7, 8, 9])
}, 2000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Another approach, admittedly awkward, is creating a simple selection of divs and using html to append the paragraphs with the values:
text.enter()
.append('div')
.merge(text)
.html(d => '<p>'+ d + '</p>');
Pay attention to the fact that you have to set the values after merge, just like in your first snippet.
Here is the running code:
function update(data) {
var text = d3.select('body')
.selectAll('div')
.data(data);
text.enter()
.append('div')
.merge(text)
.html(d => '<p>' + d + '</p>');
text.exit()
.remove();
}
update([1, 2]);
setTimeout(function() {
update([3, 4, 5, 6])
}, 1000);
setTimeout(function() {
update([7, 8, 9])
}, 2000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Finally, regarding your question:
Why?
Your second snippet kind of works, if you move the text to after the merge:
function update(data) {
var text = d3.select('body')
.selectAll('div')
.data(data);
text.enter()
.append('div')
.append('p')
.merge(text)
.text(d => d);
text.exit()
.remove();
}
update([1, 2]);
setTimeout(function() {
update([3, 4, 5, 6])
}, 1000);
setTimeout(function() {
update([7, 8, 9])
}, 2000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
However, as you can see, it's removing the inner <p> elements in the updates. That happens because your update selection is a selection of divs, so when you use text (which internally uses textContent) you're actually removing everything previously appended to those divs.
PS: in both S.O. snippets, make sure that show console is not checked, otherwise the code will select it as a <div> (S.O. snippet uses a <div> to show the console).

Unable Add multiple dataset in d3.v4.min.js

Try to add multiple dataset in graph, I am using below code because I don't want
line break even after null value in my data, please help me out , I am new in chart development.
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
</head>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var w = 500,
h = 300;
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var data = [
[10, 50],
[20, 70],
[30, 20],
[40, 60],
[50, 40],
[60, null],
[70, null],
[80, null],
[90, 90],
[100, 20]
];
var xScale = d3.scaleLinear().domain([0, 100]).range([30, w - 20]);
var yScale = d3.scaleLinear().domain([0, 100]).range([h - 20, 20]);
var counter;
var line = d3.line().x(function (d) {
return xScale(d[0]);
})
.y(function (d) {
return yScale(d[1]);
})
.defined(function (d) {
return d[1] || d[1] === '0';
});
var filteredData = data.filter(line.defined());
svg.append("path")
.attr("d", line(filteredData))
.attr("stroke-width", 2)
.attr("stroke", "teal")
.attr("fill", "none");
var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale);
svg.append("g")
.attr("transform", "translate(0," + (h - 20) + ")")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(30,0)")
.call(yAxis);
</script>
</body>
</html>

shapes overlay instead of transition in d3.js

I tried to have a beeswarm plot shift datapoint location to no avail. First, I made a simple scatter plot which can transition except x axis overlays on toggling.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>D3: Transitioning points to randomized values, plus rescaled axes!</title>
<script src="https://d3js.org/d3.v4.min.js"></script><style type="text/css">
/* No style rules here yet */
</style>
</head>
<body>
<label>
<input type="radio" name='market' value="a" checked/>a
<input type="radio" name='market' value="b"/>b
</label>
<p>Click on this text to update the chart with new data values as many times as you like!</p>
<script type="text/javascript">
//Width and height
var w = 500;
var h = 300;
var padding = 30;
//Dynamic, random dataset
dataset = [
{"id":1,
"value":20,
"group":"a"},
{"id":1,
"value":10,
"group":"a"},
{"id":1,
"value":30,
"group":"a"},
{"id":1,
"value":40,
"group":"a"},
{"id":1,
"value":42,
"group":"a"},
{"id":1,
"value":10,
"group":"b"},
{"id":1,
"value":12,
"group":"b"},
{"id":1,
"value":15,
"group":"b"},
{"id":1,
"value":23,
"group":"b"},
{"id":1,
"value":22,
"group":"b"},
{"id":1,
"value":54,
"group":"b"}
]
//Create scale functions
var xScale = d3.scaleLinear()
.range([padding, w - padding * 2]);
var yScale = d3.scaleLinear()
.range([h - padding, padding]);
//Define X axis
var xAxis = d3.axisBottom()
.scale(xScale)
.ticks(5);
//Define Y axis
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
draw(dataset.filter(d=> d.group=="a"));
//////////////////
//toggle//
//////////////////
d3.selectAll("input")
.on("change", function() {
var data_new = dataset.filter(d => (d.group == this.value));
draw(data_new);
});
//////////////////
//Create circles
//////////////////
function draw(dataset) {
//////////////////
//Create axis
xScale.domain([0, d3.max(dataset, function(d) { return d.value; })])
yScale.domain([0, 2])
var xAxis = d3.axisBottom()
.scale(xScale)
.ticks(5);
//Create X axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (h - padding) + ")")
.transition(2000)
.call(xAxis);
//Create Y axis
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);
//////////////////
//draw circle
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) {
return xScale(d.value);
})
.attr("cy", function(d) {
return yScale(d.id);
})
.attr("r", 3)
.attr("opacity",0.2);
//update
svg.selectAll("circle")
.transition()
.duration(1000)
.attr("cx", function(d) {
return xScale(d.value);
})
}
</script>
</body>
</html>
However when apply a similar code for beeswarm plot, the points don't shift location on selection, they just layer on, like the x axis in the first example:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
</style>
<label>
<input type="radio" name='market' value="a" checked/>a
<input type="radio" name='market' value="b"/>b
</label>
<svg width="400" height="200"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var svg = d3.select("svg"),
margin = {top: 40, right: 40, bottom: 40, left: 40},
width = svg.attr("width") - margin.left - margin.right,
height = svg.attr("height") - margin.top - margin.bottom;
var formatValue = d3.format(",d");
var x = d3.scaleLog()
.rangeRound([0, width]);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
data = [
{"id":1,
"value":20,
"group":"a"},
{"id":1,
"value":21,
"group":"a"},
{"id":1,
"value":30,
"group":"a"},
{"id":1,
"value":32,
"group":"a"},
{"id":1,
"value":42,
"group":"a"},
{"id":1,
"value":10,
"group":"b"},
{"id":1,
"value":12,
"group":"b"},
{"id":1,
"value":15,
"group":"b"},
{"id":1,
"value":23,
"group":"b"},
{"id":1,
"value":22,
"group":"b"},
{"id":1,
"value":24,
"group":"b"}
]
//default
draw(data.filter(d=> d.group=="a"));
d3.selectAll("input")
.on("change", function()
{
var newdata = draw(data.filter(d=> d.group==this.value));
draw(newdata)
} )
/////////////////
//draw swarmplot
/////////////////
function draw(data) {
// transition
var t = d3.transition()
.duration(750);
x.domain(d3.extent(data, d=> d.value));
var simulation = d3.forceSimulation(data)
.force("x", d3.forceX(function(d) { return x(d.value); }).strength(1))
.force("y", d3.forceY(height / 3))
.force("collide", d3.forceCollide(18))
.stop();
for (var i = 0; i < 120; ++i) simulation.tick();
//axis
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).ticks(20, ".0s"));
//for mouse-over
var cell = g.append("g")
.attr("class", "cells")
.selectAll("g").data(d3.voronoi()
.extent([[-margin.left, -margin.top], [width + margin.right, height + margin.top]])
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.polygons(data)).enter().append("g");
//circle
cell.append("circle")
.attr("r", 3)
.attr("cx", function(d) { return d.data.x; })
.attr("cy", function(d) { return d.data.y; })
.attr("fill", d => (d.data.Food_Sub_Group))
.attr("opacity", 0.4);
//update circle
cell.selectAll("circle")
.transition()
.duration(1000)
.attr("cx", function(d) { return d.data.x; })
}
</script>
Anything amiss here? Thanks.
Here is the difference:
In draw function of the first code, you bind data to 'circle' elements. If you have 5 circle elements before and new data size is 5, then no element is added, what changed is only data. If you have 5 circles before and new data size is 6, then with 'enter()' and 'append' function, after execution, you will have 6 elements.
In draw function of the second code, you add new elements. Every call to draw function adds new points. If you have 5 circle elements before and new data size is 5, then 5 elements are added, you will have 10 elements.
What you say 'no shift location' is actually multiple points be plotted in same place(the point seems to be darker) or different places(more points).
See data binding in d3 which may help.

How to filter elements and hide others?

I modified some d3 filtering blocks with the intention to filter by year (ideally through a slider instead of having a button for each year) but encountered two issues:
upon calling filter() and exit().remove() nothing happens
how can i add a slider or autoplay across the years? I looked through some examples but they're a bit complex for beginner.
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://d3js.org/d3.v4.min.js"></script>
<meta charset="utf-8">
<title>D3: Loading data from a CSV file</title>
</head>
<body>
<p class="s12"> select 2012 </p>
<p class="s16"> select 2016 </p>
<script type="text/javascript">
var margin = {top: 20, right: 20, bottom: 30, left: 40},
w = 600 - margin.left - margin.right,
h = 300 - margin.top - margin.bottom;
var padding = 40;
var data = [{
"Food": "Apples",
"Deliciousness": 9,
"year": 2010
}, {
"Food": "Green Beans",
"Deliciousness": 5,
"year": 2012
}, {
"Food": "Egg Salad Sandwich",
"Deliciousness": 4,
"year": 2016
}, {
"Food": "Cookies",
"Deliciousness": 10,
"year": 2018
} ];
// format the data
data.forEach(function(d) {
d.Deliciousness = +d.Deliciousness;
});
var svg = d3.select("body")
.append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left+"," +
margin.top+")");
var xScale = d3.scaleBand()
.domain(d=>d.Food)
.range([margin.left,w+margin.left])
.paddingInner(0.2);
xScale.domain(data.map(function(d) { return d.Food; }));
var xAxis = d3.axisBottom()
.scale(xScale)
.ticks(5);
var yScale = d3.scaleLinear()
.domain([0, d3.max(data, d=>d.Deliciousness)])
.rangeRound([h+margin.top,margin.top]);
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5);
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x',(d,i) => margin.left + i*w/data.length)
.attr('y',d=>yScale(d.Deliciousness))
.attr('width', xScale.bandwidth())
.attr('height',d =>h-yScale(d.Deliciousness))
.attr('fill',function(d){
if (d===30) return "red";
return "rgb(0,0,"+d.Deliciousness*10+")" ;});
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
d3.select(".s12")
.on("click", function() {
svg.selectAll("rect")
.filter(function(d) {
return d.year !== 2012;
})
.remove();
d3.select(".s16")
.on("click", function() {
svg.selectAll("rect")
.filter(function(d) {
return d.year !== 2016;
})
.remove();
});
</script>
</body>
</html>
code updated
Firstly, fix the class problem explained in the comments section.
Your problem here is the logic of this block:
svg.selectAll("rect")
.data(data.filter(function(d) {
return d.year == 2012;
}))
.exit().remove();
It makes little sense: what you're doing is filtering the data array by year (2012), which will return just 1 object. Then, you're selecting all the rectangles, binding a new data array with just one object (which will leave all the other rectangles without any data) and then calling remove on the exit selection. That's not correct.
Since you don't have a new data, just filter the selection:
svg.selectAll("rect")
.filter(function(d) {
return d.year !== 2012;
})
.remove();
That way, you simply remove the rectangles that don't represent 2012.
This is the updated code:
var margin = {
top: 20,
right: 20,
bottom: 30,
left: 40
},
w = 600 - margin.left - margin.right,
h = 300 - margin.top - margin.bottom;
var padding = 40;
var data = [{
"Food": "Apples",
"Deliciousness": 9,
"year": 2010
}, {
"Food": "Green Beans",
"Deliciousness": 5,
"year": 2012
}, {
"Food": "Egg Salad Sandwich",
"Deliciousness": 4,
"year": 2016
}, {
"Food": "Cookies",
"Deliciousness": 10,
"year": 2018
}];
// format the data
data.forEach(function(d) {
d.Deliciousness = +d.Deliciousness;
});
var svg = d3.select("body")
.append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," +
margin.top + ")");
var xScale = d3.scaleBand()
.domain(d => d.Food)
.range([margin.left, w + margin.left])
.paddingInner(0.2);
xScale.domain(data.map(function(d) {
return d.Food;
}));
var xAxis = d3.axisBottom()
.scale(xScale)
.ticks(5);
var yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.Deliciousness)])
.rangeRound([h + margin.top, margin.top]);
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5);
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', (d, i) => margin.left + i * w / data.length)
.attr('y', d => yScale(d.Deliciousness))
.attr('width', xScale.bandwidth())
.attr('height', d => h - yScale(d.Deliciousness))
.attr('fill', function(d) {
if (d === 30) return "red";
return "rgb(0,0," + d.Deliciousness * 10 + ")";
});
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
d3.select(".s12")
.on("click", function() {
svg.selectAll("rect")
.filter(function(d) {
return d.year !== 2012;
})
.remove();
});
<script src="https://d3js.org/d3.v4.min.js"></script>
<p class="s12"> select 2012 </p>
<p class="s16"> select 2016 </p>

Resources