d3 Change colour of tick on axis with function - d3.js

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

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 multiple circles per tick from grouped data array

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>

Creating semi circular a Gauge using NVd3

I have a project in which I am using nvd3.js and want to create a semi-circular gauge. How do I create it?
I did try to use guage.js for the same but the problem I am facing there is that I cannot add customized/tags labels for the gauge that are of string type and I could not find a way around the problem. Any help is appreciated.
This is what I came up with. It's not perfect, the tooltips are rather meaningless. Also I've been trying to come up with a gradient fill to give it a 3D look, but haven't managed to yet.
<div style="height: 200px; width: 350px;">
<div style="height: 350px; width: 350px;">
<svg id="gaugeContainer"></svg>
</div>
</div>
<script>
//d3.json("/api/gauge", updateChart);
updateChart({ "max": 100, "current": 90 });
function updateChart(data) {
var containerSvg = "gaugeContainer";
var height = 350;
var width = 350;
// The first graph section always starts at 0 degrees, but we
// want it to be horizontal so we rotate the whole thing -90
// degrees, then correct the labels.
// The formula for needle angle is ((current/max) * 180) - 90
// current / max will always be 1.0 or less,
// the fraction of max that the needle indicates,
// * 180 converts to degrees and -90 compensates for rotation.
var indicatorAngle = ((data.current / data.max) * 180) - 90;
var colors = ["green", "orange", "red", "white"];
// Apparently after 4 sections are defined, the fifth one is
// dead, as in not graphed. We play a little trick, defining a
// very small white section so we can display
// the max value at the end of its visible range.
// The key values could just as easily be strings.
var graphData = [
{ key: Math.round((data.max / 8) * 1), y: 1 },
{ key: Math.round((data.max / 8) * 3), y: 1 },
{ key: Math.round((data.max / 8) * 6), y: 2 },
{ key: Math.round((data.max / 8) * 8), y: 0.2 },
{ key: "", y: 3.8 }
];
var arcRadius = [
{ inner: 0.6, outer: 1 },
{ inner: 0.6, outer: 1 },
{ inner: 0.6, outer: 1 },
{ inner: 0.6, outer: 1 }
];
nv.addGraph(function () {
var chart = nv.models.pieChart()
.x(function (d) { return d.key })
.y(function (d) { return d.y })
.donut(true)
.showTooltipPercent(false)
.width(width)
.height(height)
.color(colors)
.arcsRadius(arcRadius)
.donutLabelsOutside(true)
.showLegend(false)
.growOnHover(false)
.labelSunbeamLayout(false);
d3.select("#" + containerSvg)
.datum(graphData)
.transition().duration(1)
.attr('width', width)
.attr('height', height)
.attr("transform", "rotate(-90)")
.call(chart);
// draw needle
d3.select("#" + containerSvg)
.append("path")
.attr("class", "line")
.attr("fill", "none")
.style("stroke", "gray")
.style("stroke-width", "1")
.attr("d", "M0, -3 L153, 0 0,3 Z")
.attr("transform", "translate(175,179) rotate(" + indicatorAngle + ")");
d3.select("#" + containerSvg)
.append("circle")
.attr("r", "6")
.attr("cx", "0")
.attr("cy", "0")
.style("stroke", "gray")
.style("stroke-width", "1")
.attr("transform", "translate(175,179) rotate(" + indicatorAngle + ")");
// correct text rotation
d3.select("#" + containerSvg).selectAll("text")
.attr("transform", "rotate(90)");
return chart;
});
}
</script>

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 append html element as child programmatically [duplicate]

