displaying axis from min to max value - calculating scale and labels
Asked Answered
N

4

9

Writing a routine to display data on a horizontal axis (using PHP gd2, but that's not the point here).

The axis starts at $min to $max and displays a diamond at $result, such an image will be around 300px wide and 30px high, like this:

example
(source: testwolke.de)

In the example above, $min=0, $max=3, $result=0.6. Now, I need to calculate a scale and labels that make sense, in the above example e.g. dotted lines at 0 .25 .50 .75 1 1.25 ... up to 3, with number-labels at 0 1 2 3.

If $min=-200 and $max=600, dotted lines should be at -200 -150 -100 -50 0 50 100 ... up to 600, with number-labels at -200 -100 0 100 ... up to 600.

With $min=.02and $max=5.80, dotted lines at .02 .5 1 1.5 2 2.5 ... 5.5 5.8 and numbers at .02 1 2 3 4 5 5.8.

I tried explicitly telling the function where to put dotted lines and numbers by arrays, but hey, it's the computer who's supposed to work, not me, right?!

So, how to calculate???

Nub answered 17/2, 2013 at 17:22 Comment(6)
noone? at least some hint?Nub
Is there a certain number of labels you want displayed per picture?Allies
@Kyle: no fixed number of labels, it has to fit the values. Taking into account the width of 300px, there's a kind of natural limitation.Nub
Thanks for the response. I did post a solution that will give you a specified number of labels. I'll leave it there. Perhaps you will find at least some of it useful.Allies
linked image does not workSperoni
@Speroni repaired it, thank you!Nub
G
7

An algorithm (example values $min=-186 and $max=+153 as limits):

  1. Take these two limits $min, $max and mark them if you wish

  2. Calculate the difference between $max and $min: $diff = $max - $min
    153 - (-186) = 339

  3. Calculate 10th logarithm of the difference $base10 = log($diff,10) = 2,5302

  4. Round down: $power = round($base10) = 2.
    This is your tenth power as base unit

  5. To calculate $step calculate this:
    $base_unit = 10^$power = 100;
    $step = $base_unit / 2; (if you want 2 ticks per one $base_unit).

  6. Calculate if $min is divisible by $step, if not take the nearest (round up) one
    (in the case of $step = 50 it is $loop_start = -150)

  7. for ($i=$loop_start; $i<=$max; $i++=$step){ // $i's are your ticks

  8. end

I tested it in Excel and it gives quite nice results, you may want to increase its functionality,

for example (in point 5) by calculating $step first from $diff,
say $step = $diff / 4 and round $step in such way that $base_unit is divisible by $step;

this will avoid such situations that you have between (101;201) four ticks with $step=25 and you have 39 steps $step=25 between 0 and 999.

Graff answered 26/2, 2013 at 13:34 Comment(3)
this is awesome, can't wait to try it!Nub
michi, this was invented by me, not tested in too many cases, I wanted to give you a clue. The most crucial points are point 4 and 5. I always rounded it down but maybe for example when you have 2.999 you should round it up? Remember, that this log(339,10) is 2,5302 which will round up to 3 (that equals 1000). It is a logarithmic scale, just think how to round it, maybe if greater than 2.6989 (log 500) round up, if less round down?Graff
errr... I'll get back to you as soon as I tried. I have to confess that my maths-skills are less that average, and your explanatuons about log leve my brain blank... so I'll try and report :-)Nub
P
6

ACM Algorithm 463 provides three simple functions to produce good axis scales with outputs xminp, xmaxp and dist for the minimum and maximum values on the scale and the distance between tick marks on the scale, given a request for n intervals that include the data points xmin and xmax:

  1. Scale1() gives a linear scale with approximately n intervals and dist being an integer power of 10 times 1, 2 or 5.
  2. Scale2() gives a linear scale with exactly n intervals (the gap between xminp and xmaxp tends to be larger than the gap produced by Scale1()).
  3. Scale3() gives a logarithmic scale.

The original 1973 paper is online here, which provides more explanation than the code linked to above.

The code is in Fortran but it is just a set of arithmetical calculations so it is very straightforward to interpret and convert into other languages. I haven't written any PHP myself, but it looks a lot like C so you might want to start by running the code through f2c which should give you something close to runnable in PHP.

There are more complicated functions that give prettier scales (e.g. the ones in gnuplot), but Scale1() would likely do the job for you with minimal code.

(This answer builds on my answer to a previous question Graph axis calibration in C++)

(EDIT -- I've found an implementation of Scale1() that I did in Perl):

use strict;

sub scale1 ($$$) {
  # from TOMS 463
  # returns a suitable scale ($xMinp, $xMaxp, $dist), when called with 
  # the minimum and maximum x values, and an approximate number of intervals
  # to divide into. $dist is the size of each interval that results.
  # @vInt is an array of acceptable values for $dist.
  # @sqr is an array of geometric means of adjacent values of @vInt, which
  # is used as break points to determine which @vInt value to use.
  #
  my ($xMin, $xMax, $n) = @_;
  @vInt = {1, 2, 5, 10};
  @sqr = {1.414214, 3.162278, 7.071068 }
  if ($xMin > $xMax) {
    my ($tmp) = $xMin;
    $xMin = $xMax;
    $xMax = $tmp;
    }
  my ($del) = 0.0002;   # accounts for computer round-off
  my ($fn) = $n;
  # find approximate interval size $a
  my ($a) = ($xMax - $xMin) / $fn;
  my ($al) = log10($a);
  my ($nal) = int($al);
  if ($a < 1) {
    $nal = $nal - 1;
    }
  # $a is scaled into a variable named $b, between 1 and 10
  my ($b) = $a / 10^$nal;
  # the closest permissable value for $b is found)
  my ($i);
  for ($i = 0; $i < $_sqr; $i++) {
    if ($b < $sqr[$i]) last;
  }
  # the interval size is computed
  $dist = $vInt[$i] * 10^$nal;
  $fm1 = $xMin / $dist;
  $m1 = int($fm1);
  if ($fm1 < 0) $m1--;
  if (abs(($m1 + 1.0) - $fm1) < $del) $m1++;
  # the new minimum and maximum limits are found
  $xMinp = $dist * $m1;
  $fm2 = $xMax / $dist;
  $m2 = $fm2 + 1;
  if ($fm2 < -1) $m2--;
  if (abs ($fm2 + 1 - $m2) < $del) $m2--;
  $xMaxp = $dist * $m2;
  # adjust limits to account for round-off if necessary
  if ($xMinp > $xMin) $xMinp = $xMin;
  if ($xMaxp < $xMax) $xMaxp = $xMax;
  return ($xMinp, $xMaxp, $dist);
  }

sub scale1_Test {
  $par = (-3.1, 11.1, 5,
      5.2, 10.1, 5,
      -12000, -100, 9);
  print "xMin\txMax\tn\txMinp\txMaxp,dist\n";
  for ($i = 0; $i < $_par/3; $i++) {
    ($xMinp, $xMaxp, $dist) = scale1($par[3*$i+0], 
    $par[3*$i+1], $par[3*$i+2]);
    print "$par[3*$i+0]\t$par[3*$i+1]\t$par[3*$i+2]\t$xMinp\t$xMaxp,$dist\n";
    }
  }
Phonic answered 25/2, 2013 at 19:35 Comment(1)
Love the pointer to algorithms from the 70's. I wouldn't call this question a duplicate, but this is not new.Undaunted
A
3

I know that this isn't exactly what you are looking for, but hopefully it will get you started in the right direction.

$min = -200;
$max = 600;
$difference = $max - $min;
$labels = 10;
$picture_width = 300;
/* Get units per label */
$difference_between = $difference / ($labels - 1);
$width_between = $picture_width / $labels;
/* Make the label array */
$label_arr = array();
$label_arr[] = array('label' => $min, 'x_pos' => 0);
/* Loop through the number of labels */
for($i = 1, $l = $labels; $i < $l; $i++) {
    $label = $min + ($difference_between * $i);
    $label_arr[] = array('label' => $label, 'x_pos' => $width_between * $i);
}
Allies answered 19/2, 2013 at 17:55 Comment(1)
hummm... awarded the bounty to this answer (whioch is helpful, too)... but wanted to award Voitcus algorithm... newbie-mistake ... what can I do?Nub
S
1

A quick example would be something in the lines of $increment = ($max-$min)/$scale where you can tweak scale to be the variable by which the increment scales. Since you devide by it, it should change proportionately as your max and min values change. After that you will have a function like:

$end = false;
while($end==false){
    $breakpoint = $last_value + $increment; // that's your current breakpoint
    if($breakpoint > $max){
        $end = true;
    } 
}

At least thats the concept... Let me know if you have troubles with it.

Shammy answered 26/2, 2013 at 8:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.