d3 multiple circles per tick from grouped data array - d3.js

I'm trying to learn d3 very quickly and I'm getting pretty stuck on selection and joining.
I want to be able to draw an axis with dots for each member of an array. Some of the array members have the same x value, but I still want to see as many dots as there are with that value. My array (in React with useState) looks like so:
const [data, setData] = useState(
[
{x: 2020, colour: "purple", y1: 0.001, y2: 0.63},
{x: 2027, colour: "red", y1: 0.003, y2: 0.84},
{x: 2031, colour: "yellow", y1: 0.024, y2: 0.56},
{x: 2031, colour: "green", y1: 0.054, y2: 0.22},
{x: 2040, colour: "blue", y1: 0.062, y2: 0.15},
{x: 2050, colour: "orange", y1: 0.062, y2: 0.15}
]
);
You can see there are two values for 2031 and I want to draw a yellow dot, then a purple dot below, at the x axis tick labelled "2031".
So I group my data with this reduce function (purloined from SO):
const dot = data.reduce(
(r, v, _, __, k = v.x) => ((r[k] || (r[k] = [])).push(v), r),
{}
);
...which produces this:
{ 2020: [{x: 2020, colour: "purple", y1: 0.001, y2: 0.63}],
2027: [...] }
I initiate my x axis and create a placeholder for it:
const g = d3.axisBottom( scX ).tickValues(
data.map(d => {
return d.x
})
)
svg.append( "g" )
.attr( "transform", "translate(" + 25 + "," + pxY/2 + ")")
.call( g )
.selectAll(".tick")
And then I want to call my dot variable and iterate over the nested arrays:
svg
.selectAll(".tick")
.call( dot )
.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", 5)
.attr("fill", dot.colour)
What I would like this to do is draw a circle for every one of the nested arrays, with a fill the colour named in that array - this does not work?
Could anyone explain?

There is no need to group your data. You can just see data as an array, where every element will correspond to one circle. Multiple circles can exist with the same x-value, nothing enforces that they can't.
There is also no need to set the axis ticks like that, d3 will most likely do everything for you. d3-axis is an absolute convenience - you're meant to tweak the defaults, not build everything from scratch here.
You need to learn about data joins, since you apparently also don't know that you can access the data of an element using function(d, i) { ... } or (d, i) => ... to set the colour that way.
const data = [{
x: 2020,
colour: "purple",
y1: 0.001,
y2: 0.63
},
{
x: 2027,
colour: "red",
y1: 0.003,
y2: 0.84
},
{
x: 2031,
colour: "yellow",
y1: 0.024,
y2: 0.56
},
{
x: 2031,
colour: "green",
y1: 0.054,
y2: 0.22
},
{
x: 2040,
colour: "blue",
y1: 0.062,
y2: 0.15
},
{
x: 2050,
colour: "orange",
y1: 0.062,
y2: 0.15
}
];
const width = 600,
height = 300;
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
const x = d3.scaleLinear()
.domain(d3.extent(data, d => d.x))
.range([50, 550]);
const y1 = d3.scaleLinear()
.domain(d3.extent(data, d => d.y1))
.range([275, 25]);
const y2 = d3.scaleLinear()
.domain(d3.extent(data, d => d.y2))
.range([3, 10]);
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("fill", d => d.colour)
.attr("cx", d => x(d.x))
.attr("cy", d => y1(d.y1))
.attr("r", d => y2(d.y2));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>

Related

How to draw a multi-line chart with d3 version7 graph with string in x-axis?

