home / junkbox / logscale 対数目盛

 こんなの。

 JavaScriptで大きさを変えられるようにしてみたもの。

1から10の間で、1ごとに線を引くと1と2の間が空きすぎて、1から2の間に補助線入れるとそれをどこまで入れればいいか悩んで・・・これをいい具合に自動で計算する方法を考えてみました。

home / junkbox / logscale アルゴリズム

home / junkbox / logscale 対数っぽい目盛 16 Feb 2020

home / junkbox / logscale 実装例

JavaScriptによる実装

ソース → logscale.js

 このページの冒頭にある、マウスで大きさ変えられるヤツが実際に動作しているサンプルです。

PostScriptによる実装

ソース 開く

%!
% i <logscale-proc> -
% use parent's dictionary as it is.
/logscale-proc {
    dup v3 mul 1000 div % i val
    dup v1 ge 1 index v2 le and {
        exch % val i
        1 index func y1 sub wpd mul exch % val pos i
        p % -
    } {
        pop pop
    } ifelse
} bind def

% written base step <logscale-sub> wirtten decade
% use parent's dictionary as it is.
% call:
%   written: must be write at least one line.
%   base: base value (10/100/1000)
%   step: line step (1/2/5/10/20/50/100)
% return:
%   written: true if at least one line is written
%   decade: true if the last line of the decade is wirtten
/logscale-sub {
    /step exch def
    /base exch def
    /j i base idiv base mul def
    /i i step idiv step mul def
    false exch % initial written return value
    { % use written on call here
        i step add step j base add {
            logscale-proc
            pop true    % written
        } for
        /j j base add def
        /i j def
    } if
    {
        i 1000 ge { true exit } if
        v3 j base add dup step sub calcdelta msp lt { false exit } if
        i step add step j base add {
            logscale-proc
            pop true    % written
        } for
        /j j base add def
        /i j def
    } loop
} bind def

/logscale-nosparse-sequence <<
    400 [ 500 600 700 800 1000 ]
    300 [ 400 500 600 800 1000 ]
    200 [ 300 500 700 1000 ]
>> def

% draw one decade.
% use parent's dictionary as it is.
/logscale-decade {
    1 {
        false
        10 1 logscale-sub { pop exit } if
        10 2 logscale-sub { pop exit } if
        10 5 logscale-sub { pop exit } if
        100 10 logscale-sub { pop exit } if
        100 20 logscale-sub { pop exit } if
        100 50 logscale-sub { pop exit } if
        false 1000 100 logscale-sub { pop pop exit } if pop

        % written is on stack
        v3 1000 750 calcdelta msp ge or {
            nosparse {
                logscale-nosparse-sequence i get {
                    logscale-proc
                } forall
            } {
                i 100 add 100 500 { logscale-proc } for
                use750 { 750 logscale-proc } if
                1000 logscale-proc
            } ifelse
            exit
        } if

        v3 1000 500 calcdelta msp ge {
            use250 {
                [ 250 500 1000 ]
            } {
                [ 200 500 1000 ]
            } ifelse { logscale-proc } forall
            exit
        } if

        v3 300 100 calcdelta msp ge {
            [ 300 1000 ] { logscale-proc } forall
            exit
        } if

        1000 logscale-proc
        exit
    } repeat
} bind def

% start end width minspacing proc <logscale> -
% start end width minspacing proc dict <logscale> -
% proc: val pos lineidx <proc> -
% func: val <func> val
%   return value in propotion to the scale
%   default { log }
% dict: nosparse(bool), use750(bool), use250(bool), calcdelta(proc)
% calcdelta: v3 larger smaller <calcdelta> distance
%   return actual distance distance between v3*larger/1000 and v3*smaller/1000
%   default: log(larger / smaller) * widthperdecade
/logscale {
    1 dict begin
    /nosparse false def
    /use750 false def
    /use250 false def
    dup type /dicttype eq { { def } forall } if
    % /calcdelta defined, use calcdelta.
    % /calcdelta not defined and func defined, use func.
    % both are not defined, use starndard log calcdelta
    currentdict /calcdelta known {
        currentdict /func known not {
            /logscale-no-func trap
        } if
    } {
        currentdict /func known {
            /calcdelta {
                2 index mul 1000 div func exch
                2 index mul 1000 div func exch
                sub wpd mul exch pop
            } bind def
        } {
            /func { log } bind def
            /calcdelta { % v3 larger smaller <calcdelta> dist
                div log wpd mul exch pop
            } bind def
        } ifelse
    } ifelse
    [ /p /msp /w /v2 /v1 ] { exch def } forall
    /y1 v1 func def
    /wpd w v2 func y1 sub div def   % width per decade
    /k 1 def
    /m 10 v1 log ceiling exp def
    /i v1 k m mul div 1000 mul ceiling cvi 1 sub def
    {
        /v3 k m mul def
        logscale-decade
        v3 v2 ge { exit } if
        /k k 10 mul def
        /i 100 def
    } loop
    end
} bind def
メソッド・プロシージャの仕様
logscale

PostScript

start end width minspc proc logscale -
start end width minspc proc optdict logscale -

JavaScript

logscale(start, end, width, minspc, proc, opt = {});

 値 start から end までの対数目盛を描画する。 実際に描画をするのは proc で指定したプロシージャで、 logscale 自体は描画処理を行わない。 widthstart から end までの幅、 minspc は目盛線の最低間隔。 単位は proc と辻褄が合っていれば問わない。

 戻り値はない。

 PostScriptの optdict は辞書、JavaScriptの opt はオブジェクトで、以下のオプションを指定できる。

