Sorting a list of electronic components? - sorting

In a spreadsheet the bill of materials of electronic components lists the components by their values, for example, 1R0 (for 1 Ohm), 1K0 (for 1 kilo-Ohm), or 22p (for 22 pico-Farad), 1n0 (for 1 nano-Farad). How can numbers in this format be sorted in numerical order?
Before, unsorted:
Resistors
1K0
1R0
Capacitors
1n0
22p
After, sorted:
Resistors
1R0
1K0
Capacitors
22p
1n0

You can use the following custom function in order to sort your values:
var unitMap = {
'p': 1e-12,
'n': 1e-9,
'u': 1e-6,
'm': 1e-3,
'R': 1,
'K': 1e3,
'M': 1e6,
}
var unitRegex = /[pnumRKM]/;
function parseValue(val) {
var result = {};
var unitIdx = val.search(unitRegex);
var int = parseInt(val.substring(0, unitIdx));
var dec = parseFloat("0." + val.substring(unitIdx+1));
var multiplier = unitMap[val[unitIdx]];
return (int + dec) * multiplier;
}
/**
* Sorts E96 values.
*
* #param {range} input The range to sort.
* #param {number} input The column to sort by, starting at 1.
* #param {boolean} input Is ascending
* #return Sorted range.
* #customfunction
*/
function CUSTOMSORT(values, sort_column, is_ascending) {
values.sort(function (a, b) {
var a_value = parseValue(a[sort_column-1]);
var b_value = parseValue(b[sort_column-1]);
return is_ascending ? a_value - b_value : b_value - a_value;
});
return values;
}
The behaviour is pretty much the same as you would expect from the Sheets' built-in =SORT() function, albeit with less features. You can see two examples below:

you can custom sort it like this where you define the sorting order of each scalar:
=ARRAYFORMULA(SUBSTITUTE(TRANSPOSE(QUERY(TRANSPOSE(IFERROR(
ARRAY_CONSTRAIN(SORT({REGEXEXTRACT(A2:A, "(\d+)(.*)"),
VLOOKUP(REGEXEXTRACT(A2:A, "\d+(.*)"),
{"R0", 1;
"K0", 2;
"M0", 3}, 2, 0)}, 3, 1, 1, 1), 999^99, 2))),,999^99)), " ", ))

Related

Find the combination of a number set so that the total difference between two number sets is minimal

Find the combinations of lots assigned from newLots into oldLots, so that the sum of differences between newLotCombinations and oldLots is optimal.
If a difference is less than 0, it counts as 0.
All lots from newLot must be assigned into newLotCombinations.
/**
*
* e.g.
*
* newLots = [20.16, 9.95, 12.62, 7.44, 11.18, 9.02, 8.21, 8.22, 6.57, 6.63]
* oldLots = [12.03, 14.03, 16.04, 17.8, 18.04, 22.05]
*
* newLotCombinations | oldLot | Difference (newLotCombinations - oldLot)
* 20.16, 9.95 | 12.03 | 18.03 (20.16 + 9.95 - 12.03)
* 12.62, 7.44 | 14.03 | 6.03 (12.62 + 7.44 - 14.03)
* 11.18 | 16.04 | 0
* ...
* ...
* Sum of differences = 18.03 + 6.03 + 0 + ...
*/
I think this should involve memoizing the paths that I have gone through, like a map, and walking backward when a path is not correct (total sum larger than the assumption)
This is what I have so far:
const newLots = [20.16, 9.95, 12.62, 7.44, 11.18, 9.02, 8.21, 8.22, 6.57, 6.63]; // stack
const oldLots = [12.03, 14.03, 16.04, 17.8, 18.04, 22.05];
// newLotCombinations is an array of array [[], [], [], [], [], []] // i and j
const newLotCombinations = oldLots.map(() => []);
// Assuming the max. sum of differences is 5.
const MAX_SUM_OF_DIFFERENCES = 7;
const sum = 0;
// store information about a path?
const paths = {};
const loopLots = (i = 0, j = 0) => {
if (i === -1) {
console.log('it should not come to here');
console.log(
"No possible combination for max sum of differences:",
MAX_SUM_OF_DIFFERENCES
);
} else if (!newLots.length) {
console.log(
"Combination that works with max sum of difference ",
MAX_SUM_OF_DIFFERENCES,
newLotCombinations
);
}
newLotCombinations[i][j] = newLots.pop();
if (getSumOfDifferences() > MAX_SUM_OF_DIFFERENCES) {
// put it back to stack
newLots.push(newLotCombinations[i].pop());
if (i + 1 < newLotCombinations.length) {
loopLots(i + 1, newLotCombinations[i+ 1].length);
} else {
// It keeps popping until an array has more than 1 element.
// But this is incorrect, as it will loop with 3 combinations of numbers
// It should walk backward until last divergence
while (newLotCombinations[i] && (i === newLotCombinations.length - 1 || newLotCombinations[i].length < 2)) {
newLots.push(newLotCombinations[i].pop());
i--;
}
if (newLotCombinations[i]) {
newLots.push(newLotCombinations[i].pop());
newLotCombinations[i + 1][newLotCombinations[i + 1].length] = newLots.pop();
// loopLots(i + 1, newLotCombinations[i + 1].length);
loopLots(0, newLotCombinations[0].length);
} else {
console.log(
"No possible combination for max sum of differences:",
MAX_SUM_OF_DIFFERENCES
);
}
}
} else {
loopLots(0, newLotCombinations[0].length);
}
};
const getSumOfDifferences = () => {
let sumOfDifferences = 0;
newLotCombinations.forEach((lots, i) => {
const lotSum = lots.reduce((sum, lot) => {
sum += lot;
return sum;
}, 0);
const difference = lotSum - oldLots[i];
if (difference > 0) {
sumOfDifferences += difference;
}
});
return sumOfDifferences;
};
loopLots();
The logic of using newLotCombinations[i].length < 2 is incorrect, because it keeps pushing in the same alternating numbers. If I memoize the paths to check whether I should go further, how can I know when I am walking backward or forward if simply saving the paths that I have walked through?
I am thinking I also should not save a subset path. I should save a path that reaches the end (i.e. 6), because a subset of path contains unknown paths ahead.