I'm trying to get the values for x and y to make a circles using d3.js v4. With the following code I manage to create the chart like behavior of the circles, but when I try to run the same code in v4 it doesn't work anymore. I know that there are some differences in the update to v4 but I didn't find any information about it. So i was wondering if someone can help me to run this code in d3.js v4.
Here is the code using v3 (it will break using v4):
var svg = d3.select('body').append('svg')
.attr('width', 250)
.attr('height', 250);
//render the data
function render(data) {
//Bind
var circles = svg.selectAll('circle').data(data);
//Enter
circles.enter().append('circle')
.attr('r', 10);
//Update
circles
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
//Exit
circles.exit().remove();
}
var myObjects = [{
x: 100,
y: 100
}, {
x: 130,
y: 120
}, {
x: 80,
y: 180
}, {
x: 180,
y: 80
}, {
x: 180,
y: 40
}];
render(myObjects);
<script src='https://d3js.org/d3.v3.min.js'></script>
This is the expected behaviour, and I've explained this before in this answer (not a duplicate, though).
What happened is that Mike Bostock, D3 creator, introduced a magic behaviour in D3 v2, which he kept in D3 v3.x, but decided to abandon in D3 v4.x. To read more about that, have a look here: What Makes Software Good? This is what he says:
D3 2.0 introduced a change: appending to the enter selection would now copy entering elements into the update selection [...] D3 4.0 removes the magic of enter.append. (In fact, D3 4.0 removes the distinction between enter and normal selections entirely: there is now only one class of selection.)
Let's see it.
Here is your code with D3 v3:
var svg = d3.select('body').append('svg')
.attr('width', 250)
.attr('height', 250);
//render the data
function render(data) {
//Bind
var circles = svg.selectAll('circle').data(data);
//Enter
circles.enter().append('circle')
.attr('r', 10);
//Update
circles
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
//Exit
circles.exit().remove();
}
var myObjects = [{
x: 100,
y: 100
}, {
x: 130,
y: 120
}, {
x: 80,
y: 180
}, {
x: 180,
y: 80
}, {
x: 180,
y: 40
}];
render(myObjects);
<script src='https://d3js.org/d3.v3.min.js'></script>
Now the same code, with D3 v4. It will "break":
var svg = d3.select('body').append('svg')
.attr('width', 250)
.attr('height', 250);
//render the data
function render(data) {
//Bind
var circles = svg.selectAll('circle').data(data);
//Enter
circles.enter().append('circle')
.attr('r', 10);
//Update
circles
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
//Exit
circles.exit().remove();
}
var myObjects = [{
x: 100,
y: 100
}, {
x: 130,
y: 120
}, {
x: 80,
y: 180
}, {
x: 180,
y: 80
}, {
x: 180,
y: 40
}];
render(myObjects);
<script src='https://d3js.org/d3.v4.min.js'></script>
By "break" I mean the circles will be appended, but they will not receive the x and y properties in the "enter" selection, and they will default to zero. That's why you see all circles at the top left corner.
Solution: merge the selections:
circles.enter().append('circle')
.attr('r', 10)
.merge(circles) //from now on, enter + update
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
According to the API, merge()...
... is commonly used to merge the enter and update selections after a data-join. After modifying the entering and updating elements separately, you can merge the two selections and perform operations on both without duplicate code.
Here is the code with merge():
var svg = d3.select('body').append('svg')
.attr('width', 250)
.attr('height', 250);
//render the data
function render(data) {
//Bind
var circles = svg.selectAll('circle').data(data);
//Enter
circles.enter().append('circle')
.attr('r', 10)
.merge(circles) //from now on, enter + update
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
//Exit
circles.exit().remove();
}
var myObjects = [{
x: 100,
y: 100
}, {
x: 130,
y: 120
}, {
x: 80,
y: 180
}, {
x: 180,
y: 80
}, {
x: 180,
y: 40
}];
render(myObjects);
<script src='https://d3js.org/d3.v4.min.js'></script>
Update pattern was changed in d3v4.
Excerpt from the documentation:
In addition, selection.append no longer merges entering nodes into the
update selection; use selection.merge to combine enter and update
after a data join.
You should rewrite your code this way:
var svg = d3.select('body').append('svg')
.attr('width', 250)
.attr('height', 250);
//render the data
function render(data){
//Bind
var circles = svg.selectAll('circle').data(data);
//Enter
circles.enter().append('circle')
.attr('r', 10).merge(circles) // <== !!!
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; });
//Exit
circles.exit().remove();
}
var myObjects = [
{x: 100, y: 100},
{x: 130, y: 120},
{x: 80, y: 180},
{x: 180, y: 80},
{x: 180, y: 40}
];
render(myObjects);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.11.0/d3.min.js"></script>

Resources