nosparse

 600以降で抜け落ちる100線がある場合、 false を指定すると600から900までの100線の描画処理を行わない。 true を指定すると600以降についても描画処理を行い proc の呼び出しを行う。 呼び出しの対象とならない100線が存在することに注意。 デフォルトは false

use750

  true にすると、450の位置に線が引けない場合に、150の位置に線が引けるならば750の位置に線を引く(150-200が引けるならば、その5倍の750-1000も引ける)。 750なので100線ではなく50線として描画され、人間が見たときに700や800ではなく750であるという判断材料になる。 デフォルトは false

use250

  true にすると、150の位置に線が引けない場合に、100-200-500-1000ではなく、100-250-500-1000と分割する。 use750 と同様、50線となるので、200ではなく250であるという判断材料になる。 デフォルトは false

func

 軸変換関数。 プロシージャを指定する。 引数として startend の間の値 x が渡されるので、それを軸上の距離に比例する値に変換して返す。 実際に目盛を打つ点( procpos に渡す値)は

\begin{align*} \frac{func(x)-func(start)}{func(end) - func(start)}\cdot width \end{align*}

になる。 通常は { log } 。 完全な対数ではないが、対数っぽい変化をするような量がある場合に、それを func に指定すると、対数目盛っぽく線を間引いて描画できる。

calcdelta

 軸差分関数。 プロシージャを指定する。 引数として基準値 v3 、大きいほうの値に対する積 larger 、小さいほうの値に対する積 smaller の3つを取る。 \(v3\cdot larger / 1000\) と \(v3\cdot smaller / 1000\) に対する目盛の軸上の距離を返す。 通常は

\begin{align*} \frac{func(v3\cdot larger / 1000)-func(v3\cdot smaller/1000)}{func(end) - func(start)}\cdot width \end{align*}

になっている。 calcdelta を指定した場合はfuncも必ず指定しなければならない。

 なお、 wpd (width per decade)という変数が定義されており、

\begin{align*} \frac{width}{func(end) - func(start)} \end{align*}

と定義されているので、以下のように書ける。

{ % v3 larger smaller
    2 index mul 1000 div func exch
    2 index mul 1000 div func exch
    sub wpd mul exch pop
} bind def

 このプロシージャは、2回 func を呼び出すよりも効率のよい軸差分を求める方法がある場合に意味がある。 実際、 func{ log } の場合、

\begin{align*} &\{\log(v3\cdot larger / 1000)-\log(v3\cdot smaller/1000)\}\cdot wpd \eol &= \log\frac{v3\cdot larger / 1000}{v3\cdot smaller/1000}\cdot wpd \eol &= \log\frac{larger}{smaller}\cdot wpd \end{align*}

となり、\(\log\)の計算を1度で済ますことができる上に、 v3 が不要になる。

  proc には3つの引数が渡される。 最初の引数は val で、その線における値(数値型)。 ラベルを描画するときはこの値を文字列に変換して表示すればよい。 ふたつ目は pos で、 start を0、 endwidth とした場合の位置を示す(数値型)。 この位置に目盛線や目盛ラベルを表示する。 3つ目は lineindex で、100から1000までの整数値である。 線種を決定するのに使用する。 なお、 proc の呼び出し順は値の小さい順とは限らない。

 たとえば、

0.8 125 8.5 0.1 { 3 array astore == } logscale

とすれば、8.5の長さの中に最低間隔0.1で、開始値0.8、終了値125で対数目盛を引くとしたら、どの位置にどの値でどんな線を引けばいいかを教えてくれる。 この8.5とか0.1という数字は長さを示すが、単位は好きに決めてよい。 この例ではレターサイズの横幅をイメージしているので、8.5インチに0.1インチ(約2.5mm)間隔で目盛を打つ感じである。

 実際に

/drawline {
    1 dict begin
    /i exch def /pos exch def pop
    i 500 mod 0 eq { 0 setgray } {
    i 100 mod 0 eq { 0.3 setgray } {
    i 50 mod 0 eq { 0.6 setgray } {
        0.8 setgray
    } ifelse } ifelse } ifelse
    pos 72 mul 0 moveto 0 72 rlineto stroke
    end
} bind def
0.8 125 8.5 0.1 { drawline } logscale

とすれば、Ghostscriptのデフォルトページサイズは横8.5インチなので、用紙の下端に所望の目盛が現れる。 pos 72 mul の部分でインチをポイントに変換している。

 あるいは、第2パラメータは無視してしまって、第1パラメータから自分で描画位置を計算してもよい。

 グラフ用紙として描画した例を以下に示す。

 各線はこうなっている。

種類線種太さ[pt]明るさ
1線点線極細0.5
5線破線0.20.5
10線実線0.20.5
50線破線0.40
100線実線0.40
桁線実線0.80

線の太さは0.2ポイント単位にしてあるので、360dpiのプリンタで再現可能である。 このグラフ用紙では5線より細かい線は出てこないが、1線は画面上での見やすさを考えて極細にしてある。 360dpiのプリンタでは極細と0.2ポイントの区別は付かないが、線種で区別が付く。 元々目盛線は明るめに描画しているが、明るさが0ではないものはそれよりも明るく(うすく)描画しているという意味で、この値が1だと真っ白になって見えなくなる。


Copyright (C) 2019-2023 akamoz.jp

$Id: index.htm,v 1.20 2023/10/09 23:20:32 you Exp $