Scoring two sequences of ordered numbers for their similarity to one-another

How would I go about scoring two sequences of numbers such that
5, 8, 28, 31 (differences of 3, 20 and 3)
6, 9, 26, 29 differences of 3, 17 and 3
are considered similar "enough" but a sequence of
8 11 31 34 (differences of 3, 20 and 3, errors of 3, 3, 3, 3)
Is too dissimilar to allow?
The second set of numbers has an absolute error of
1 1 2 2 and that is low "enough" to accept.
If that error was too high I'd like to be able to reject it.
To give a little background, these are indicators of time and when events arrived to a computer. The first sequence is the expected time of arrival and the second sequence is the actual times they arrived. Knowing that the sequence is at least in the correct order I need to be able to score the similarity to the expectation and accept or reject it by tweaking some sort of value.
If it were standard deviation for a set of numbers where order didn't matter I could just reject the second set based on its own standard deviation.
Since this is not the case I had the idea of measuring deviance and position error.
Position error shouldn't exceed 3, though this number should not be integer - it needs to be decimal as the numbers are more realistically floating point, or at least accurate to 6 decimal places.
It also needs to work equally well, or perhaps offer a variant in which a much longer series of numbers can be scored fairly.
In the longer series of numbers it it not likely the position error will exceed 3 so the position error would still be fairly low.
This is a partial solution I have found using a Person's correlation coefficient series for each time x fits into y. It uses the form of the equation that works off expected values. The comments describe it fairly well.
function getPearsonsCorrelation(x, y)
{
/**
* Pearsons can be calculated in an alternative fashion as
* p(x, y) = (E(xy) - E(x)*E(y))/sqrt[(E(x^2)-(E(x))^2)*(E(y^2)-(E(y))^2)]
* where p(x, y) is the Pearson's correlation result, E is a function referring to the expected value
* E(x) = var expectedValue = 0; for(var i = 0; i < x.length; i ++){ expectedValue += x[i]*p[i] }
* where p[i] is the probability of that variable occurring, here we substitute in 1 every time
* hence this simplifies to E(x) = sum of all x values
* sqrt is the square root of the result in square brackets
* ^2 means to the power of two, or rather just square that value
**/
var maxdelay = y.length - x.length; // we will calculate Pearson's correlation coefficient at every location x fits into y
var xl = x.length
var results = [];
for(var d = 0; d <= maxdelay; d++){
var xy = [];
var x2 = [];
var y2 = [];
var _y = y.slice(d, d + x.length); // take just the segment of y at delay
for(var i = 0; i < xl; i ++){
xy.push(x[i] * _y[i]); // x*y array
x2.push(x[i] * x[i]); // x squareds array
y2.push(_y[i] * _y[i]); // y squareds array
}
var sum_x = 0;
var sum_y = 0;
var sum_xy = 0;
var sum_x2 = 0;
var sum_y2 = 0;
for(var i = 0; i < xl; i ++){
sum_x += x[i]; // expected value of x
sum_y += _y[i]; // expected value of y
sum_xy += xy[i]; // expected value of xy/n
sum_x2 += x2[i]; // expected value of (x squared)/n
sum_y2 += y2[i]; // expected value of (y squared)/n
}
var numerator = xl * sum_xy - sum_x * sum_y; // expected value of xy - (expected value of x * expected value of y)
var denomLetSide = xl * sum_x2 - sum_x * sum_x; // expected value of (x squared) - (expected value of x) squared
var denomRightSide = xl * sum_y2 - sum_y * sum_y; // expected value of (y squared) - (expected value of y) squared
var denom = Math.sqrt(denomLetSide * denomRightSide);
var pearsonsCorrelation = numerator / denom;
results.push(pearsonsCorrelation);
}
return results;
}

Function to return value from "chopped" input range

I'm looking for a function or algorithm that, for a value in a specified range, will return a value in the same range but based on the chop/divide the value is in. Difficult to explain - some expected output based on this empty shell
function choppedRange(value, min, max, chops) {
// value - a value in range min to max
// chops - integer defining how many "subranges" or "chops" to return values from
...
}
// Map (linear conversion) input value in range oldMin -> oldMax
// to a value in range newMin -> newMax
function remap(oldValue, oldMin, oldMax, newMin, newMax) {
return (((oldValue - oldMin) * (newMax - newMin)) / (oldMax - oldMin)) + newMin;
}
What the illustration is trying to show is the following:
Determine in which chop the input value (blue dot) is. (let's say it's value is 0.35...)
Remap (function above) the value (blue) 0.35 in range 0,25 to 0.5
to be in the range of min and max arguments e.g. 0 to 1: remap(0.35, 0.25, 0.5, 0, 1)
The output (green dot) with the example values should be 0.3999...
Hope you can see what I mean.
As you can see I have the remapping done - but I'm having trouble deciding the "chop" values for the input to remap - like this:
remap(input_value, chop_min, chop_max, min, max)
I want to find chop_min and chop_max based on the chops argument in choppedRange
function choppedRange(value, min, max, chops) {
// Figure out chop_min and chop_max
...
return remap(value, chop_min, chop_max, min, max)
}
I ended up with this implementation
function choppedRange(value, min, max, chops) {
// value - a value in range min to max
// chops - integer defining how many "subranges" or "chops" to return values from
var chopMin = min, chopMax = max, chopValue = max / chops, i, c
for (i = 1; i <= chops; i++) {
c = chopValue * i
if (c < value)
chopMin = c
if (c >= value) {
chopMax = c
break;
}
}
return (((value - chopMin) * (max - min)) / (chopMax - chopMin)) + min
}
var r = choppedRange(0, 0, 1, 4)
console.log('Result',r)
r = choppedRange(0.35, 0, 1, 4)
console.log('Result',r)
r = choppedRange(0.5, 0, 1, 4)
console.log('Result',r)
r = choppedRange(0.6, 0, 1, 4)
console.log('Result',r)
r = choppedRange(1, 0, 1, 4)
console.log('Result',r)