I am a working on multiline chart using d3 version7 , I have data like
data1: sample1[] = [
{
symbol: 'a',
x1: '2022-01-01',
y1: 150
},
{
symbol: 'c',
x1: '2022-01-01',
y1: 300
},
{
symbol: 'a',
x1: '2022-01-02',
y1: 200
},
{
symbol: 'c',
x1: '2022-01-02',
y1: 700
},
{
symbol: 'a',
x1: '2022-01-03',
y1: 750
},
{
symbol: 'c',
x1: '2022-01-03',
y1: 100
},
];
In X-axis I want to display x1 as string and group with a symbol.
I am able to plot x-axis and y-axis from below code
const x = d3.scaleBand()
.domain(this.data1.map((d: any) => { return d.x1 }))
.range([0, this.width]);
this.svg.append("g")
.attr("transform", `translate(0, ${this.height})`)
.call(d3.axisBottom(x).ticks(5));
// Add Y axis
const y = d3.scaleLinear()
.domain([0, d3.max(this.data1, ((d: any) => {
return d.y1;[![enter image description here][1]][1]
}))])
.range([this.height, 0]);
this.svg.append("g")
.call(
d3.axisLeft(y)
);
And It looks like this screenshot of axis
Now When draw a multiple line using this code
const dataNest = Array.from(
d3.group(this.data1, d => d.symbol), ([key, value]) => ({ key, value })
);
var legendSpace = this.width / dataNest.length; // spacing for the legend
dataNest.forEach((d1: any, i: any) => {
const xScale = d3
.scaleBand()
.domain(d1.value.map((d: any) => { return d.x1 }))
// .domain([0, d3.max(this.data1.map((d: any) => d.x1))])
.range([0, this.width])
// y-scale
const yScale = d3
.scaleLinear()
.domain([0, d3.max(this.data1.map((d: any) => d.y1))])
.range([this.height, 0]);
// Data line
const line = d3
.line()
.x((d: any) => d.x1)
.y((d: any) => yScale(d.y1));
this.svg.append("path")
.data(dataNest)
.join("path")
.attr("fill", "none")
.attr("class", "line")
.style("stroke", this.colors[i])
.attr("d", line(d1.value));
// Add the Legend
this.svg.append("text")
.attr("x", (legendSpace / 2) + i * legendSpace) // space legend
.attr("y", this.height + (this.margin.bottom / 2) + 15)
.attr("class", "legend") // style the legend
.style("fill", this.colors[i])
.text(d1.key);
});
I am not able to plot any line and I am getting an error saying,
Error: attribute d: Expected number, "MNaN,160LNaN,146.…".
So anyone have solution for displaying the multiline chart.
In your line generator function, you don't use the xScale to convert values to pixels.
const line = d3
.line()
.x((d: any) => d.x1)
.y((d: any) => yScale(d.y1));
should be
const line = d3
.line()
.x((d: any) => xScale(d.x1))
.y((d: any) => yScale(d.y1));
A few additional notes:
In every loop of the forEach you define a new xScale and yScale that are identical to the existing x and y used for rendering the axes. Just use x and y.
The same goes for the line generator, which should be the same for all lines.
Why do you use a forEach on the dataNest array? This is what the d3 data join is for as worked out in the snippet below.
Use an appropriate scale for the data type. scaleBand is for categorical data and something like bar charts. You have dates and should use scaleTime. If you really don't want to use a time scale, stick to scalePoint for line charts as the bandwidth is fixed to zero.
const data1 = [
{symbol: 'a', x1: '2022-01-01', y1: 150},
{symbol: 'c', x1: '2022-01-01', y1: 300},
{symbol: 'a', x1: '2022-01-02', y1: 200},
{symbol: 'c', x1: '2022-01-02', y1: 700},
{symbol: 'a', x1: '2022-01-03', y1: 750},
{symbol: 'c', x1: '2022-01-03', y1: 100},
];
const width = 500,
height = 180;
const svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
const x = d3.scalePoint()
.domain(data1.map(d => d.x1))
.range([30, width-30]);
svg.append("g")
.attr("transform", `translate(0, ${height-30})`)
.call(d3.axisBottom(x).ticks(5));
const y = d3.scaleLinear()
.domain([0, d3.max(data1, d => d.y1)])
.range([height-30, 0]);
svg.append("g")
.attr("transform", `translate(30, 0)`)
.call(d3.axisLeft(y));
const dataNest = Array.from(
d3.group(data1, d => d.symbol), ([key, value]) => ({ key, value })
);
const line = d3.line()
.x(d => x(d.x1))
.y(d => y(d.y1));
svg.selectAll("path.line")
.data(dataNest)
.join("path")
.attr("class", "line")
.attr("fill", "none")
.style("stroke", d => d.key === 'a' ? 'blue' : 'red')
.attr("d", d => line(d.value));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.3.0/d3.min.js"></script>

d3 Change colour of tick on axis with function

