#lang racket/base ;; Data structure that represents a tick, and functions that produce ticks. (require racket/string racket/list racket/contract racket/pretty racket/match racket/sequence unstable/latent-contract/defthing "math.rkt" "contract.rkt" "format.rkt" "utils.rkt" "axis-transform.rkt" "sample.rkt" "date-time.rkt" "currency.rkt") (provide (all-defined-out)) (struct pre-tick (value major?) #:transparent) (struct tick pre-tick (label) #:transparent) (struct ticks (layout format) #:transparent #:property prop:procedure (λ (t x-min x-max) (match-define (ticks layout format) t) (define ts (map pre-tick-inexact->exact (layout x-min x-max))) (match-define (list (pre-tick xs majors) ...) ts) (map tick xs majors (format x-min x-max ts)))) (defcontract ticks-layout/c (real? real? . -> . (listof pre-tick?))) (defcontract ticks-format/c (real? real? (listof pre-tick?) . -> . (listof string?))) (defparam ticks-default-number exact-positive-integer? 4) ;; =================================================================================================== ;; Helpers (define-syntax-rule (with-exact-bounds x-min x-max body ...) (cond [(x-min . > . x-max) (error 'bounds-check "expected min <= max; given min = ~e and max = ~e" x-min x-max)] [else (let ([x-min (inexact->exact x-min)] [x-max (inexact->exact x-max)]) body ...)])) (define (linear-seq-args x-min x-max step) (define start (* (ceiling (/ x-min step)) step)) (define end (* (floor (/ x-max step)) step)) (define num (+ 1 (inexact->exact (round (/ (- end start) step))))) (values start end num)) (define (linear-major-values/step x-min x-max step) (define-values (start end num) (linear-seq-args x-min x-max step)) (linear-seq start end num)) (defproc (linear-minor-values/step [major-xs (listof real?)] [major-step real?] [minor-ticks exact-nonnegative-integer?]) (listof real?) (cond [(or (zero? minor-ticks) (empty? major-xs)) empty] [else (define major-start (first major-xs)) (define minor-step (/ major-step (+ minor-ticks 1))) (for*/list ([x (in-list (cons (- major-start major-step) major-xs))] [i (in-range 1 (+ minor-ticks 1))]) (+ x (* i minor-step)))])) (defproc (tick-values->pre-ticks [major-xs (listof real?)] [minor-xs (listof real?)] ) (listof pre-tick?) (define major-ts (map (λ (x) (pre-tick x #t)) major-xs)) (define minor-ts (map (λ (x) (pre-tick x #f)) minor-xs)) (sort (append major-ts minor-ts) < #:key pre-tick-value)) ;; =================================================================================================== ;; Linear ticks (default tick function, evenly spaced) (defproc (linear-tick-step [x-min real?] [x-max real?] [num-ticks exact-positive-integer?] [base (and/c exact-integer? (>=/c 2))] [divisors (listof exact-positive-integer?)]) real? (define range (- x-max x-min)) (define mag (expt base (floor-log/base base range))) (define epsilon (expt 10 (- (digits-for-range x-min x-max)))) (define e-start (floor-log/base base num-ticks)) (define-values (step diff) (for*/fold ([step #f] [diff +inf.0]) ([e (in-range e-start -2 -1)] [d (in-list (sort divisors <))]) ;; when num-ticks > base, we sometimes must divide by (expt base e) instead of just base (define new-step (/ mag d (expt base e))) ;; find the start, end and number of ticks with this step size (define-values (new-start new-end new-num) (linear-seq-args x-min x-max new-step)) ;; endpoints don't count in the number of ticks (a concession for contour-ticks, which ;; seems to work well outside of contour plots anyway) (let* ([new-num (if ((abs (- new-start x-min)) . < . epsilon) (- new-num 1) new-num)] [new-num (if ((abs (- new-end x-max)) . < . epsilon) (- new-num 1) new-num)]) ;; keep the step size that generates the number of ticks closest to num-ticks (define new-diff (abs (- new-num num-ticks))) (cond [(new-diff . <= . diff) (values new-step new-diff)] [else (values step diff)])))) (if step step (/ range num-ticks))) (defproc (linear-tick-values [x-min real?] [x-max real?] [num-ticks exact-positive-integer?] [base (and/c exact-integer? (>=/c 2))] [divisors (listof exact-positive-integer?)] ) (values (listof real?) (listof real?)) (with-exact-bounds x-min x-max (cond [(= x-min x-max) (values empty empty)] [else (define major-step (linear-tick-step x-min x-max num-ticks base divisors)) (define major-xs (linear-major-values/step x-min x-max major-step)) (define num-major-ticks (length major-xs)) (define minor-xs (let loop ([mult 2]) (cond [(mult . > . 4) empty] [else (define minor-step (linear-tick-step x-min x-max (* mult num-ticks) base divisors)) (define minor-xs (linear-major-values/step x-min x-max minor-step)) (cond [(empty? (remove* minor-xs major-xs)) ;; this covers the major ticks as well; check for additional minor ticks (define real-minor-xs (remove* major-xs minor-xs)) (cond [(empty? real-minor-xs) (loop (+ 1 mult))] [else real-minor-xs])] [else (loop (+ 1 mult))])]))) (values major-xs minor-xs)]))) (defproc (linear-ticks-layout [#:number number exact-positive-integer? (ticks-default-number)] [#:base base (and/c exact-integer? (>=/c 2)) 10] [#:divisors divisors (listof exact-positive-integer?) '(1 2 4 5)] ) ticks-layout/c (λ (x-min x-max) (define-values (major-xs minor-xs) (linear-tick-values x-min x-max number base divisors)) (tick-values->pre-ticks major-xs minor-xs))) (defproc (linear-ticks-format) ticks-format/c (λ (x-min x-max ts) (with-exact-bounds x-min x-max (define digits (digits-for-range x-min x-max)) (for/list ([t (in-list ts)]) (real->plot-label (pre-tick-value t) digits))))) (defproc (linear-ticks [#:number number exact-positive-integer? (ticks-default-number)] [#:base base (and/c exact-integer? (>=/c 2)) 10] [#:divisors divisors (listof exact-positive-integer?) '(1 2 4 5)] ) ticks? #:document-body (ticks (linear-ticks-layout #:number number #:base base #:divisors divisors) (linear-ticks-format))) ;; =================================================================================================== ;; No ticks (defthing no-ticks-layout ticks-layout/c #:document-value (λ (x-min x-max) empty)) (defthing no-ticks-format ticks-format/c #:document-value (λ (x-min x-max pre-ticks) (map (λ (_) "") pre-ticks))) (defthing no-ticks ticks? #:document-value (ticks no-ticks-layout no-ticks-format)) ;; =================================================================================================== ;; Exponential ticks (for log scale) (defproc (log-ticks-layout [#:number number exact-positive-integer? (ticks-default-number)] [#:base base (and/c exact-integer? (>=/c 2)) 10] ) ticks-layout/c (λ (x-min x-max) (with-exact-bounds x-min x-max (when ((exact->inexact x-min) . <= . 0) (raise-type-error 'log-ticks-layout "positive real" 0 x-min x-max)) (define log-start (ceiling-log/base base x-min)) (define log-end (floor-log/base base x-max)) (define skip (max 1 (ceiling (/ (+ 1 (- log-end log-start)) number)))) (filter (λ (t) (<= x-min (pre-tick-value t) x-max)) (append* (for/list ([log-x (in-range (- log-start 1) (+ log-end 2))] [m (in-cycle (in-range skip))]) (define x (expt base log-x)) (cond [(= skip 1) (for/list ([i (in-range 0 (sub1 base))]) (pre-tick (+ x (* i x)) (and (zero? i) (zero? m))))] [else (list (pre-tick x (zero? m)))]))))))) (defproc (log-ticks-format [#:base base (and/c exact-integer? (>=/c 2)) 10]) ticks-format/c (define base-str (number->string base)) (λ (x-min x-max ts) (with-exact-bounds x-min x-max (define epsilon (expt 10 (- (digits-for-range x-min x-max)))) (define base-digits (digits-for-range 0 base)) (for/list ([t (in-list ts)]) (define x (pre-tick-value t)) (define log-x (floor-log/base base x)) (define (major-str) (if (zero? log-x) "1" (format "~a~a" base-str (integer->superscript log-x)))) (cond [((abs (- x (expt base log-x))) . < . epsilon) (major-str)] [(zero? log-x) (real->plot-label x base-digits)] [else (format "~a×~a" (real->plot-label (/ x (expt base log-x)) base-digits) (major-str))]))))) (defproc (log-ticks [#:number number exact-positive-integer? (ticks-default-number)] [#:base base (and/c exact-integer? (>=/c 2)) 10] ) ticks? #:document-body (ticks (log-ticks-layout #:number number #:base base) (log-ticks-format #:base base))) ;; =================================================================================================== ;; Date/time helpers (defproc (find-linear-tick-step [x-min real?] [x-max real?] [num-ticks exact-positive-integer?] [steps (listof real?)]) real? (with-exact-bounds x-min x-max (define epsilon (expt 10 (- (digits-for-range x-min x-max)))) (define-values (step diff) (for/fold ([step #f] [diff +inf.0]) ([new-step (in-list (sort steps <))]) (define-values (new-start new-end new-num) (linear-seq-args x-min x-max new-step)) ;; endpoints don't count in number of ticks (see linear-tick-step) (let* ([new-num (if ((abs (- new-start x-min)) . < . epsilon) (- new-num 1) new-num)] [new-num (if ((abs (- new-end x-max)) . < . epsilon) (- new-num 1) new-num)]) (define new-diff (abs (- new-num num-ticks))) (cond [(new-diff . <= . diff) (values new-step new-diff)] [else (values step diff)])))) step)) (define (count-changing-fields formatter fmt-list xs) (let ([fmt-list (filter symbol? fmt-list)]) (define formatted-dates (for/list ([x (in-list xs)]) (apply-formatter formatter fmt-list x))) (count (λ (fields) (not (apply equal?* fields))) (transpose formatted-dates)))) ;; Find the shortest format string that has the maximum number of changing fields (define (choose-format-list formatter fmt-lists xs) (let ([fmt-lists (sort fmt-lists < #:key (λ (fmt-list) (count symbol? fmt-list)) #:cache-keys? #t)]) (argmax (λ (fmt-list) (count-changing-fields formatter fmt-list xs)) fmt-lists))) ;; =================================================================================================== ;; Date ticks (defthing 24h-descending-date-ticks-formats (listof string?) #:document-value '("~Y-~m-~d ~H:~M:~f" "~Y-~m-~d ~H:~M" "~Y-~m-~d ~Hh" "~Y-~m-~d" "~Y-~m" "~Y" "~m-~d ~H:~M:~f" "~m-~d ~H:~M" "~m-~d ~Hh" "~m-~d" "~H:~M:~f" "~H:~M" "~Hh" "~M:~fs" "~Mm" "~fs")) (defthing 12h-descending-date-ticks-formats (listof string?) #:document-value '("~Y-~m-~d ~I:~M:~f ~p" "~Y-~m-~d ~I:~M ~p" "~Y-~m-~d ~I ~p" "~Y-~m-~d" "~Y-~m" "~Y" "~m-~d ~I:~M:~f ~p" "~m-~d ~I:~M ~p" "~m-~d ~I ~p" "~m-~d" "~I:~M:~f ~p" "~I:~M ~p" "~I ~p" "~M:~fs" "~Mm" "~fs")) (defparam date-ticks-formats (listof string?) 24h-descending-date-ticks-formats) ;; Tick steps to try, in seconds (define date-steps (list 1 2 4 5 10 15 20 30 40 45 seconds-per-minute (* 2 seconds-per-minute) (* 4 seconds-per-minute) (* 5 seconds-per-minute) (* 10 seconds-per-minute) (* 15 seconds-per-minute) (* 20 seconds-per-minute) (* 30 seconds-per-minute) (* 45 seconds-per-minute) seconds-per-hour (* 2 seconds-per-hour) (* 3 seconds-per-hour) (* 4 seconds-per-hour) (* 6 seconds-per-hour) (* 8 seconds-per-hour) (* 12 seconds-per-hour) (* 18 seconds-per-hour) seconds-per-day (* 2 seconds-per-day) (* 4 seconds-per-day) (* 5 seconds-per-day) (* 10 seconds-per-day) seconds-per-week (* 2 seconds-per-week) avg-seconds-per-month (* 2 avg-seconds-per-month) (* 3 avg-seconds-per-month) (* 4 avg-seconds-per-month) (* 6 avg-seconds-per-month) (* 8 avg-seconds-per-month) (* 9 avg-seconds-per-month) avg-seconds-per-year (* 2 avg-seconds-per-year) (* 4 avg-seconds-per-year) (* 5 avg-seconds-per-year))) (define (date-tick-values x-min x-max num-ticks) (with-exact-bounds x-min x-max (cond [(= x-min x-max) (values empty empty)] [else (define range (- x-max x-min)) (define step (cond [(range . < . (* num-ticks (first date-steps))) (linear-tick-step x-min x-max num-ticks 10 '(1 2 4 5))] [(range . > . (* num-ticks (last date-steps))) (* avg-seconds-per-year (linear-tick-step (/ x-min avg-seconds-per-year) (/ x-max avg-seconds-per-year) num-ticks 10 '(1 2 4 5)))] [else (find-linear-tick-step x-min x-max num-ticks date-steps)])) (define date-round (cond [(step . >= . avg-seconds-per-year) utc-seconds-round-year] [(step . >= . avg-seconds-per-month) utc-seconds-round-month] [else (λ (d) d)])) (define major-xs (linear-major-values/step x-min x-max step)) (values (map date-round major-xs) empty)]))) (defproc (date-ticks-layout [#:number number exact-positive-integer? (ticks-default-number)] ) ticks-layout/c (λ (x-min x-max) (define-values (major-xs minor-xs) (date-tick-values x-min x-max number)) (tick-values->pre-ticks major-xs minor-xs))) (defproc (date-ticks-format [#:formats formats (listof string?) (date-ticks-formats)]) ticks-format/c (define fmt-lists (map parse-format-string formats)) (λ (x-min x-max ts) (with-exact-bounds x-min x-max (define formatter (plot-date-formatter x-min x-max)) (define xs (map pre-tick-value ts)) (cond [(empty? xs) empty] [else (define fmt-list (choose-format-list formatter fmt-lists xs)) (cons (string-append* (apply-formatter formatter fmt-list (first xs))) (for/list ([last-x (in-list xs)] [x (in-list (rest xs))]) (define fmt-list (choose-format-list formatter fmt-lists (list last-x x))) (string-append* (apply-formatter formatter fmt-list x))))])))) (defproc (date-ticks [#:number number exact-positive-integer? (ticks-default-number)] [#:formats formats (listof string?) (date-ticks-formats)] ) ticks? #:document-body (ticks (date-ticks-layout #:number number) (date-ticks-format #:formats formats))) ;; =================================================================================================== ;; Time ticks (defthing 24h-descending-time-ticks-formats (listof string?) #:document-value '("~dd ~H:~M:~f" "~dd ~H:~M" "~dd ~Hh" "~dd" "~H:~M:~f" "~H:~M" "~Hh" "~M:~fs" "~Mm" "~fs")) (defthing 12h-descending-time-ticks-formats (listof string?) #:document-value '("~dd ~I:~M:~f ~p" "~dd ~I:~M ~p" "~dd ~I ~p" "~dd" "~I:~M:~f ~p" "~I:~M ~p" "~I ~p" "~M:~fs" "~Mm" "~fs")) (defparam time-ticks-formats (listof string?) 24h-descending-time-ticks-formats) ;; Tick steps to try, in seconds (define time-steps (list 1 2 4 5 10 15 20 30 40 45 seconds-per-minute (* 2 seconds-per-minute) (* 4 seconds-per-minute) (* 5 seconds-per-minute) (* 10 seconds-per-minute) (* 15 seconds-per-minute) (* 20 seconds-per-minute) (* 30 seconds-per-minute) (* 45 seconds-per-minute) seconds-per-hour (* 2 seconds-per-hour) (* 3 seconds-per-hour) (* 4 seconds-per-hour) (* 6 seconds-per-hour) (* 8 seconds-per-hour) (* 12 seconds-per-hour) (* 18 seconds-per-hour) seconds-per-day (* 2 seconds-per-day) (* 4 seconds-per-day) (* 5 seconds-per-day) (* 10 seconds-per-day) (* 15 seconds-per-day) (* 30 seconds-per-day) (* 60 seconds-per-day) (* 90 seconds-per-day))) (define (time-tick-values x-min x-max num-ticks) (with-exact-bounds x-min x-max (cond [(= x-min x-max) (values empty empty)] [else (define range (- x-max x-min)) (define step (cond [(range . < . (* num-ticks (first time-steps))) (linear-tick-step x-min x-max num-ticks 10 '(1 2 4 5))] [(range . > . (* num-ticks (last time-steps))) (* seconds-per-day (linear-tick-step (/ x-min seconds-per-day) (/ x-max seconds-per-day) num-ticks 10 '(1 2 4 5)))] [else (find-linear-tick-step x-min x-max num-ticks time-steps)])) (define major-xs (linear-major-values/step x-min x-max step)) (values major-xs empty)]))) (defproc (time-ticks-layout [#:number number exact-positive-integer? (ticks-default-number)] ) ticks-layout/c (λ (x-min x-max) (define-values (major-xs minor-xs) (time-tick-values x-min x-max number)) (tick-values->pre-ticks major-xs minor-xs))) (defproc (time-ticks-format [#:formats formats (listof string?) (time-ticks-formats)]) ticks-format/c (define fmt-lists (map parse-format-string formats)) (λ (x-min x-max ts) (with-exact-bounds x-min x-max (define formatter (plot-time-formatter x-min x-max)) (define xs (map pre-tick-value ts)) (cond [(empty? xs) empty] [else (define fmt-list (choose-format-list formatter fmt-lists xs)) (cons (string-append* (apply-formatter formatter fmt-list (first xs))) (for/list ([last-x (in-list xs)] [x (in-list (rest xs))]) (define fmt-list (choose-format-list formatter fmt-lists (list last-x x))) (string-append* (apply-formatter formatter fmt-list x))))])))) (defproc (time-ticks [#:number number exact-positive-integer? (ticks-default-number)] [#:formats formats (listof string?) (time-ticks-formats)] ) ticks? #:document-body (ticks (time-ticks-layout #:number number) (time-ticks-format #:formats formats))) ;; =================================================================================================== ;; Byte and bit ticks ;; "", Kilo, Mega, Giga, Tera, Peta, Exa, Zeta, Yotta (define byte-suffixes #("B" "KB" "MB" "GB" "TB" "PB" "EB" "ZB" "YB")) (define bit-suffixes #("b" "Kb" "Mb" "Gb" "Tb" "Pb" "Eb" "Zb" "Yb")) (defproc (bit/byte-ticks-format [#:size size (or/c 'byte 'bit) 'byte] [#:kind kind (or/c 'CS 'SI) 'CS]) ticks-format/c (λ (x-min x-max ts) (with-exact-bounds x-min x-max (define suffixes (if (eq? size 'bit) bit-suffixes byte-suffixes)) (define-values (base pow) (case kind [(SI) (values 10 3)] [else (values 2 10)])) (define x-largest (max* (abs x-min) (abs x-max))) (define b (floor-log/base (expt base pow) x-largest)) (define format-str (cond [(and (b . >= . 0) (b . < . (vector-length suffixes))) (format "~a ~a" "~a" (vector-ref suffixes b))] [else (format "~a×~a~a ~a" "~a" base (integer->superscript (* b pow)) (vector-ref suffixes 0))])) (define unit (expt base (* b pow))) (define digits (digits-for-range (/ x-min unit) (/ x-max unit))) (for/list ([t (in-list ts)]) (define unit-x (/ (pre-tick-value t) unit)) (format format-str (real->plot-label unit-x digits #f)))))) (defproc (bit/byte-ticks [#:number number exact-positive-integer? (ticks-default-number)] [#:size size (or/c 'byte 'bit) 'byte] [#:kind kind (or/c 'CS 'SI) 'CS] ) ticks? #:document-body (define si? (eq? kind 'SI)) (ticks (linear-ticks-layout #:number number #:base (if si? 10 2) #:divisors (if si? '(1 2 4 5) '(1 2))) (bit/byte-ticks-format #:size size #:kind kind))) ;; =================================================================================================== ;; Currency ;; US "short scale" suffixes (defthing us-currency-scales (listof string?) #:document-value '("" "K" "M" "B" "T")) ;; The UK officially uses the short scale since 1974 ;; Million is abbreviated "m" instead of "mn" because "mn" stands for minutes (defthing uk-currency-scales (listof string?) #:document-value '("" "k" "m" "bn" "tr")) ;; European countries use the long scale: million, milliard, billion (defthing eu-currency-scales (listof string?) #:document-value '("" "K" "M" "Md" "B")) ;; The larger the scale suffixes get, the less standardized they are; so we stop at billion (long) ;; US negative amounts are in parenthesis: (defthing us-currency-formats (list/c string? string? string?) #:document-value '("~$~w.~f~s" "(~$~w.~f~s)" "~$0")) ;; The UK is more reasonable, using a negative sign for negative amounts: (defthing uk-currency-formats (list/c string? string? string?) #:document-value '("~$~w.~f~s" "-~$~w.~f~s" "~$0")) ;; The more common EU format (e.g. France, Germany, Italy, Spain): (defthing eu-currency-formats (list/c string? string? string?) #:document-value '("~w,~f ~s~$" "-~w,~f ~s~$" "0 ~$")) (defparam currency-ticks-scales (listof string?) us-currency-scales) (defparam currency-ticks-formats (list/c string? string? string?) us-currency-formats) (struct amount-data (sign whole fractional unit suffix) #:transparent) (define (currency-formatter x-min x-max) (λ (fmt data) (case fmt [(~$) (amount-data-sign data)] [(~w) (number->string (amount-data-whole data))] [(~f) (match-define (amount-data _sign _whole f unit _suffix) data) (define digits (digits-for-range (/ x-min unit) (/ x-max unit))) (cond [(= 1 unit) (substring (real->decimal-string* f 2 (max 2 digits)) 2)] [(zero? f) "0"] [else (substring (real->decimal-string* f 1 (max 1 digits)) 2)])] [(~s) (amount-data-suffix data)] [else #f]))) (defproc (currency-ticks-format [#:kind kind (or/c string? symbol?) 'USD] [#:scales scales (listof string?) (currency-ticks-scales)] [#:formats formats (list/c string? string? string?) (currency-ticks-formats)] ) ticks-format/c (match-define (list positive-format-string negative-format-string zero-format-string) formats) (define positive-format-list (parse-format-string positive-format-string)) (define negative-format-list (parse-format-string negative-format-string)) (define zero-format-list (parse-format-string zero-format-string)) (define suffixes (list->vector scales)) (define n (vector-length suffixes)) (λ (x-min x-max ts) (with-exact-bounds x-min x-max (define formatter (currency-formatter x-min x-max)) (define sign (cond [(string? kind) kind] [else (hash-ref currency-code->sign kind (λ () (symbol->string kind)))])) (define x-largest (max* (abs x-min) (abs x-max))) (define b (let ([b (floor-log/base 1000 x-largest)]) (if (b . < . 0) (+ b 1) b))) (define suffix (cond [(and (b . >= . 0) (b . < . n)) (vector-ref suffixes b)] [else (format "×10~a" (integer->superscript (* b 3)))])) (define unit (cond [(= 0 (string-length suffix)) 1] [else (expt 1000 b)])) (for/list ([t (in-list ts)]) (define x (pre-tick-value t)) (define format-list (cond [(positive? x) positive-format-list] [(negative? x) negative-format-list] [else zero-format-list])) (define unit-x (/ (abs x) unit)) (define whole (inexact->exact (floor unit-x))) (define frac (- unit-x whole)) (string-append* (apply-formatter formatter format-list (amount-data sign whole frac unit suffix))))))) (defproc (currency-ticks [#:number number exact-positive-integer? (ticks-default-number)] [#:kind kind (or/c string? symbol?) 'USD] [#:scales scales (listof string?) (currency-ticks-scales)] [#:formats formats (list/c string? string? string?) (currency-ticks-formats)] ) ticks? #:document-body (ticks (linear-ticks-layout #:number number) (currency-ticks-format #:kind kind #:scales scales #:formats formats))) ;; =================================================================================================== ;; Fractions (define (format-fraction x) (cond [(inexact? x) (format-fraction (inexact->exact x))] [(x . < . 0) (format "-~a" (format-fraction (- x)))] [(x . = . 0) "0"] [(x . < . 1) (format "~a/~a" (numerator x) (denominator x))] [else (define d (denominator x)) (cond [(d . = . 1) (format "~a" (numerator x))] [else (define w (floor x)) (let ([x (- x w)]) (format "~a ~a/~a" w (numerator x) (denominator x)))])])) (defproc (fraction-ticks-format [#:base base (and/c exact-integer? (>=/c 2)) 10] [#:divisors divisors (listof exact-positive-integer?) '(1 2 3 4 5)] ) ticks-format/c (define fracs (remove-duplicates (map (λ (d) (/ d base)) divisors))) (λ (x-min x-max ts) (define digits (digits-for-range x-min x-max base (ceiling-log/base base 1000))) (define fracs (remove-duplicates (map (λ (d) (* (/ base d) (expt base (- digits)))) divisors))) (for/list ([t (in-list ts)]) (define x (inexact->exact (pre-tick-value t))) (define xs (for/list ([frac (in-list fracs)]) (* frac (round (/ x frac))))) (format-fraction (argmin (λ (y) (abs (- x y))) xs))))) (defproc (fraction-ticks [#:base base (and/c exact-integer? (>=/c 2)) 10] [#:divisors divisors (listof exact-positive-integer?) '(1 2 3 4 5)] ) ticks? #:document-body (ticks (linear-ticks #:base base #:divisors divisors) (fraction-ticks-format #:base base #:divisors divisors))) ;; =================================================================================================== ;; Tick combinators (defproc (ticks-mimic [thunk (-> ticks?)]) ticks? (ticks (λ (x-min x-max) ((ticks-layout (thunk)) x-min x-max)) (λ (x-min x-max ts) ((ticks-format (thunk)) x-min x-max ts)))) (defproc (ticks-scale [t ticks?] [fun invertible-function?]) ticks? (match-define (invertible-function f g) fun) (match-define (ticks layout format) t) (ticks (λ (x-min x-max) (define ts (layout (f x-min) (f x-max))) (for/list ([t (in-list ts)]) (match-define (pre-tick x major?) t) (pre-tick (g x) major?))) (λ (x-min x-max ts) (format (f x-min) (f x-max) (map (λ (t) (match-define (pre-tick x major?) t) (pre-tick (f x) major?)) ts))))) (defproc (ticks-add [t ticks?] [xs (listof real?)] [major? boolean? #t]) ticks? (match-define (ticks layout format) t) (ticks (λ (x-min x-max) (append (layout x-min x-max) (for/list ([x (in-list xs)]) (pre-tick x major?)))) format)) (defproc (linear-scale [m rational?] [b rational? 0]) invertible-function? #:document-body (invertible-function (λ (x) (+ (* m x) b)) (λ (y) (/ (- y b) m)))) ;; =================================================================================================== ;; Tick utils (define (same-label? t1 t2) (string=? (tick-label t1) (tick-label t2))) (define (collapse-equiv-ticks ts near-format-string) (match-define (list (tick xs majors labels) ...) ts) (define x (/ (apply + xs) (length ts))) (define major? (ormap values majors)) (define label1 (first labels)) (define label2 (last labels)) (define label (cond [(string=? label1 label2) label1] [else (format near-format-string label1 label2)])) (tick x major? label)) (defproc (collapse-ticks [ts (listof tick?)] [near? (tick? tick? . -> . boolean?)] [near-format-string string? "~a|~a"]) (listof tick?) (let ([ts (sort ts < #:key pre-tick-value)]) (append* (for/list ([ts (in-list (group-neighbors ts (λ (t1 t2) (or (same-label? t1 t2) (near? t1 t2)))))]) (define n (length ts)) (define m (count pre-tick-major? ts)) (cond [(n . <= . 1) ts] [(m . = . 0) (list (collapse-equiv-ticks ts near-format-string))] [(m . = . 1) (filter pre-tick-major? ts)] [else (list (collapse-equiv-ticks (filter pre-tick-major? ts) near-format-string))]))))) (defproc (pre-tick-inexact->exact [t pre-tick?]) pre-tick? (match-define (pre-tick x major?) t) (pre-tick (inexact->exact x) major?)) (defproc (tick-inexact->exact [t tick?]) tick? (match-define (tick x major? label) t) (tick (inexact->exact x) major? label)) (defproc (contour-ticks [z-ticks ticks?] [z-min real?] [z-max real?] [levels (or/c 'auto exact-positive-integer? (listof real?))] [intervals? boolean?]) (listof tick?) (define epsilon (expt 10 (- (digits-for-range z-min z-max)))) (match-define (ticks layout format) z-ticks) ;; initial tick layout (define ts (cond [(eq? levels 'auto) (filter pre-tick-major? (layout z-min z-max))] [else (define zs (cond [(list? levels) (filter (λ (z) (<= z-min z z-max)) levels)] [else (linear-seq z-min z-max levels #:start? #f #:end? #f)])) (map (λ (z) (pre-tick z #t)) zs)])) (let* (;; remove z-min tick (or the one close to it) if present [ts (if (and (not (empty? ts)) ((abs (- z-min (pre-tick-value (first ts)))) . < . epsilon)) (rest ts) ts)] ;; remove z-max tick (or the one close to it) if present [ts (if (and (not (empty? ts)) ((abs (- z-max (pre-tick-value (last ts)))) . < . epsilon)) (drop-right ts 1) ts)] ;; add z-min and z-max if doing intervals [ts (cond [(not intervals?) ts] [else (append (list (pre-tick z-min #t)) ts (list (pre-tick z-max #t)))])]) ;; format the ticks (match-define (list (pre-tick zs majors) ...) ts) (define labels (format z-min z-max ts)) (map tick zs majors labels))) (defproc (format-tick-labels [x-ticks ticks?] [x-min real?] [x-max real?] [xs (listof real?)] ) (listof string?) (match-define (ticks layout format) x-ticks) (let* ([tick-xs (map pre-tick-value (filter pre-tick-major? (layout x-min x-max)))] [tick-xs (remove* xs tick-xs)] [tick-xs (if (empty? tick-xs) empty (list (apply min tick-xs) (apply max tick-xs)))] [tick-xs (sort (append xs tick-xs) <)]) (define ts (map (λ (x) (pre-tick x #t)) tick-xs)) (for/list ([x (in-list tick-xs)] [l (in-list (format x-min x-max ts))] #:when (member x xs)) l)))