Skip overlapping tick labels in Plotly Javascript - javascript

We are using scatter plots in Plotly.JS to display 2D graph data over a large X range, so we use logarithmic scaling. Zooming and panning works very well, except for one small issue: the X tick labels are confusing because Plotly uses single-digit labels for minor (non-powers of 10) ticks:
I can use tickFormat: '0.1s' to show real numbers (which is what users want) instead of single digits, but then there are cases where these labels can overlap:
I can also add dtick: 'D2' which only displays subticks at positions 2 and 5 and not all digits, but this is then fixed and doesn't adjust to scaling any more.
Ideally, I could specify subtick-label digits where to skip the label (but not the vertical line) completely, without having to resort to tickmode: array and having to specify all tick labels manually, and still benefit from automatic tick adjustment depending on scaling.
For example, if all subtick digits are displayed, I would say I'd like to have tick labels at positions 1, 2, 3, 5, 7, the result would look like this:
The other display modes (digits 2 & 5 only, or just the power of 10) would not change.
Is there a way to do this? If so, how? I'm not afraid of patching Plotly if required, but right now I don't know where to start looking.

Usually I solve this by rotating the labels by some 35-45 degrees. That way they are all there and still readable.
https://plotly.com/javascript/reference/#layout-xaxis-tickangle
tickangle
Parent: layout.xaxis
Type: angle
Default: "auto"
Sets the angle of the tick labels with respect to the horizontal. For example, a `tickangle` of -90 draws the tick labels vertically.

OK, so I looked deeply into the Plotly configuration options and there are options that are semi-automatically modified 'live' depending on zoom levels, but the conditions when those modifications happen are hardcoded.
So here's a patch (for Plotly v1.36, because that's what we currently use). I modified some of the hardcoded conditions when to change from 1,10,100,... to 1,2,5,10,20,50,100,... to 1,2,3,4,5,... labeling on logarithmic axes and removed the tick labels at 4, 6, 8 and 9 for the last case to avoid text overlapping.
For my application, this now works well and I could not find any more overlapping of tick labels. Also, I got rid of the single digits between powers of ten which confused some users.
--- a/plotly-latest.v1.36.js 2021-05-24 21:45:28.000000000 +0100
+++ b/plotly-latest.v1.36.js 2022-02-02 10:21:08.000000000 +0100
## -127302,13 +127302,13 ##
if(!nt) {
if(ax.type === 'category') {
minPx = ax.tickfont ? (ax.tickfont.size || 12) * 1.2 : 15;
nt = ax._length / minPx;
}
else {
- minPx = ax._id.charAt(0) === 'y' ? 40 : 80;
+ minPx = ax._id.charAt(0) === 'y' ? 40 : 100;
nt = Lib.constrain(ax._length / minPx, 4, 9) + 1;
}
// radial axes span half their domain,
// multiply nticks value by two to get correct number of auto ticks.
if(ax._name === 'radialaxis') nt *= 2;
## -127395,14 +127395,21 ##
// Start with it cleared and mark that we're in calcTicks (ie calculating a
// whole string of these so we should care what the previous date head was!)
ax._prevDateHead = '';
ax._inCalcTicks = true;
var ticksOut = new Array(vals.length);
- for(var i = 0; i < vals.length; i++) ticksOut[i] = axes.tickText(ax, vals[i]);
+ // if scaling == log, skip some intermediate tick labels to avoid overlapping text
+ var skipTexts = /^[4689]/;
+ var text;
+ for(var i = 0; i < vals.length; i++) {
+ text = axes.tickText(ax, vals[i]);
+ if(ax.type == "log" && ax.dtick == "D1" && text.text.match(skipTexts)) text.text = "";
+ ticksOut[i] = text;
+ }
ax._inCalcTicks = false;
return ticksOut;
};
function arrayTicks(ax) {
## -127535,18 +127542,20 ##
// ticks on a linear scale, labeled fully
roughDTick = Math.abs(Math.pow(10, rng[1]) -
Math.pow(10, rng[0])) / nt;
base = getBase(10);
ax.dtick = 'L' + roundDTick(roughDTick, base, roundBase10);
+ ax.tickformat = '';
}
else {
// include intermediates between powers of 10,
// labeled with small digits
// ax.dtick = "D2" (show 2 and 5) or "D1" (show all digits)
- ax.dtick = (roughDTick > 0.3) ? 'D2' : 'D1';
+ ax.dtick = (roughDTick > 0.4) ? 'D2' : 'D1';
+ ax.tickformat = '0.1s';
+ ax.hoverformat = '0.2s'; // Workaround to fix hoverinfo label formatting
}
}
else if(ax.type === 'category') {
ax.tick0 = 0;
ax.dtick = Math.ceil(Math.max(roughDTick, 1));
}
The patch is hereby released as Public Domain to make it as easy as possible to use this for direct integration into Plotly (maybe as a "D3" formatting option?).