I have an x axis in d3 with labels and circles for each tick. I want to change the colour of each dot with a function so that the colour comes from an array.
I have a function already that gets ticks from the array and positions them on the scale, but the same logic doesn't work for changing attributes of each circle.
I would like to be able to select each circle that is a child of the .tick class and change it's stroke attribute:
svg.selectAll(".tick", "circle")
.each(d => {
var dot = d3.select(d, "circle").attr("stroke", "red")
console.log("DOT", dot)
return dot
})
I would probably change the each to a proper for loop to iterate over both the data and circles arrays by matching indexes.
How would I make the circle colours correspond to those in the objects in the array 'data'?
import React, {useState, useEffect} from 'react';
import * as d3 from 'd3';
import './App.css';
function App() {
const [currentYear, setCurrentYear] = useState(2020);
const [maxYear, setMaxYear] = useState(30);
const [data, setData] = useState(
[
{x: 2020, colour: "purple", y1: 0.001, y2: 0.63},
{x: 2027, colour: "red", y1: 0.003, y2: 0.84},
{x: 2031, colour: "yellow", y1: 0.024, y2: 0.56},
{x: 2035, colour: "green", y1: 0.054, y2: 0.22},
{x: 2040, colour: "blue", y1: 0.062, y2: 0.15},
{x: 2050, colour: "orange", y1: 0.062, y2: 0.15}
]
);
const initialiseData = () => {
const svg = d3.select( "svg" );
const pxX = svg.attr( "width" );
const pxY = svg.attr( "height" );
const makeScale = ( accessor, range ) => {
console.log("RANGE", accessor, range)
return d3.scaleLinear()
.domain( d3.extent( data, accessor ) )
.range( range )
.nice()
}
const scX = makeScale( d => d["x"], [0, pxX - 50]);
const g = d3.axisBottom( scX ).tickValues(
data.map(d => {
return d.x
})
)
svg.append( "g" )
.attr( "transform", "translate(" + 25 + "," + pxY/2 + ")")
.call( g );
svg.selectAll(".domain").attr( "visibility", "hidden" );
svg.selectAll( ".tick" )
.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", 5)
.attr("fill", "white")
.attr("stroke", "grey")
.attr("stroke-width", "4px")
svg.selectAll(".tick line")
.attr("visibility", "hidden")
svg.selectAll( ".tick text")
.attr("font-size", 20)
.attr("dy", "1.5em")
svg.selectAll(".tick", "circle")
.each(d => {
var dot = d3.select(d, "circle")
console.log("DOT", dot)
return
})
}
useEffect(() => {
if (data) {
initialiseData();
}
}, [data])
return (
<div className="App">
<svg id="demo1" width="1200" height="400" style={{background: "lightgrey"}}/>
</div>
);
}
export default App;
Your svg.selectAll( ".tick" ).append("circle") creates the circles. Using selectAll is a little like doing a for loop: it creates many elements, and each time, the data is bound to the created element.
You can provide a function to .attr() (and most other things in D3) that takes as an argument the bound data, usually written d. If you put in a selectAll, it'll be applied to each element.
See Learn D3: Joins for a more complete explanation. Putting it all together:
svg.selectAll( ".tick" )
.data(data)
.append("circle")
.attr("fill", d => d.colour) // d is an element of the data: get colour from it

Is there a better way to label each group?

I want to label each group is there a better way to do it?
Right now I am doing this:
const svgViewport = d3.select("body")
.append("svg")
.attr("width", 150)
.attr("height", 150);
let myData = [
[{
x: 30,
y: 40
},
{
x: 30,
y: 60
}
],
[{
x: 70,
y: 40
},
{
x: 70,
y: 60
}
]
];
let labelData=[{name:"group1",x:20,y:30},
{name:"group2",x:70,y:30}];
const groups = svgViewport.selectAll(null)
.data(myData)
.enter()
.append("g");
const circles = groups.selectAll(null)
.data(d => d)
.enter()
.append("circle")
.attr("cx", (d) => d.x)
.attr("cy", (d) => d.y)
.attr("r", 10);
const labels=svgViewport.selectAll("g").data(labelData).append("text").text((d)=>{return d.name;}).attr("x",(d)=>{return d.x})
.attr("y",(d)=>{return d.y});
I want to label each group of shapes can this be done without holding in the labelData x and y positions?

d3.js. How to animate throughout all data set from start to end?

