Truncate long month names in time axis - javascript

I'm trying to work out how to change the formatting for months when drawing an axis with a scaleTime data range.
I've been able to manage the layout as expected, but i'd like the full month names to be truncated. See the example below - I'd like "February" to be "Feb".
The other dates are fine.
The summary of my axis code is:
const x = scaleTime()
.domain(dateExtent)
.rangeRound([marginSize.left, width - marginSize.right]);
const xAxis = axisBottom(x)
.ticks(timeDay.every(2))
.tickSizeOuter(0);
Is there a means of providing the formatting for those date values?

The easiest and fastest alternative is just using tickFormat with the specifier you want, so all ticks will have the same structure.
However, assuming you want to change only the boundary ticks (for a format that doesn't match the adjacent ones, as you described), you can get the ticks after the axis generator created them and check their value.
For instance, an axis with February, like yours:
const svg = d3.select("svg");
const scale = d3.scaleTime()
.range([20, 580])
.domain([new Date("January 20, 2020"), new Date("February 10, 2020")]);
const axis = d3.axisBottom(scale)(svg.append("g").attr("transform", "translate(0,50)"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="100"></svg>
We get the ticks and check if the text matches the desired specifier, otherwise we change it:
d3.selectAll(".tick text").each(function(d) {
if (this.textContent !== d3.timeFormat("%a %d")(d)) this.textContent = d3.timeFormat("%b")(d);
});
Here is the result:
const svg = d3.select("svg");
const scale = d3.scaleTime()
.range([20, 580])
.domain([new Date("January 20, 2020"), new Date("February 10, 2020")]);
const axis = d3.axisBottom(scale)(svg.append("g").attr("transform", "translate(0,50)"));
d3.selectAll(".tick text").each(function(d) {
if (this.textContent !== d3.timeFormat("%a %d")(d)) this.textContent = d3.timeFormat("%b")(d);
});
svg.append("ellipse")
.attr("cx", 342)
.attr("cy", 63)
.attr("rx", 20)
.attr("ry", 8)
ellipse {
fill: none;
stroke: red;
stroke-width: 2;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="100"></svg>

Related

Add spacing between axis and start point

I want to add spacing between 0 and March in the x axis so that it looks better.
startdate and enddate are calculated by d3.min and d3.max.
So those columns looks bad because of their width.
var x = d3.scaleTime().range([0, width]);
var datestart = d3.min(data, function(d) { return parseDate(d.date); });
var dateend = d3.max(data, function(d) { return parseDate(d.date);});
x.domain([datestart, dateend]);
How can I do this? How to add a padding using d3.scaleTime()?
As you mentioned in your comment (maybe a reply?), there is no padding in a time scale.
Therefore, the problem here is one that from time to time (no pun intended) appears at SO: you are using the wrong tool for the task. Bar charts should always use a categorical (qualitative) scale, not a quantitative scale or a time scale.
However, if you really need to use the time scale here (for whatever reason), you can add the padding in the domain, using offset.
For instance, this will subtract 15 days at the beginning of your domain, and add 15 days at the end:
var datestart = d3.min(data, function(d) {
return d3.timeDay.offset(parseDate(d.date), -15);
//subtract 15 days here ---------------------^
});
var dateend = d3.max(data, function(d) {
return d3.timeDay.offset(parseDate(d.date), 15);
//add 15 days here -------------------------^
});
You can tweak that value until you have an adequate padding. As I don't know how you are calculating the width of the bars (again, bars should not be used in a time scale), 15 days is just a guess here.

Round x axis of a time scale to nearest half hour

I'm constructing a graph right now which is taking in data from a postgres backend. For the construction of the x-axis, I have the following:
var x = d3.scaleTime()
.domain(d3.extent(data_prices, function(d){
var time = timeParser(d.timestamp);
return time;
}))
.range([0,width])
where timeParser is a function representing d3.timeParse().
I have a data point which is at 16:58 and another at 22:06 and it looks a little ugly having it just stick at the side like that. How would I say, for instance, have there be a slight padding of say, +/- 30 minutes for each and continue the trendline path on each end? (or at least just the first part)
To create a padding in a time scale, use interval.offset. According to the API:
Returns a new date equal to date plus step intervals. If step is not specified it defaults to 1. If step is negative, then the returned date will be before the specified date.
Let's see it working. This is an axis based on a time scale with a min and a max similar to yours:
var svg = d3.select("svg");
var data = ["16:58", "18:00", "20:00", "22:00", "22:06"].map(function(d) {
return d3.timeParse("%H:%M")(d)
});
var scale = d3.scaleTime()
.domain(d3.extent(data))
.range([20, 580]);
var axis = d3.axisBottom(scale);
var gX = svg.append("g")
.attr("transform", "translate(0,50)")
.call(axis)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="600" height="100"></svg>
Now, to create the padding, we just need to subtract and add 30 minutes at the extremes:
var scale = d3.scaleTime()
.domain([d3.timeMinute.offset(d3.min(data), -30),
//subtract 30 minutes here --------------^
d3.timeMinute.offset(d3.max(data), 30)
//add 30 minutes here ------------------^
])
.range([20, 580]);
Here is the result:
var svg = d3.select("svg");
var data = ["16:58", "18:00", "20:00", "22:00", "22:06"].map(function(d) {
return d3.timeParse("%H:%M")(d)
});
var scale = d3.scaleTime()
.domain([d3.timeMinute.offset(d3.min(data), -30), d3.timeMinute.offset(d3.max(data), 30)])
.range([20, 580]);
var axis = d3.axisBottom(scale);
var gX = svg.append("g")
.attr("transform", "translate(0,50)")
.call(axis)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="600" height="100"></svg>
Have in mind that this solution does not round the extreme ticks to the nearest half hour: it adds and subtracts exactly half an hour.
So, to round to the nearest half hour, you can do a simple math using Math.floor and Math.ceil:
.domain([
d3.min(data).setMinutes(Math.floor(d3.min(data).getMinutes() / 30) * 30),
d3.max(data).setMinutes(Math.ceil(d3.max(data).getMinutes() / 30) * 30)
])
Here is the demo:
var svg = d3.select("svg");
var data = ["16:58", "18:00", "20:00", "22:00", "22:06"].map(function(d) {
return d3.timeParse("%H:%M")(d)
});
var scale = d3.scaleTime()
.domain([d3.min(data).setMinutes(Math.floor(d3.min(data).getMinutes() / 30) * 30), d3.max(data).setMinutes(Math.ceil(d3.max(data).getMinutes() / 30) * 30)])
.range([20, 580]);
var axis = d3.axisBottom(scale);
var gX = svg.append("g")
.attr("transform", "translate(0,50)")
.call(axis)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="600" height="100"></svg>

Create scatter plot with unequal intervals on x-axis using d3.js

I am trying to create a scatter plot with unequal intervals on the X-axis using d3.js. My CSV data is shown here partially:
chr,pos,val
22,8947,8.58891099252
22,8978,4.65541559632
22,8996,6.33685790218
22,8997,9.00384002282
22,9006,4.39533823989
MT,9471,5.0655064583
MT,9472,7.83798949399
MT,9473,0.587797595352
MT,9474,4.6475160648
MT,9475,2.52382097771
MT,9476,7.8431366396
MT,9477,1.71519736769
MT,9478,2.61168595179
MT,9479,4.15061022346
MT,9470,7.1477707428
The number of pos values for each chr value may be different. In some cases, it could be 20, in others 100 and so on. I need to create a plot of val on the y-axis vs chr on the x-axis, with the x-interval for each chr being equal to the number of pos values for that chr. Although ordinal scale for the x-axis seems suitable here, it probably doesn't support unequal intervals. With linear scale, unequal intervals can be shown using polylinear scales, but the presence of alphabetic characters in chr mean no ticks are shown. Does anyone know how I can show unequal intervals in d3.js?
UPDATE:
I have some code here for the domain and ticks using a linear scale:
const x = d3.scale.linear()
.domain(input.map((d) => {
if (d.chr === 'MT') {
return 23;
}
if (d.chr === 'X') {
return 24;
}
return d.chr;
}))
.range(xTicks);
I can't understand how to show the ticks now.With this it shows 23 and 24 instead of MT and X.
I am not sure of this part:
const xAxis = d3.svg.axis().scale(x).orient('bottom').tickValues(input.map((d) => {
if (d.chr === 'MT') {
// returning a string here shows NaN
return 23;
}
if (d.chr === 'X') {
return 24;
}
return d.chr;
}));
Here is an example of how conditionally formatting the ticks using tickFormat (not tickValues).
Suppose the data is:
19, 20, 21, 22, 23, 24;
But we are going to change 23 for "X" and 24 for "MT" in the ticks. Click "run code snippet":
var data = [19, 20, 21, 22, 23, 24];
var width = 400, height = 100;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("heigth", height);
var xScale = d3.scale.linear()
.domain(d3.extent(data))
.range([0, width*.9]);
var xAxis = d3.svg.axis()
.orient("bottom")
.ticks(6)
.tickFormat(function(d){
if(d == 23){
return "X"
} else if(d==24){
return "MT"
} else {
return d
}
})
.scale(xScale);
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(20,20)")
.call(xAxis);
.axis path,
.axis line {
fill: none;
stroke: #aaa;
shape-rendering: crispEdges;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I started at chromosome 19 just to save some space, but you can get the general idea.

Colorbrewer in D3 & Scales

I have a geoJSON looking like so
{"type":"FeatureCollection",
"crs":{"type":"name",
"properties":{"name":"urn:ogc:def:crs:OGC:1.3:CRS84"}},
"features":[{"type":"Feature",
"properties":{"scalerank":10,"natscale":1,"labelrank":8,"featurecla":"Admin-1 capital","name":"Colonia del Sacramento","namepar":null,"namealt":null,"diffascii":0,"nameascii":"Colonia del Sacramento","adm0cap":0,"capalt":0,"capin":null,"worldcity":0,"megacity":0,"sov0name":"Uruguay","sov_a3":"URY","adm0name":"Uruguay","adm0_a3":"URY","adm1name":"Colonia","iso_a2":"UY","note":null,"latitude":-34.479999,"longitude":-57.840002,"changed":4,"namediff":1,"diffnote":"Added missing admin-1 capital. Population from GeoNames.","pop_max":21714,"pop_min":21714,"pop_other":0,"rank_max":7,"rank_min":7,"geonameid":3443013,"meganame":null,"ls_name":null,"ls_match":0,"checkme":0},
"geometry":{"type":"Point","coordinates":[-57.8400024734013,-34.4799990054175]
}}]
}
I want to set to use colorbrewer to chose colors, depending on the value pop_max takes. Then I want to display this point data on a leaflet map through overlaying a svg ontop of leaflet. I can easily display the points and chose the color like so:
var feature = g.selectAll("path")
.data(collection.features)
.enter()
.append("path")
.style("fill", function(d) {
if(d.properties.pop_max) < 1000 {
return("red")
} else if {....
};
});
However, inconvenient.
So i tried:
var colorScale = d3.scale.quantize()
.range(colorbrewer.Greens[7])
.domain(0,30000000);
var feature = g.selectAll("path")
.data(collection.features)
.enter()
.append("path")
.style("fill", function(d) {
colorScale(d.properties.pop_max);
});
That does not display any points at all... Note that I estimated my domain. 0 is not necessarily the lowest number nor 30000000 the highest.
Any ideas?
First you'll need to find the max and min pop_max, something like this should work:
var extent = d3.extent(geojson.features, function(d) { return d.properties.pop_max; });
Second, since you want colors to represent 7 ranges of values you should be using d3.scale.threshold:
var N = 7,
step = (extent[1] - extent[0]) / (N + 1),
domain = d3.range(extent[0], extent[1] + step, step);
var colorScale = d3.scale.threshold()
.domain(domain)
.range(colorbrewer.Greens[N]);
EDITS
Looks like quantile can do this easier:
d3.scale.quantile()
.domain([extent[0], extent[1]])
.range(colorbrewer.Greens[N]);

Display only whole values along y axis in d3.js when range is 0-1 or 0-2

I am using d3js to display a realtime representation of the views of a website. For this I use a stack layout and I update my dataset by JSON at the moment.
When there is only 1 or 2 views being displayed on the y axis, which is dynamic related to the amount of views in the graph, the axis labels are: 1 => 0, 0.2, 0.4, 0.6, 0.8, 1, the axis labels are: 2 => 0, 0.5, 1, 1.5, 2 This makes no sense for my dataset since it displays views of a page, and you can't have half a view.
I have a linear scale in d3js I base my y axis on
var y_inverted = d3.scale.linear().domain([0, 1]).rangeRound([0, height]);
According to the documentation of rangeRound() I should only get whole values out of this scale. For drawing my axis I use:
var y_axis = svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(0,0)")
.call(y_inverted.axis = d3.svg.axis()
.scale(y_inverted)
.orient("left")
.ticks(5));
Because it is a realtime application I update this every second by calling:
function update(){
y_inverted.domain([yStackMax, 0]);
y_axis.transition()
.duration(interval)
.ease("linear")
.call(y_inverted.axis);
}
yStackMax is calculated from a stacklayout, as far as I know the data used for the y values only contain integers.
var yStackMax = d3.max(layers, function(layer) {
return d3.max(layer, function(d) {
return d.y0 + d.y;
});
});
I have tried several things to get a proper value for my y axis.
d3.svg.axis()
.scale(y_inverted)
.orient("left")
.ticks(5).tickFormat(d3.format(",.0f"))
Got me the closest sofar, but it still displays 0, 0, 0, 1, 1, 1
Basically what I want is to only have 1 tick when yStackMax is 1, 2 ticks when it's 2, but it should also work if yStackMax is 12 or 1,000,000
Short answer: You can dynamically set the number of ticks. Set it to 1 to display only two tick labels:
var maxTicks = 5, minTicks = 1;
if (yStackMax < maxTicks) {
y_axis.ticks(minTicks)
}
else {
y_axis.ticks(maxTicks)
}
Long Answer (going a bit off topic):
While playing with your example I came up with a rather "complete solution" to all your formatting problems. Feel free to use it :)
var svg = d3.select("#svg")
var width = svg.attr("width")
var height = svg.attr("height")
var yStackMax = 100000
var interval = 500
var maxTicks = 5
var minTicks = 1
var y_inverted = d3.scale.linear().domain([0, 1]).rangeRound([0, height])
var defaultFormat = d3.format(",.0f")
var format = defaultFormat
var y_axis = d3.svg.axis()
.scale(y_inverted)
.orient("left")
.ticks(minTicks)
.tickFormat(doFormat)
var y_axis_root;
var decimals = 0;
function countDecimals(v){
var test = v, count = 0;
while(test > 10) {
test /= 10
count++;
}
return count;
}
function doFormat(d,i){
return format(d,i)
}
function init(){
y_axis_root = svg.append("g")
.attr("class", "y axis")
// I modified your example to move the axis to a visible part of the screen
.attr("transform", "translate(150,0)")
.call(y_axis)
}
// custom formatting functions:
function toTerra(d) { return (Math.round(d/10000000000)/100) + "T" }
function toGiga(d) { return (Math.round(d/10000000)/100) + "G" }
function toMega(d) { return (Math.round(d/10000)/100) + "M" }
function toKilo(d) { return (Math.round(d/10)/100) + "k" }
// the factor is just for testing and not needed if based on real world data
function update(factor){
factor = (factor) || 0.1;
yStackMax*=factor
decimals = countDecimals(yStackMax)
console.log("yStackMax decimals:",decimals, factor)
if (yStackMax < maxTicks) {
format = defaultFormat
y_axis.ticks(minTicks)
}
else {
y_axis.ticks(maxTicks)
if (decimals < 3 ) format = defaultFormat
else if(decimals < 6 ) format = toKilo
else if(decimals < 9 ) format = toMega
else if(decimals < 12) format = toGiga
else format = toTerra
}
y_inverted.domain([yStackMax, 0]);
y_axis_root.transition()
.duration(interval)
.ease("linear")
.call(y_axis);
}
init()
setTimeout(update, 200)
setTimeout(update, 400)
setTimeout(update, 600)
You can try it together with this html snippet:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.v2.js"></script>
</head>
<body>
<div><svg id="svg" width="200" height="300"></svg></div>
<script src="axis.js"></script>
<button id="button1" onclick="update(10)">+</button>
<button id="button2" onclick="update(0.1)">-</button>
</body>
</html>
I know it is a bit off topic but I usually like to provide running examples/solutions. Regard the additional formatting stuff as a bonus to the actual problem.
If you ask for a certain number of ticks (via axis.ticks() ) then d3 will try to give you that many ticks - but will try to use pretty values. It has nothing to do with your data.
Your solutions are to use tickFormat, as you did, to round all the values to integer values, only ask for one tick as Juve answered, or explicitly set the tick values using axis.tickValues([...]) which would be pretty easy used in conjunction with d3.range
rangeRound will not help in this case because it relates to the output range of the scale, which in this case is the pixel offset to plot at: between 0 and height.
Going off of Superboggly's answer, this is what worked for me. First I got the max (largest) number from the y domain using y.domain().slice(-1)[0] and then I built an array of tick values from that using d3.range()...
var y_max = y.domain().slice(-1)[0]
var yAxis = d3.svg.axis()
.scale(y)
.tickValues(d3.range(y_max+1))
.tickFormat(d3.format(",.0f"))
Or just let the ticks as they are and "hide" decimal numbers
d3.svg.axis()
.scale(y_inverted)
.orient("left")
.ticks(5).tickFormat(function(d) {
if (d % 1 == 0) {
return d3.format('.f')(d)
} else {
return ""
}
});
Here is the code:
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickFormat(d3.format(".2s"));

Categories

Resources