Related

D3 js what kind of scale to use for unbalanced data

I have to render few circles on which "r" attr depends on the input data. The problem with input data is one of the value is so huge compared to rest of the data points that when i use scaleLinear | Pow | Log with range 2 - 35 ; all my circles have radius between 2 - 3 and only that few data points are radius 35. so what kind of scale can i use for such data for better visual ?
You could maybe use d3.scaleQuantize.
Here is the example provided by the documentation:
var width = d3.scaleQuantize()
.domain([10, 100])
.range([1, 2, 4]);
width(20); // 1
width(50); // 2
width(80); // 4
Or you could write your own classification function. Something like this (with 3 classes here):
if input data < x => r = 2
else if x <= input data < y => r = 8
else (input data >=y) => r = 14

Calculate Ratios in JS like Humble Bundle

Do you know the sliders that you have on humblebundle.com when selecting where you want the money to go? Well when you adjust any one ratio it will automatically adjust the rest.
So say you're paying $20 no matter what but you want to adjust your tip to HB from $2 to $5, the ratios that were on the other stuff should automatically lowered to match but I have no idea what I'm doing.
This is as close as I get mathematically:
var settip = 50;
var tip = 5;
var devs = 75;
var donation = 20;
tip = settip;
var newAvail = 100 - tip;
var rCalc = 100 - (devs + donation);
devs = ((devs + rCalc) * newAvail) * .01;
donation = ((donation + rCalc) * newAvail) * .01;
console.log("New Ratio Calculation: " + rCalc);
console.log("New available space: " + newAvail);
console.log(tip);
console.log(devs);
console.log(donation);
The console logs are just so I can try and put it together in my head where things are going wrong. The numbers are also whole numbers first: 50 instead of .5 because Javascript is not accurate and I don't want to do the fix code every time, I'd rather figure out how to make the code work first and then think about optimizing.
So if anyone could guide me on a method or where I am going wrong here, then that'd be great. Thanks.
Tip is tip to the bundle maker.
Devs is tip to the devs.
Donation is tip to the donation box.
Each number is the ratio. Settip is the new ratio, I should be able to change any one value and have it automatically change all others but I can't even figure out how to do the first part so I couldn't begin to try for the second part of making it actually functional.
I think this problem is not as easy as it might seem if you want to cover different edge cases. Here I assume that you distribute money so you need following properties:
Each amount must be whole integer in cents
Sum of all amounts must be equal to the total sum
The simplest way to deal with it in JS is to make all calculations using whole numbers (e.g. sum in cents instead of dollars) and format them in more human-readable way on UI. Still even with this simplification it requires some non-trivial code:
function updateRates(rates, newValue, index) {
var i, len = rates.length;
var sum = 0;
for (i = 0; i < len; i++)
sum += rates[i];
var oldValue = rates[index];
var newRest = sum - newValue;
var curRest = sum - rates[index];
rates[index] = newValue;
var remainders = new Array(len);
var fraction, value, subsum = 0;
for (i = 0; i < len; i++) {
if (i === index) continue;
// special case, all other sliders were at 0 - split value equally
if (curRest === 0) {
fraction = 1.0 / (len - 1)
}
else {
fraction = rates[i] / curRest
}
value = newRest * fraction;
rates[i] = Math.floor(value); // always round down and then distribute rest according to the Largest remainder method
subsum += rates[i];
remainders[i] = {
index: i,
value: value - rates[i]
};
}
// sort remainders and distribute rest (fractions) accordingly
remainders.sort(function (a, b) {
var av = a.value;
var bv = b.value;
if (av === bv)
return 0;
if (av < bv)
return 1;
else
return -1;
});
for (i = 0; subsum < newRest; i++) {
rates[remainders[i].index] += 1;
subsum += 1;
}
return rates;
}
Some non-trivial tests:
1. updateRates([85,10,5], 82, 0) => [82, 12, 6]
2. updateRates([85,10,5], 83, 0) => [83, 11, 6]
3. updateRates([85,10,5], 84, 0) => [84, 11, 5]
4. updateRates([100,0,0], 95, 0) => [95, 2, 3]
5. updateRates([4,3,3,1], 0, 0) => [0, 5, 5, 1]
Pay attention to the example #5. If one used some naive rounding, sum will not be preserved. Effectively you need to distribute +4 in proportion 3:3:1. It means you should add +12/7, +12/7 and +4/7. Since 12/7 = 1 5/7, according to standard mathematical rules all three should be rounded up resulting in +2, +2, +1 but we only got +4 cents to distribute. To fix this issue the largest remainder method is used to distribute fractional cents among categories. Simply speaking the idea is that we first distribute only whole number of cents (i.e. always round down), calculate how many cents are actually left and then distribute them one by one. The biggest possible drawback of this method is that some rates that started with equal values might have different values after update. On the other hand this can't be avoided as example #4 shows: you can't split 5 cents equally between two categories.
To restate what I think you want: the three variables tip, devs and donation should always sum to 100. When one variable is updated, the other two should be updated to compensate. The automatic updates should keep the same ratios to each other (for example, if donation is double devs, and tips is updated, then the updated donation value should still be double the devs value).
If I've got that right, then this should work for you:
var tips = 5;
var devs = 20;
var donation = 75;
var setTips = function(newValue) {
tips = newValue;
var sum = devs + donation;
var devShare = devs / sum; // the share devs gets between devs and donation
var donationShare = 1 - devShare; // or could calculate as donation / sum
devs = (100 - tips) * devShare; // the remaining times it's share ratio
donation = (100 - tips) * donationShare; // the remaining times it's share ratio
};
// test it out
setTips(50);
console.log(tips, devs, donation);