I draw a circle and want to run it transition from first to the last point of data set. But can't understand how to do it. Code available here. How can i do it? What is the best practice for this kind of animation?
var data = [[{
x: 10,
y: 10,
r: 10,
color: "red"
}, {
x: 70,
y: 70,
r: 15,
color: "green"
}, {
x: 130,
y: 130,
r: 20,
color: "blue"
}]];
function setUp() {
this.attr("cx", function(d, i) {
return d[i].x;
}).attr("cy", function(d, i) {
return d[i].y;
}).attr("r", function(d, i) {
return d[i].r;
}).attr("fill", function(d, i) {
return d[i].color;
});
}
var canvas = d3.select("body")
.append("svg")
.attr("width", 300)
.attr("height", 300);
canvas.append("rect")
.attr("width", 300)
.attr("height", 300)
.attr("fill", "lightblue");
var circles = canvas.selectAll("circle")
.data(data)
.enter()
.append("circle")
.call(setUp);
Are you looking to do something like this?
var data = [[{
x: 10,
y: 10,
r: 10,
color: "red"
}], [{
x: 70,
y: 70,
r: 15,
color: "green"
}], [{
x: 130,
y: 130,
r: 20,
color: "blue"
}]];
...
var circles = canvas.selectAll("circle")
.data(data[0]);
circles
.enter()
.append("circle")
.call(setUp);
circles
.data(data[1])
.transition()
.duration(2000)
.call(setUp)
.each("end",function(){
circles
.data(data[2])
.transition()
.duration(2000)
.call(setUp);
});
Edits For Comment
If you have a variable number of points, this is a great place to use a recursive function:
// first point
var circles = canvas.selectAll("circle")
.data([data[0]]);
circles
.enter()
.append("circle")
.call(setUp);
// rest of points...
var pnt = 1;
// kick off recursion
doTransition();
function doTransition(){
circles
.data([data[pnt]])
.transition()
.duration(2000)
.call(setUp)
.each("end",function(){
pnt++;
if (pnt >= data.length){
return;
}
doTransition();
});
}
Updated example.

d3.js: dataset array w/ multiple y-axis values

I am a total beginner to d3.js so please be kind :)
considering this jsbin example
I have the following dataset:
var dataset = [
[d3.time.hour.utc.offset(now, -5), 1, 10],
[d3.time.hour.utc.offset(now, -4), 2, 20],
[d3.time.hour.utc.offset(now, -3), 3, 30],
[d3.time.hour.utc.offset(now, -2), 4, 40],
[d3.time.hour.utc.offset(now, -1), 5, 50],
[now, 6, 60],
];
Two questions.
Does d3 provide a better approach to finding the max value for my y-axis data (all columns but the 0th, the 0th column is x-axis (time)) in my dataset array? Currently I am just looping through the entire dataset array and making a second array, excluding the first column. Perhaps there is a better datastructure other than an array I should be using for this entirely?
var data_arr = [];
for (row in dataset){
for (col=1;col < dataset[row].length; col++){
data_arr.push(dataset[row][col]);
}
}
var yScale = d3.scale.linear()
.domain([0, d3.max(data_arr)])
.range([h - padding, padding]);
Once thats resolved, I still need to determine how to graph multiple y-axis values in general! This worked fine before I needed multiple y-axis values:
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
})
.attr("r", 2);
Please take a look at the graph w/ code here now for full context: http://jsbin.com/edatol/1/edit
Any help is appreciated!
I've made a couple of changes to your example and you can see the results at http://jsbin.com/edatol/2/edit.
First, I modified your data a little bit. This is mostly just a style thing, but I find it's easier to work with objects instead of arrays:
//Static dataset
var dataset = [
{x: d3.time.hour.utc.offset(now, -5), y1: 1, y2: 10},
{x: d3.time.hour.utc.offset(now, -4), y1: 2, y2: 20},
{x: d3.time.hour.utc.offset(now, -3), y1: 3, y2: 30},
{x: d3.time.hour.utc.offset(now, -2), y1: 4, y2: 40},
{x: d3.time.hour.utc.offset(now, -1), y1: 5, y2: 50},
{x: now, y1: 6, y2: 60},
];
Then you can find your domains and ranges like this:
var xDomain = d3.extent(dataset, function(i) { return i.x; });
var maxY = d3.max(dataset, function(i) { return Math.max(i.y1, i.y2); });
Then to add multiple y-values, you just have to append an additional circle with the appropriate values. I gave them different classes so that you can use that to select them if you want to do transitions or updates later on.
//Create circles
svg.selectAll(".y1")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) { return xScale(d.x); })
.attr("cy", function(d) { return yScale(d.y1); })
.attr("class", "y1")
.attr("r", 2);
//Create circles
svg.selectAll(".y2")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) { return xScale(d.x); })
.attr("cy", function(d) { return yScale(d.y2); })
.attr("class", "y2")
.attr("r", 2);

Resources