Calculating the size of a box for multiple products for carrier module in prestashop

I am currently developing a carrier module for Kuroneko which is a door-to-door Japanese delivery company. This carrier, along with the weight, takes the size of the box into consideration. But it doe not use the volume, it would be too easy. It uses the sum of the three dimensions (height, weight and depth).
If you've got only one product to box, it's still easy but what happens when you need to box 2 products? Let's say you have two products of dimensions (x1,y1,z1) and (x2,y2,z2), how do you calculate the final box size X,Y,Z in a way you keep X+Y+Z minimal?
Here is my provisional solution, but tell me if there's a better way to calculate:
let m1 be the minimal dimension of the first product, min(x1,y1,z1) and m2 for the second. You have to account for rotations of the products in the box to fit them in the best way and thus define new dimensions nx,ny,nz for both products. Let's suppose that nx = m. If m = x then ny = y, nz = z, else if m=y then, ny= x, nz = z, else if m=z, ny = y, nz= x. The total box size thus become 2*nx,max(ny1,ny2),max(nz1,nz2).
But as far as I see, I don't think this method will work for more than 2 products. Any idea?
Answer Demo Here.
You can check the code here (I added canvas to visualize)
Logic:
Find total Volume (w*h*d)[+(w*h*d)..]
Collect all possible width height and depth values, sort each from lowest to highest
Find all possible sum permutations for width, then for height, then for width
3a. Example: sum permutations for width ranges 1,2,3 would be 1, 2, 3, 4, 5, 6
3b. we need this because in no way could the final value for width be 1.5 for example based on the example (3a.)
Find all possible combinations of Width, Height and Depth based on the permutations calculated on (3.)
Store all combinations where the total volume is equal or greater than the total Volume from (1.)
5a. This is because it is not possible that the final volume could be less than the actual Volume (1.)
5b. For Volumes greater than (1.) it means that's dead space.
Sort all combinations from (5.) Ascending, the first result will be the most accurate Volume
It is possible that the most accurate volume still could have different dimensions
7a. Example: Volume 16 can be 2x2x4 or 4x4x1 or 2x1x8 or 16x1x1
7b. Find the sum of W+H+D for each and the smallest sum would be the even more accurate dimensions.
7c. Example from (7a.) 2+2+4 = 8, 4+4+1 = 9, 2+1+8 = 11, 16+1+1 = 18 .... So our script would choose 2 x 2 x 4
I was looking for this algorithm but the link is down, however i could find it using the wayback machine.
Anyway, i will post it here because it might be useful for someone else
<?php
$test = '1,2,3|4,2,1|0.1,0.9,0.01';
$dimensions = explode("|", $test);
//1. Find total volume
$volume = 0;
//2. Find WHD ranges
$widthRange = array();
$heightRange = array();
$depthRange = array();
foreach($dimensions as $dimension) {
list($width, $height, $depth) = explode(',', $dimension);
$volume += $width * $height * $depth;
$widthRange[] = $width;
$heightRange[] = $height;
$depthRange[] = $depth;
}
//3. Order the WHD ranges
sort($widthRange);
sort($heightRange);
sort($depthRange);
echo 'Volume: '.$volume.'<br />';
echo 'Width Range: '.implode(', ', $widthRange).'<br />';
echo 'Height Range: '.implode(', ', $heightRange).'<br />';
echo 'Depth Range: '.implode(', ', $depthRange).'<br />';
//4. Figure out every combination with WHD
$widthCombination = array();
$heightCombination = array();
$depthCombination = array();
function combination($list) {
$combination = array();
$total = pow(2, count($list));
for ($i = 0; $i < $total; $i++) {
$set = array();
//For each combination check if each bit is set
for ($j = 0; $j < $total; $j++) {
//Is bit $j set in $i?
if (pow(2, $j) & $i) $set[] = $list[$j];
}
if(empty($set) || in_array(array_sum($set), $combination)) {
continue;
}
$combination[] = array_sum($set);
}
sort($combination);
return $combination;
}
$widthCombination = combination($widthRange);
$heightCombination = combination($heightRange);
$depthCombination = combination($depthRange);
echo 'Width Combination: '.implode(', ', $widthCombination).'<br />';
echo 'Height Combination: '.implode(', ', $heightCombination).'<br />';
echo 'Depth Combination: '.implode(', ', $depthCombination).'<br />';
$stacks = array();
foreach($widthCombination as $width) {
foreach($heightCombination as $height) {
foreach($depthCombination as $depth) {
$v = $width*$height*$depth;
if($v >= $volume) {
$stacks[$v][$width+$height+$depth] = array($width, $height, $depth);
}
}
}
}
ksort($stacks);
foreach($stacks as $i => $dims) {
ksort($stacks[$i]);
foreach($stacks[$i] as $j => $stack) {
rsort($stack);
break;
}
break;
}
echo '<pre>'.print_r($stacks, true).'</pre>';
All the credit belongs to Christian Blanquera
Implemented the algorithm in python, translated the code by Macr1408, credit to the original author of the algorithm Christian Blanquera.
from functools import reduce
from itertools import product
def combination(dim: list) -> list:
"""Calculate all possible sum permutations for a given list
of numbers.
Args:
dim (list): A list of numbers
Returns:
list: All possible sum permutations
"""
combination = []
total = pow(2, len(dim))
for i in range(total):
set_v = []
for j in range(total):
if (pow(2, j) & i):
set_v.append(dim[j])
if len(set_v) == 0 or sum(set_v) in combination:
continue
combination.append(sum(set_v))
return sorted(combination)
# dimensions => [(w1, h1, l1), (w2, h2, l2), ...]
def calculate_volumetric_total(dimensions: list[tuple[float, float, float]]) -> tuple:
"""Calculate the volumetric dimensions of the box needed to store products
with sizes stored as tuples of (width, height, length). Based on the following
algorithm:
1. Find total Volume (w*h*d)[+(w*h*d)..]
2. Collect all possible width height and depth values, sort each from lowest to highest
3. Find all possible sum permutations for width, then for height, then for width
3a. Example: sum permutations for width ranges 1,2,3 would be 1, 2, 3, 4, 5, 6
3b. we need this because in no way could the final value for width be 1.5 for example based on the example (3a.)
4. Find all possible combinations of Width, Height and Depth based on the permutations calculated on (3.)
5. Store all combinations where the total volume is equal or greater than the total Volume from (1.)
5a. This is because it is not possible that the final volume could be less than the actual Volume (1.)
5b. For Volumes greater than (1.) it means that's dead space.
6. Sort all combinations from (5.) Ascending, the first result will be the most accurate Volume
7. It is possible that the most accurate volume still could have different dimensions
7a. Example: Volume 16 can be 2x2x4 or 4x4x1 or 2x1x8 or 16x1x1
7b. Find the sum of W+H+D for each and the smallest sum would be the even more accurate dimensions.
7c. Example from (7a.) 2+2+4 = 8, 4+4+1 = 9, 2+1+8 = 11, 16+1+1 = 18 .... So our script would choose 2 x 2 x 4
Args:
dimensions (list[tuple[float, float, float]]): A list of all products/boxes
to store together in the form width, height, length.
Returns:
tuple: A tuple of width, height, length values representing the box needed to
store all the provided dimensions in.
"""
# 1
total_volume = sum([reduce(lambda x, y: x*y, t) for t in dimensions])
# 2, sorting happens when combining values
all_widths = [t[0] for t in dimensions]
all_heights = [t[1] for t in dimensions]
all_lengths = [t[2] for t in dimensions]
# 3
width_combination = combination(all_widths)
height_combination = combination(all_heights)
length_combination = combination(all_lengths)
# 4
vals = {}
for perm in product(width_combination, height_combination, length_combination):
# 5
volume = reduce(lambda x, y: x*y, perm)
if volume >= total_volume:
vals[sum(perm)] = perm
# 6
return vals[sorted(vals.keys())[0]]
Javascript implementation of the algorithm developed by Christian Blanquera.
Graphic demo
/**
* Calculate all possible sum permutations for a given list of numbers
* #param list A list of numbers
* #return {*[]} All possible sum permutations
*/
function combination(list) {
let combination = [];
const total = Math.pow(2, list.length);
for (let i = 0; i < total; i++) {
const set = [];
//For each combination check if each bit is set
for (let j = 0; j < total; j++) {
//Is bit j set in i?
if (Math.pow(2, j) & i) {
set.push(list[j])
}
}
const sum = set.reduce((partialSum, a) => partialSum + a, 0);
if (set.length < 1 || combination.includes(sum)) continue;
combination.push(sum)
}
return combination.sort();
}
/**
* Calculate the volumetric dimensions of the box needed to store products
* with sizes stored as array of [width, height, length].
* #param dimensions A list of all products/boxes to store together in the form width, height, length.
* #return {{[p: string]: unknown}} A object of width, height, length values representing the box needed to
* store all the provided dimensions in.
*/
function calculateVolumetricTotal(dimensions) {
let totalVolume = 0
let dims = {
widthRange: [],
heightRange: [],
depthRange: []
}
// 1. Find total volume
// 2. Collect all possible width height and depth values
dimensions.forEach(dimension => {
const [width, height, depth] = dimension.map(x => parseFloat(x))
dims.widthRange.push(width)
dims.heightRange.push(height)
dims.depthRange.push(depth)
totalVolume += width * height * depth
})
// sorting happens when combining values
for (const dim in dims) {
dims[dim].sort()
}
/**
* 3. Find all possible sum permutations for width, then for height, then for width
* 3a. Example: sum permutations for width ranges 1,2,3 would be 1, 2, 3, 4, 5, 6
* 3b. we need this because in no way could the final value for width be 1.5 for example based on the example (3a.)
*/
const combinations = {
width: combination(dims.widthRange),
height: combination(dims.heightRange),
depth: combination(dims.depthRange)
}
/**
* 4. Find all possible combinations of Width, Height and Depth based on the permutations calculated on (3.)
*/
let stacks = {};
combinations.width.forEach(width => {
combinations.height.forEach(height => {
combinations.depth.forEach(depth => {
const v = width * height * depth;
/**
* 5. Store all combinations where the total volume is equal or greater than the total Volume
* 5a. This is because it is not possible that the final volume could be less than the actual Volume
* 5b. For Volumes greater than (1.) it means that's dead space.
*/
if (v >= totalVolume) {
stacks[v] = {}
stacks[v][width + height + depth] = [width, height, depth]
}
})
})
})
/**
* 6. Sort all combinations from (5.) Ascending,
* the first result will be the most accurate Volume
*/
return Object.fromEntries(
Object.entries(stacks).sort(([v1], [v2]) => v1 < v2 ^ 1),
)
}
The above algorithm do not work for dimensions:
$test = '100,10,10|50,50,50';
First result is:
(
[0] => 50
[1] => 60
[2] => 50
)
but the first product do not fit. Combination array should only include sizes sums that are greater or equal the size of the biggest dimension (Width Combination should not include 50).