Editing Highcharts.js donut data programmatically va JS

I am trying to do the same for donut (editing the donut data programmatically), but the code just wouldn't work for me, although the syntax seems to be straghtforward here.
My goal is to find the data point in the donut which corresponds to the given x-axis value and set the value to 10. Any ideas on this ?
Here's the JSFIddle
btnEdit.click(function() {
// chart.series[0].data[0].update(x += 10); - this code doesn't work
var x = prompt("Please enter your name");
// find the data point that corresponds to x
// Set it to 10
});
Actually, you are looking for a point by its name, not its x value, because x value is a number, name is a string (for categorized data there is natural mapping between those two).
btnEdit.click(function() {
// chart.series[0].data[0].update(x += 10); - this code doesn't work
var i = 0,
points = chart.series[0].data,
len = points.length,
x = prompt("Please enter your name"),
point;
for (; i < len; i++) {
point = points[i];
if (point.name === x) {
point.update({
y: 10
});
break;
}
}
// find the data point that corresponds to x
// Set it to 10
});
Example: http://jsfiddle.net/caLv5d6x/3/
The above solution assumes that you have points which their name are unique - but it doesn't have to be like that.
A small adjustment is enough:
btnEdit.click(function() {
// chart.series[0].data[0].update(x += 10); - this code doesn't work
var x = prompt("Please enter your name");
chart.series[0].data.forEach(function (point) {
if (point.name === x) {
point.update({
y: 10
}, false, false);
}
});
chart.redraw();
// find the data point that corresponds to x
// Set it to 10
});
Example: http://jsfiddle.net/caLv5d6x/4/

The Optimal Urinal Strategy

