I open this thread because i started learning D3js but i have a problem using the scaleOrdinal function on my website. As you can see from the screenshot below the numbers are not linked properly to the colors i want displayed (i.e. the highest number has the second lightest color).
I checked many times the HTML and JS code and everything seems to be correct:
HTML
<div >
<div >
<table id="postings-table">
<th>Postings rank</th>
<th>User name</th>
<th>User code</th>
<th>Grade</th>
<th>Documents loaded</th>
<th>Postings loaded</th>
<% for (let row of firstResults) {%>
<tr>
<td><%= row.postings_rank %></td>
<td><%= row.fullname.slice(0,26) %></td>
<td><a href="<%=`/postings?glcode=&description=&startregdate=&endregdate=&starteffdate=&endeffdate=&costcenter=&docnum=&usercode=${row.user_code}&acccenter=&ordernum=&paymode=&supplcode=&countrycode=&fs_pos=`%>"><%= row.user_code %></a></td>
<td><%= row.grade_description.slice(0,21) %></td>
<td><%= Number(row.doc_count).toLocaleString("it-IT", {maximumFractionDigits:0}) %></td>
<td><%= Number(row.postings_count).toLocaleString("it-IT", {maximumFractionDigits:0}) %></td>
</tr>
<% } %>
</table>
</div>
</div>
JS
const color_scale = d3.scaleOrdinal().range(['#f0f9e8','#bae4bc','#7bccc4','#43a2ca','#0868ac']).domain(d3.extent(firstResults, d => d.postings_count));
d3.selectAll("#postings-table td:last-of-type")
.data(firstResults).style("background-color", (d, i, n) => color_scale(d.postings_count));
Can you please tell me what am i doing wrong? I have really no idea why the colors are not linked properly.
CodePudding user response:
The behavior you are seeing is expected, you are using an ordinal scale for data that is not ordinal.
d3.extent returns the min and max of an array - but the domain of an ordinal scale (since it is for ordinal data) requires each value of the domain to be defined. If you pass a value outside of the domain to the scale it is assigned the next unassigned value in the range.
You could keep the ordinal scale if you pass every scalable value to the domain and sorting those values.
d3.scaleOrdinal()
.range(['#f0f9e8','#bae4bc','#7bccc4','#43a2ca','#0868ac'])
.domain(firstResults
.map(d => d.postings_count)
.sort((a, b) => a - b)
);
If there are more values in the domain than range, some colors in the range will be reused.
But it is probably better to use a sequential scale:
Sequential scales, like diverging scales, are similar to continuous scales in that they map a continuous, numeric input domain to a continuous output range.
let scale = d3.scaleSequential(d3.interpolateGnBu)
.domain([0,20]);
let svg = d3.select('body')
.append('svg')
.attr('width', 500)
svg.selectAll(null)
.data(d3.range(20).map(d=>Math.random()*20))
.enter()
.append('rect')
.attr('x', (d,i)=> i*20)
.attr('y', 10)
.attr('width',18)
.attr('height',18)
.attr('fill', d=>scale(d));
svg.selectAll(null)
.data(d3.range(20))
.enter()
.append('rect')
.attr('x', (d,i)=> i*20)
.attr('y', 30)
.attr('width',18)
.attr('height',18)
.attr('fill', d=>scale(d));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
First row is random data, second ordered to show interpolation
You can create a custom color interpolator to tweak the colors how you want.
Alternatively you could use a linear/continuous scale and provide the same color values you have used in your ordinal scale domain, which you could compute using the extent of values.
The above two options don't provide for discrete color options for any given input. However, you could use a threshold scale to provide a discrete color for any given input, for this you would need a linear scale:
let scale = d3.scaleThreshold()
.range(['#f0f9e8','#bae4bc','#7bccc4','#43a2ca','#0868ac'])
.domain([4,8,12,16]);
let svg = d3.select('body')
.append('svg')
.attr('width', 500)
svg.selectAll(null)
.data(d3.range(20).map(d=>Math.random()*20))
.enter()
.append('rect')
.attr('x', (d,i)=> i*20)
.attr('y', 10)
.attr('width',18)
.attr('height',18)
.attr('fill', d=>scale(d));
svg.selectAll(null)
.data(d3.range(20))
.enter()
.append('rect')
.attr('x', (d,i)=> i*20)
.attr('y', 30)
.attr('width',18)
.attr('height',18)
.attr('fill', d=>scale(d));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
As above, first row is random data, second ordered to show interpolation
The threshold scale takes one less item in the domain array than the range - as explained in the docs.