Choosing an attractive linear scale for a graph's Y Axis

I'm writing a bit of code to display a bar (or line) graph in our software. Everything's going fine. The thing that's got me stumped is labeling the Y axis.
The caller can tell me how finely they want the Y scale labeled, but I seem to be stuck on exactly what to label them in an "attractive" kind of way. I can't describe "attractive", and probably neither can you, but we know it when we see it, right?
So if the data points are:
15, 234, 140, 65, 90
And the user asks for 10 labels on the Y axis, a little bit of finagling with paper and pencil comes up with:
0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250
So there's 10 there (not including 0), the last one extends just beyond the highest value (234 < 250), and it's a "nice" increment of 25 each. If they asked for 8 labels, an increment of 30 would have looked nice:
0, 30, 60, 90, 120, 150, 180, 210, 240
Nine would have been tricky. Maybe just have used either 8 or 10 and call it close enough would be okay. And what to do when some of the points are negative?
I can see Excel tackles this problem nicely.
Does anyone know a general-purpose algorithm (even some brute force is okay) for solving this? I don't have to do it quickly, but it should look nice.
A long time ago I have written a graph module that covered this nicely. Digging in the grey mass gets the following:
Determine lower and upper bound of the data. (Beware of the special case where lower bound = upper bound!
Divide range into the required amount of ticks.
Round the tick range up into nice amounts.
Adjust the lower and upper bound accordingly.
Lets take your example:
15, 234, 140, 65, 90 with 10 ticks
lower bound = 15
upper bound = 234
range = 234-15 = 219
tick range = 21.9. This should be 25.0
new lower bound = 25 * round(15/25) = 0
new upper bound = 25 * round(1+235/25) = 250
So the range = 0,25,50,...,225,250
You can get the nice tick range with the following steps:
divide by 10^x such that the result lies between 0.1 and 1.0 (including 0.1 excluding 1).
translate accordingly:
0.1 -> 0.1
<= 0.2 -> 0.2
<= 0.25 -> 0.25
<= 0.3 -> 0.3
<= 0.4 -> 0.4
<= 0.5 -> 0.5
<= 0.6 -> 0.6
<= 0.7 -> 0.7
<= 0.75 -> 0.75
<= 0.8 -> 0.8
<= 0.9 -> 0.9
<= 1.0 -> 1.0
multiply by 10^x.
In this case, 21.9 is divided by 10^2 to get 0.219. This is <= 0.25 so we now have 0.25. Multiplied by 10^2 this gives 25.
Lets take a look at the same example with 8 ticks:
15, 234, 140, 65, 90 with 8 ticks
lower bound = 15
upper bound = 234
range = 234-15 = 219
tick range = 27.375
Divide by 10^2 for 0.27375, translates to 0.3, which gives (multiplied by 10^2) 30.
new lower bound = 30 * round(15/30) = 0
new upper bound = 30 * round(1+235/30) = 240
Which give the result you requested ;-).
------ Added by KD ------
Here's code that achieves this algorithm without using lookup tables, etc...:
double range = ...;
int tickCount = ...;
double unroundedTickSize = range/(tickCount-1);
double x = Math.ceil(Math.log10(unroundedTickSize)-1);
double pow10x = Math.pow(10, x);
double roundedTickRange = Math.ceil(unroundedTickSize / pow10x) * pow10x;
return roundedTickRange;
Generally speaking, the number of ticks includes the bottom tick, so the actual y-axis segments are one less than the number of ticks.
Here is a PHP example I am using. This function returns an array of pretty Y axis values that encompass the min and max Y values passed in. Of course, this routine could also be used for X axis values.
It allows you to "suggest" how many ticks you might want, but the routine will return
what looks good. I have added some sample data and shown the results for these.
#!/usr/bin/php -q
<?php
function makeYaxis($yMin, $yMax, $ticks = 10)
{
// This routine creates the Y axis values for a graph.
//
// Calculate Min amd Max graphical labels and graph
// increments. The number of ticks defaults to
// 10 which is the SUGGESTED value. Any tick value
// entered is used as a suggested value which is
// adjusted to be a 'pretty' value.
//
// Output will be an array of the Y axis values that
// encompass the Y values.
$result = array();
// If yMin and yMax are identical, then
// adjust the yMin and yMax values to actually
// make a graph. Also avoids division by zero errors.
if($yMin == $yMax)
{
$yMin = $yMin - 10; // some small value
$yMax = $yMax + 10; // some small value
}
// Determine Range
$range = $yMax - $yMin;
// Adjust ticks if needed
if($ticks < 2)
$ticks = 2;
else if($ticks > 2)
$ticks -= 2;
// Get raw step value
$tempStep = $range/$ticks;
// Calculate pretty step value
$mag = floor(log10($tempStep));
$magPow = pow(10,$mag);
$magMsd = (int)($tempStep/$magPow + 0.5);
$stepSize = $magMsd*$magPow;
// build Y label array.
// Lower and upper bounds calculations
$lb = $stepSize * floor($yMin/$stepSize);
$ub = $stepSize * ceil(($yMax/$stepSize));
// Build array
$val = $lb;
while(1)
{
$result[] = $val;
$val += $stepSize;
if($val > $ub)
break;
}
return $result;
}
// Create some sample data for demonstration purposes
$yMin = 60;
$yMax = 330;
$scale = makeYaxis($yMin, $yMax);
print_r($scale);
$scale = makeYaxis($yMin, $yMax,5);
print_r($scale);
$yMin = 60847326;
$yMax = 73425330;
$scale = makeYaxis($yMin, $yMax);
print_r($scale);
?>
Result output from sample data
# ./test1.php
Array
(
[0] => 60
[1] => 90
[2] => 120
[3] => 150
[4] => 180
[5] => 210
[6] => 240
[7] => 270
[8] => 300
[9] => 330
)
Array
(
[0] => 0
[1] => 90
[2] => 180
[3] => 270
[4] => 360
)
Array
(
[0] => 60000000
[1] => 62000000
[2] => 64000000
[3] => 66000000
[4] => 68000000
[5] => 70000000
[6] => 72000000
[7] => 74000000
)
Try this code. I've used it in a few charting scenarios and it works well. It's pretty fast too.
public static class AxisUtil
{
public static float CalculateStepSize(float range, float targetSteps)
{
// calculate an initial guess at step size
float tempStep = range/targetSteps;
// get the magnitude of the step size
float mag = (float)Math.Floor(Math.Log10(tempStep));
float magPow = (float)Math.Pow(10, mag);
// calculate most significant digit of the new step size
float magMsd = (int)(tempStep/magPow + 0.5);
// promote the MSD to either 1, 2, or 5
if (magMsd > 5.0)
magMsd = 10.0f;
else if (magMsd > 2.0)
magMsd = 5.0f;
else if (magMsd > 1.0)
magMsd = 2.0f;
return magMsd*magPow;
}
}
Sounds like the caller doesn't tell you the ranges it wants.
So you are free to changed the end points until you get it nicely divisible by your label count.
Let's define "nice". I would call nice if the labels are off by:
1. 2^n, for some integer n. eg. ..., .25, .5, 1, 2, 4, 8, 16, ...
2. 10^n, for some integer n. eg. ..., .01, .1, 1, 10, 100
3. n/5 == 0, for some positive integer n, eg, 5, 10, 15, 20, 25, ...
4. n/2 == 0, for some positive integer n, eg, 2, 4, 6, 8, 10, 12, 14, ...
Find the max and min of your data series. Let's call these points:
min_point and max_point.
Now all you need to do is find is 3 values:
- start_label, where start_label < min_point and start_label is an integer
- end_label, where end_label > max_point and end_label is an integer
- label_offset, where label_offset is "nice"
that fit the equation:
(end_label - start_label)/label_offset == label_count
There are probably many solutions, so just pick one. Most of the time I bet you can set
start_label to 0
so just try different integer
end_label
until the offset is "nice"
I'm still battling with this :)
The original Gamecat answer does seem to work most of the time, but try plugging in say, "3 ticks" as the number of ticks required (for the same data values 15, 234, 140, 65, 90)....it seems to give a tick range of 73, which after dividing by 10^2 yields 0.73, which maps to 0.75, which gives a 'nice' tick range of 75.
Then calculating upper bound:
75*round(1+234/75) = 300
and the lower bound:
75 * round(15/75) = 0
But clearly if you start at 0, and proceed in steps of 75 up to the upper bound of 300, you end up with 0,75,150,225,300
....which is no doubt useful, but it's 4 ticks (not including 0) not the 3 ticks required.
Just frustrating that it doesn't work 100% of the time....which could well be down to my mistake somewhere of course!
The answer by Toon Krijthe does work most of the time. But sometimes it will produce excess number of ticks. It won't work with negative numbers as well. The overal approach to the problem is ok but there is a better way to handle this. The algorithm you want to use will depend on what you really want to get. Below I'm presenting you my code which I used in my JS Ploting library. I've tested it and it always works (hopefully ;) ). Here are the major steps:
get global extremas xMin and xMax (inlucde all the plots you want to print in the algorithm )
calculate range between xMin and xMax
calculate the order of magnitude of your range
calculate tick size by dividing range by number of ticks minus one
this one is optional. If you want to have zero tick allways printed you use tick size to calculate number of positive and negative ticks. Total number of ticks will be their sum + 1 (the zero tick)
this one is not needed if you have zero tick allways printed. Calculate lower and upper bound but remember to center the plot
Lets start. First the basic calculations
var range = Math.abs(xMax - xMin); //both can be negative
var rangeOrder = Math.floor(Math.log10(range)) - 1;
var power10 = Math.pow(10, rangeOrder);
var maxRound = (xMax > 0) ? Math.ceil(xMax / power10) : Math.floor(xMax / power10);
var minRound = (xMin < 0) ? Math.floor(xMin / power10) : Math.ceil(xMin / power10);
I round minimum and maximum values to be 100% sure that my plot will cover all the data. It is also very important to floor log10 of range wheter or not it is negative and substract 1 later. Otherwise your algorithm won't work for numbers that are lesser than one.
var fullRange = Math.abs(maxRound - minRound);
var tickSize = Math.ceil(fullRange / (this.XTickCount - 1));
//You can set nice looking ticks if you want
//You can find exemplary method below
tickSize = this.NiceLookingTick(tickSize);
//Here you can write a method to determine if you need zero tick
//You can find exemplary method below
var isZeroNeeded = this.HasZeroTick(maxRound, minRound, tickSize);
I use "nice looking ticks" to avoid ticks like 7, 13, 17 etc. Method I use here is pretty simple. It is also nice to have zeroTick when needed. Plot looks much more professional this way. You will find all the methods at the end of this answer.
Now you have to calculate upper and lower bounds. This is very easy with zero tick but requires a little bit more effort in other case. Why? Because we want to center the plot within upper and lower bound nicely. Have a look at my code. Some of the variables are defined outside of this scope and some of them are properties of an object in which whole presented code is kept.
if (isZeroNeeded) {
var positiveTicksCount = 0;
var negativeTickCount = 0;
if (maxRound != 0) {
positiveTicksCount = Math.ceil(maxRound / tickSize);
XUpperBound = tickSize * positiveTicksCount * power10;
}
if (minRound != 0) {
negativeTickCount = Math.floor(minRound / tickSize);
XLowerBound = tickSize * negativeTickCount * power10;
}
XTickRange = tickSize * power10;
this.XTickCount = positiveTicksCount - negativeTickCount + 1;
}
else {
var delta = (tickSize * (this.XTickCount - 1) - fullRange) / 2.0;
if (delta % 1 == 0) {
XUpperBound = maxRound + delta;
XLowerBound = minRound - delta;
}
else {
XUpperBound = maxRound + Math.ceil(delta);
XLowerBound = minRound - Math.floor(delta);
}
XTickRange = tickSize * power10;
XUpperBound = XUpperBound * power10;
XLowerBound = XLowerBound * power10;
}
And here are methods I mentioned before which you can write by yourself but you can also use mine
this.NiceLookingTick = function (tickSize) {
var NiceArray = [1, 2, 2.5, 3, 4, 5, 10];
var tickOrder = Math.floor(Math.log10(tickSize));
var power10 = Math.pow(10, tickOrder);
tickSize = tickSize / power10;
var niceTick;
var minDistance = 10;
var index = 0;
for (var i = 0; i < NiceArray.length; i++) {
var dist = Math.abs(NiceArray[i] - tickSize);
if (dist < minDistance) {
minDistance = dist;
index = i;
}
}
return NiceArray[index] * power10;
}
this.HasZeroTick = function (maxRound, minRound, tickSize) {
if (maxRound * minRound < 0)
{
return true;
}
else if (Math.abs(maxRound) < tickSize || Math.round(minRound) < tickSize) {
return true;
}
else {
return false;
}
}
There is only one more thing that is not included here. This is the "nice looking bounds". These are lower bounds that are numbers similar to the numbers in "nice looking ticks". For example it is better to have the lower bound starting at 5 with tick size 5 than having a plot that starts at 6 with the same tick size. But this my fired I leave it to you.
Hope it helps.
Cheers!
Converted this answer as Swift 4
extension Int {
static func makeYaxis(yMin: Int, yMax: Int, ticks: Int = 10) -> [Int] {
var yMin = yMin
var yMax = yMax
var ticks = ticks
// This routine creates the Y axis values for a graph.
//
// Calculate Min amd Max graphical labels and graph
// increments. The number of ticks defaults to
// 10 which is the SUGGESTED value. Any tick value
// entered is used as a suggested value which is
// adjusted to be a 'pretty' value.
//
// Output will be an array of the Y axis values that
// encompass the Y values.
var result = [Int]()
// If yMin and yMax are identical, then
// adjust the yMin and yMax values to actually
// make a graph. Also avoids division by zero errors.
if yMin == yMax {
yMin -= ticks // some small value
yMax += ticks // some small value
}
// Determine Range
let range = yMax - yMin
// Adjust ticks if needed
if ticks < 2 { ticks = 2 }
else if ticks > 2 { ticks -= 2 }
// Get raw step value
let tempStep: CGFloat = CGFloat(range) / CGFloat(ticks)
// Calculate pretty step value
let mag = floor(log10(tempStep))
let magPow = pow(10,mag)
let magMsd = Int(tempStep / magPow + 0.5)
let stepSize = magMsd * Int(magPow)
// build Y label array.
// Lower and upper bounds calculations
let lb = stepSize * Int(yMin/stepSize)
let ub = stepSize * Int(ceil(CGFloat(yMax)/CGFloat(stepSize)))
// Build array
var val = lb
while true {
result.append(val)
val += stepSize
if val > ub { break }
}
return result
}
}
this works like a charm, if you want 10 steps + zero
//get proper scale for y
$maximoyi_temp= max($institucion); //get max value from data array
for ($i=10; $i< $maximoyi_temp; $i=($i*10)) {
if (($divisor = ($maximoyi_temp / $i)) < 2) break; //get which divisor will give a number between 1-2
}
$factor_d = $maximoyi_temp / $i;
$factor_d = ceil($factor_d); //round up number to 2
$maximoyi = $factor_d * $i; //get new max value for y
if ( ($maximoyi/ $maximoyi_temp) > 2) $maximoyi = $maximoyi /2; //check if max value is too big, then split by 2
The above algorithms do not take into consideration the case when the range between min and max value is too small. And what if these values are a lot higher than zero? Then, we have the possibility to start the y-axis with a value higher than zero. Also, in order to avoid our line to be entirely on the upper or the down side of the graph, we have to give it some "air to breathe".
To cover those cases I wrote (on PHP) the above code:
function calculateStartingPoint($min, $ticks, $times, $scale) {
$starting_point = $min - floor((($ticks - $times) * $scale)/2);
if ($starting_point < 0) {
$starting_point = 0;
} else {
$starting_point = floor($starting_point / $scale) * $scale;
$starting_point = ceil($starting_point / $scale) * $scale;
$starting_point = round($starting_point / $scale) * $scale;
}
return $starting_point;
}
function calculateYaxis($min, $max, $ticks = 7)
{
print "Min = " . $min . "\n";
print "Max = " . $max . "\n";
$range = $max - $min;
$step = floor($range/$ticks);
print "First step is " . $step . "\n";
$available_steps = array(5, 10, 20, 25, 30, 40, 50, 100, 150, 200, 300, 400, 500);
$distance = 1000;
$scale = 0;
foreach ($available_steps as $i) {
if (($i - $step < $distance) && ($i - $step > 0)) {
$distance = $i - $step;
$scale = $i;
}
}
print "Final scale step is " . $scale . "\n";
$times = floor($range/$scale);
print "range/scale = " . $times . "\n";
print "floor(times/2) = " . floor($times/2) . "\n";
$starting_point = calculateStartingPoint($min, $ticks, $times, $scale);
if ($starting_point + ($ticks * $scale) < $max) {
$ticks += 1;
}
print "starting_point = " . $starting_point . "\n";
// result calculation
$result = [];
for ($x = 0; $x <= $ticks; $x++) {
$result[] = $starting_point + ($x * $scale);
}
return $result;
}
For anyone who need this in ES5 Javascript, been wrestling a bit, but here it is:
var min=52;
var max=173;
var actualHeight=500; // 500 pixels high graph
var tickCount =Math.round(actualHeight/100);
// we want lines about every 100 pixels.
if(tickCount <3) tickCount =3;
var range=Math.abs(max-min);
var unroundedTickSize = range/(tickCount-1);
var x = Math.ceil(Math.log10(unroundedTickSize)-1);
var pow10x = Math.pow(10, x);
var roundedTickRange = Math.ceil(unroundedTickSize / pow10x) * pow10x;
var min_rounded=roundedTickRange * Math.floor(min/roundedTickRange);
var max_rounded= roundedTickRange * Math.ceil(max/roundedTickRange);
var nr=tickCount;
var str="";
for(var x=min_rounded;x<=max_rounded;x+=roundedTickRange)
{
str+=x+", ";
}
console.log("nice Y axis "+str);
Based on the excellent answer by Toon Krijtje.
This solution is based on a Java example I found.
const niceScale = ( minPoint, maxPoint, maxTicks) => {
const niceNum = ( localRange, round) => {
var exponent,fraction,niceFraction;
exponent = Math.floor(Math.log10(localRange));
fraction = localRange / Math.pow(10, exponent);
if (round) {
if (fraction < 1.5) niceFraction = 1;
else if (fraction < 3) niceFraction = 2;
else if (fraction < 7) niceFraction = 5;
else niceFraction = 10;
} else {
if (fraction <= 1) niceFraction = 1;
else if (fraction <= 2) niceFraction = 2;
else if (fraction <= 5) niceFraction = 5;
else niceFraction = 10;
}
return niceFraction * Math.pow(10, exponent);
}
const result = [];
const range = niceNum(maxPoint - minPoint, false);
const stepSize = niceNum(range / (maxTicks - 1), true);
const lBound = Math.floor(minPoint / stepSize) * stepSize;
const uBound = Math.ceil(maxPoint / stepSize) * stepSize;
for(let i=lBound;i<=uBound;i+=stepSize) result.push(i);
return result;
};
console.log(niceScale(15,234,6));
// > [0, 100, 200, 300]
Based on #Gamecat's algorithm, I produced the following helper class
public struct Interval
{
public readonly double Min, Max, TickRange;
public static Interval Find(double min, double max, int tickCount, double padding = 0.05)
{
double range = max - min;
max += range*padding;
min -= range*padding;
var attempts = new List<Interval>();
for (int i = tickCount; i > tickCount / 2; --i)
attempts.Add(new Interval(min, max, i));
return attempts.MinBy(a => a.Max - a.Min);
}
private Interval(double min, double max, int tickCount)
{
var candidates = (min <= 0 && max >= 0 && tickCount <= 8) ? new[] {2, 2.5, 3, 4, 5, 7.5, 10} : new[] {2, 2.5, 5, 10};
double unroundedTickSize = (max - min) / (tickCount - 1);
double x = Math.Ceiling(Math.Log10(unroundedTickSize) - 1);
double pow10X = Math.Pow(10, x);
TickRange = RoundUp(unroundedTickSize/pow10X, candidates) * pow10X;
Min = TickRange * Math.Floor(min / TickRange);
Max = TickRange * Math.Ceiling(max / TickRange);
}
// 1 < scaled <= 10
private static double RoundUp(double scaled, IEnumerable<double> candidates)
{
return candidates.First(candidate => scaled <= candidate);
}
}
A demo of accepted answer
function tickEvery(range, ticks) {
return Math.ceil((range / ticks) / Math.pow(10, Math.ceil(Math.log10(range / ticks) - 1))) * Math.pow(10, Math.ceil(Math.log10(range / ticks) - 1));
}
function update() {
const range = document.querySelector("#range").value;
const ticks = document.querySelector("#ticks").value;
const result = tickEvery(range, ticks);
document.querySelector("#result").textContent = `With range ${range} and ${ticks} ticks, tick every ${result} for a total of ${Math.ceil(range / result)} ticks at ${new Array(Math.ceil(range / result)).fill(0).map((v, n) => Math.round(n * result)).join(", ")}`;
}
update();
<input id="range" min="1" max="10000" oninput="update()" style="width:100%" type="range" value="5000" width="40" />
<br/>
<input id="ticks" min="1" max="20" oninput="update()" type="range" style="width:100%" value="10" />
<p id="result" style="font-family:sans-serif"></p>

Resources