Here is an interactive page describing the problem and an academic paper going over the mathematics.
The problem can be roughly described as follows.
Given an arbitrary-length array of boolean values representing n adjacent urinals, with values of true indicating occupied and values of false indicating vacant, how would you construct an algorithm to populate this array, given any configuration, while:
Maximizing the 'privacy' of each occupant by keeping one as far as possible from other urinators on either side.
Maintaining this privacy for as long as possible by ensuring the configuration becomes saturated at the last possible time.
Faced with multiple suboptimal choices, prioritizing urinals without an adjacent urinal on either side over a merely unoccupied adjacent urinal.
I marked this javascript for simplicity, but any code or pseudo-code would be fine.
var urinals = Array
.apply(null, new Array(n))
.map(Boolean.prototype.valueOf,false);
edit - found a related problem here:
Optimal Seating Arrangement Algorithm
As close as I have to a solution:
var urinalFinder = function(urinals){
var gaps = new Array(), last = null;
for(var i = 0; i < urinals.length; i++){
last = gaps.length ? gaps[gaps.length - 1] : 0;
if(last < 0 && !urinals[i] || last > 0 && !!urinals[i] || last == 0)
gaps.push(0); // push if new sequence of vacant or occupied
// negatives are occupied count & positives vacant count
gaps[gaps.length - 1] += !!urinals[i] ? -1 : 1;
}
// find the first index of the largest gap
var maxGapSize = Math.max.apply(Math, gaps),
maxGapGapsIdx = gaps.indexOf(maxGapSize),
isFirst = maxGapGapsIdx === 0,
isLast = maxGapGapsIdx === gaps.length - 1,
maxGapIdx = 0;
if(maxGapSize < 1) return false; // no gaps available
var gapPoint = maxGapSize > 3
? Math.ceil(maxGapSize / 3) // per xkcd suggestion
: isFirst && maxGapSize === 2
? 1
: isLast && maxGapSize === 2 ? 2 : Math.ceil(maxGapSize / 2);
// find where our chosen gap begins in input array
for(var i = 0; i < maxGapGapsIdx; i++)
maxGapIdx += Math.abs(gaps[i]);
var result = maxGapIdx + gapPoint - 1; // arrays are zero-indexed
return result;
};
For example, applied to filling an array of 9 vacant spaces will fill them like this:
var foo = [0,0,0,0,0,0,0,0,0]; // nine values
for(var i = 0; i < foo.length; i++)
foo[urinalFinder(foo)] = i+1;
[4, 6, 1, 7, 2, 8, 3, 9, 5]
Does not always produce optimal results (sometimes a different placement could allow saturation a few moves later) and does not favor end urinals, but does a pretty good job fanning values around and keeping a minimum buffer for just about as long as possible.

Use jquery to dynamically number table columns diagonally

Hi there fellow coders,
I'm looking to find a way to fill a pre-built dynamic blank table with numbering (and colouring if possible) like so:
As you can see the numbering is ascending order diagonally. I know there's probably some way to calculate the number based on the tables td index but can't quite figure out how to do that for every column diagonally. Any help would be appreciated.
Update: Ok back from my Holidays. Thanks to all you clever people for your replies. As I'm sure you've all had to experience the pain in the neck clients can be, I've been told the spec has changed(again). This being the case I've had to put the grid/matrix into a database and output using a pivot table. Every square has to be customizable color-wise.
Nothing is going to waste though I have learnt quite a few nifty new javascript/jquery tricks from your responses I didn't know about before, so thanks, and I'll be sure to pay it forward :)
Here's what I came up with in the end.
Given you said "colouring if possible" I'll provide an example solution that doesn't do colours quite the way you want (it does it in a way that I found easier to code and more attractive to look at) but which does handle all the numbering correctly for varying table sizes.
The function below assumes the table already exists; in this demo I've included code that generates a table to whatever size you specify and then calls the function below to do the numbering and colours.
function numberDiagonally(tableId) {
var rows = document.getElementById(tableId).rows,
numRows = rows.length,
numCols = rows[0].cells.length,
sq = numRows + numCols - 1,
d, x, y,
i = 1,
dc,
c = -1,
colors = ["green","yellow","orange","red"];
diagonalLoop:
for (d = 0; d < sq; d++) {
dc = "diagonal" + d;
for (y = d, x = 0; y >= 0; y--, x++) {
if (x === numCols)
continue diagonalLoop;
if (y < numRows)
$(rows[y].cells[x]).html(i++).addClass(dc);
}
}
for (d = 0; d < sq; d++)
$(".diagonal" + d).css("background-color", colors[c=(c+1)%colors.length]);
}
Demo: http://jsfiddle.net/7NZt3/2
The general idea I came up with was to imagine a square twice as big as whichever of the x and y dimensions is bigger and then use a loop to create diagonals from the left edge of that bounding square going up and to the right - i.e., in the order you want the numbers. EDIT: Why twice as big as longer side? Because that's the first thing that came into my head when I started coding it and it worked (note that the variable i that holds the numbers that get displayed is not incremented for the imaginary cells). Now that I've had time to think, I realise that my sq variable can be set precisely to one less than the number of rows plus the columns - a number that ends up rather smaller for non-square tables. Code above and fiddle updated accordingly.
Note that the background colours could be set directly in the first loop, but instead I opted to assign classes and set the loops for each class later. Seemed like a good idea at the time because it meant individual diagonals could be easily selected in jQuery with a single class selector.
Explaining exactly how the rest works is left as an exercise for the reader...
UPDATE - this version does the colouring more like you asked for: http://jsfiddle.net/7NZt3/1/ (in my opinion not as pretty, but each to his own).
This fiddle populates an existing table with numbers and colors. It is not limited to being a 5x5 table. I didn't understand the logic of 15 being orange rather than yellow, so I simply grouped the diagonal cells into color regions.
// we're assuming the table exists
var $table = $('table'),
// cache the rows for quicker access
$rows = $table.find('tr'),
// determine number of rows
_rows = $rows.length,
// determine number of cells per row
_cols = $rows.first().children().length,
// determine total number of cells
max = _rows * _cols,
// current diagonal offset (for coloring)
d = 1,
// current row
r = 0,
// current cell
c = 0;
for (var i=1; i <= max; i++) {
// identify and fill the cell we're targeting
$rows.eq(r).children().eq(c)
.addClass('d' + d)
.text(i);
if (i < max/2) {
// in the first half we make a "line-break" by
// moving one row down and resetting to first cell
if (!r) {
r = c + 1;
c = 0;
d++;
continue;
}
} else {
// in the second half our "line-break" changes to
// moving to the last row and one cell to the right
if (c + 1 == _cols) {
c = 1 + r;
r = _rows -1;
d++;
continue;
}
}
r--;
c++;
}
Here's a jsFiddle that does what you asked for - http://jsfiddle.net/jaspermogg/MzNr8/8/
I took the liberty of making it a little bit user-customisable; it's interesting to see how long it takes the browser to render a 1000x1000 table using this method :-D
Assuming that each cell has an id of [column]x[row], here are teh codez for how to fill in the numbers of a square table of side length sidelength.
//populate the cells with numbers according to the spec
function nums(){
var xpos = 0
var ypos = 0
var cellval = 1
for(i=0;i<2*sidelength;i++){
if(i >= sidelength){
ypos = sidelength - 1
xpos = 1 + i - sidelength
$('td#' + xpos + 'x' + ypos).text(cellval)
cellval = cellval + 1
while(xpos + 1 < sidelength){
ypos = ypos-1
xpos = xpos+1
$('td#' + xpos + 'x' + ypos).text(cellval)
cellval = cellval + 1
}
} else {
ypos = i
xpos = 0
$('td#' + xpos + 'x' + ypos).text(cellval)
cellval = cellval + 1
while(!(ypos-1 < 0)){
ypos = ypos-1
xpos = xpos+1
$('td#' + xpos + 'x' + ypos).text(cellval)
cellval = cellval + 1
}
}
}
}
And here they are for how to colour the bugger.
// color the cells according to the spec
function cols(){
if(+$('td#0x0').text() === 99){
return false
} else {
$('td').each(function(index, element){
if(+$(this).text() > 22)
{
$(this).attr("bgcolor", "red")
}
if(+$(this).text() <= 22)
{
$(this).attr("bgcolor", "orange")
}
if(+$(this).text() <= 14)
{
$(this).attr("bgcolor", "yellow")
}
if(+$(this).text() <= 6)
{
$(this).attr("bgcolor", "green")
}
})
}
}
Enjoy, eh? :-)

Categories