diff --git a/plot-doc/plot/scribblings/params.scrbl b/plot-doc/plot/scribblings/params.scrbl index c5293c45..9ac02ddd 100644 --- a/plot-doc/plot/scribblings/params.scrbl +++ b/plot-doc/plot/scribblings/params.scrbl @@ -196,6 +196,17 @@ The symbol, and its size and opacity, used in point plots. Used as default keyword arguments of @racket[points] and @racket[points3d]. } +@deftogether[((defparam point-x-jitter x-jitter (>=/c 0) #:value 0) + (defparam point-y-jitter y-jitter (>=/c 0) #:value 0) + (defparam point-z-jitter z-jitter (>=/c 0) #:value 0))]{ +When any of @(racket x-jitter), @(racket y-jitter), or @(racket z-jitter) are non-zero, + @(racket points) and @(racket points3d) will produce points randomly translated from their + original position along the x, y, or z axis, respectively. +For instance, if each parameter is set to 1, then @(racket points '(0 0)) will produce a random point + in a square of area 1 centered at @(racket '(0 0)). +Likewise @(racket points3d) will make a random point within a unit cube centered at @(racket '(0 0 0)). +} + @deftogether[((defparam point-color color plot-color/c #:value 0) (defparam point-line-width width (>=/c 0) #:value 1))]{ The color and line width of symbols used in point plots and labeled points. diff --git a/plot-doc/plot/scribblings/renderer2d.scrbl b/plot-doc/plot/scribblings/renderer2d.scrbl index f28910d5..5c54b0bf 100644 --- a/plot-doc/plot/scribblings/renderer2d.scrbl +++ b/plot-doc/plot/scribblings/renderer2d.scrbl @@ -52,6 +52,8 @@ Not every renderer-producing function has a @(racket #:label) argument; for exam [#:sym sym point-sym/c (point-sym)] [#:color color plot-color/c (point-color)] [#:fill-color fill-color (or/c plot-color/c 'auto) 'auto] + [#:x-jitter x-jitter (>=/c 0) (point-x-jitter)] + [#:y-jitter y-jitter (>=/c 0) (point-y-jitter)] [#:size size (>=/c 0) (point-size)] [#:line-width line-width (>=/c 0) (point-line-width)] [#:alpha alpha (real-in 0 1) (point-alpha)] @@ -76,6 +78,33 @@ Readers of the first plot could only guess that the random points were generated The @(racket #:sym) argument may be any integer, a Unicode character or string, or a symbol in @(racket known-point-symbols). Use an integer when you need different points but don't care exactly what they are. + +When @(racket x-jitter) or @(racket y-jitter) is non-zero, all points are randomly translated from their original position. +Specifically, each point @(racket p) is moved to a random location inside a rectangle centered at @(racket p) with width at most @(racket x-jitter) and height at most @(y-jitter). +The new points will lie within [@(racket x-min), @(racket x-max)] and [@(racket y-min), @(racket y-max)] if these bounds are non-@(racket #f). + +@interaction[#:eval plot-eval + (plot + (points (for/list ([_i (in-range 999)]) + (list (* 10 (random)) 0)) + #:alpha 0.4 + #:y-jitter 1 + #:sym 'fullcircle1 + #:color "blue") + #:x-min -5 #:x-max 5 #:y-min -5 #:y-max 5)] + +Randomly moving data points is almost always a bad idea, but jittering in a controlled manner can sometimes be useful. +For example: +@margin-note{More examples of jittering: + @hyperlink["http://kieranhealy.org/blog/archives/2015/02/03/another-look-at-the-california-vaccination-data/"]{Another Look at the California Vaccination Data} + and + @hyperlink["https://pavelfatin.com/typing-with-pleasure/"]{Typing with Pleasure}} + +@itemlist[ + @item{To highlight the size of a dense (or @hyperlink["https://en.wiktionary.org/wiki/overplotting"]{overplotted}) sample.} + @item{To see the distribution of 1-dimensional data; as a substitute for box or violin plots.} + @item{To anonymize spatial data, showing i.e. an office's neighborhood but hiding its address.} +] } @defproc[(vector-field diff --git a/plot-doc/plot/scribblings/renderer3d.scrbl b/plot-doc/plot/scribblings/renderer3d.scrbl index ce561551..2762e1f9 100644 --- a/plot-doc/plot/scribblings/renderer3d.scrbl +++ b/plot-doc/plot/scribblings/renderer3d.scrbl @@ -28,6 +28,9 @@ See @secref["renderer2d-function-arguments"] for a detailed example. [#:sym sym point-sym/c (point-sym)] [#:color color plot-color/c (point-color)] [#:fill-color fill-color (or/c plot-color/c 'auto) 'auto] + [#:x-jitter x-jitter (>=/c 0) (point-x-jitter)] + [#:y-jitter y-jitter (>=/c 0) (point-y-jitter)] + [#:z-jitter z-jitter (>=/c 0) (point-z-jitter)] [#:size size (>=/c 0) (point-size)] [#:line-width line-width (>=/c 0) (point-line-width)] [#:alpha alpha (real-in 0 1) (point-alpha)] @@ -51,6 +54,13 @@ For example, a scatter plot of points sampled uniformly from the surface of a sp (plot3d (points3d (map vector xs ys zs) #:sym 'dot) #:altitude 25)] + +When @(racket x-jitter), @(racket y-jitter), or @(racket z-jitter) is non-zero, + each point @(racket p) is translated to a random location inside a box centered at @(racket p) with width @(racket x-jitter), height @(racket y-jitter), and depth @(racket z-jitter). +The new points will lie within [@(racket x-min), @(racket x-max)] etc. if these bounds are non-@(racket #f). + +Note that adding random noise to data, via jittering or otherwise, is usually a bad idea. +See the documentation for @(racket points) for examples where jittering may be appropriate. } @defproc[(vector-field3d diff --git a/plot-lib/plot/private/common/math.rkt b/plot-lib/plot/private/common/math.rkt index 2aa88133..199ba503 100644 --- a/plot-lib/plot/private/common/math.rkt +++ b/plot-lib/plot/private/common/math.rkt @@ -548,6 +548,27 @@ [x (if a (max x a) x)]) x)) +;; Return a Real drawn randomly from the interval [(- val jitter) (+ val jitter)]. +;; If #:ivl is given, result will lie within the given interval. +(: apply-jitter (->* [Real Nonnegative-Real] [#:ivl ivl] Real)) +(define (apply-jitter val jitter #:ivl [interval unknown-ivl]) + (let ([offset (* (random) jitter)]) + (if (zero? (random 2)) + (let ([val- (- val offset)]) + (max val- (or (ivl-min interval) val-))) + (let ([val+ (+ val offset)]) + (min val+ (or (ivl-max interval) val+)))))) + +;; Precondition: all vectors in the arguments are the same length +(:: points-apply-jitters (->* [(Listof (Vectorof Real)) (Vectorof Nonnegative-Real)] [#:ivls (U (Vectorof ivl) #f)] Void)) +(define (points-apply-jitters vals jitters #:ivls [ivls #f]) + (for ([v (in-list vals)]) + (for ([i (in-range (vector-length jitters))]) + (let ([v_i (vector-ref v i)] + [iv (if ivls (vector-ref ivls i) unknown-ivl)] + [jt (vector-ref jitters i)]) + (vector-set! v i (apply-jitter v_i jt #:ivl iv)))))) + ;; =================================================================================================== ;; Rectangles diff --git a/plot-lib/plot/private/common/parameters.rkt b/plot-lib/plot/private/common/parameters.rkt index fbad2b5f..7b5e3bc9 100644 --- a/plot-lib/plot/private/common/parameters.rkt +++ b/plot-lib/plot/private/common/parameters.rkt @@ -179,6 +179,9 @@ (defparam point-sym Point-Sym 'circle) (defparam point-color Plot-Color 0) +(defparam point-x-jitter Real 0) +(defparam point-y-jitter Real 0) +(defparam point-z-jitter Real 0) (defparam2 point-size Real Nonnegative-Real 6 (nonnegative-rational 'point-size)) (defparam2 point-line-width Real Nonnegative-Real 1 (nonnegative-rational 'point-line-width)) (defparam2 point-alpha Real Nonnegative-Real 1 (unit-ivl 'point-alpha)) diff --git a/plot-lib/plot/private/plot2d/point.rkt b/plot-lib/plot/private/plot2d/point.rkt index 582561d2..865e2c7a 100644 --- a/plot-lib/plot/private/plot2d/point.rkt +++ b/plot-lib/plot/private/plot2d/point.rkt @@ -39,6 +39,8 @@ #:sym Point-Sym #:color Plot-Color #:fill-color (U Plot-Color 'auto) + #:x-jitter Nonnegative-Real + #:y-jitter Nonnegative-Real #:size Nonnegative-Real #:line-width Nonnegative-Real #:alpha Nonnegative-Real @@ -50,6 +52,8 @@ #:sym [sym (point-sym)] #:color [color (point-color)] #:fill-color [fill-color 'auto] + #:x-jitter [x-jitter (point-x-jitter)] + #:y-jitter [y-jitter (point-y-jitter)] #:size [size (point-size)] #:line-width [line-width (point-line-width)] #:alpha [alpha (point-alpha)] @@ -67,16 +71,19 @@ [vs (filter vrational? vs)]) (cond [(empty? vs) (renderer2d #f #f #f #f)] - [else (match-define (list (vector #{xs : (Listof Real)} #{ys : (Listof Real)}) ...) vs) - (let ([x-min (if x-min x-min (apply min* xs))] - [x-max (if x-max x-max (apply max* xs))] - [y-min (if y-min y-min (apply min* ys))] - [y-max (if y-max y-max (apply max* ys))] - [fill-color (if (eq? fill-color 'auto) (->pen-color color) fill-color)]) - (renderer2d - (vector (ivl x-min x-max) (ivl y-min y-max)) #f default-ticks-fun - (points-render-fun vs sym color fill-color - size line-width alpha label)))]))])) + [else + (unless (= 0 x-jitter y-jitter) + (points-apply-jitters vs (vector x-jitter y-jitter) #:ivls (vector (ivl x-min x-max) (ivl y-min y-max)))) + (match-define (list (vector #{xs : (Listof Real)} #{ys : (Listof Real)}) ...) vs) + (let ([x-min (if x-min x-min (apply min* xs))] + [x-max (if x-max x-max (apply max* xs))] + [y-min (if y-min y-min (apply min* ys))] + [y-max (if y-max y-max (apply max* ys))] + [fill-color (if (eq? fill-color 'auto) (->pen-color color) fill-color)]) + (renderer2d + (vector (ivl x-min x-max) (ivl y-min y-max)) #f default-ticks-fun + (points-render-fun vs sym color fill-color + size line-width alpha label)))]))])) ;; =================================================================================================== ;; Vector fields diff --git a/plot-lib/plot/private/plot3d/point.rkt b/plot-lib/plot/private/plot3d/point.rkt index 893af09f..8436127e 100644 --- a/plot-lib/plot/private/plot3d/point.rkt +++ b/plot-lib/plot/private/plot3d/point.rkt @@ -39,6 +39,9 @@ #:sym Point-Sym #:color Plot-Color #:fill-color (U Plot-Color 'auto) + #:x-jitter Nonnegative-Real + #:y-jitter Nonnegative-Real + #:z-jitter Nonnegative-Real #:size Nonnegative-Real #:line-width Nonnegative-Real #:alpha Nonnegative-Real @@ -51,6 +54,9 @@ #:sym [sym (point-sym)] #:color [color (point-color)] #:fill-color [fill-color 'auto] + #:x-jitter [x-jitter (point-x-jitter)] + #:y-jitter [y-jitter (point-y-jitter)] + #:z-jitter [z-jitter (point-z-jitter)] #:size [size (point-size)] #:line-width [line-width (point-line-width)] #:alpha [alpha (point-alpha)] @@ -70,6 +76,11 @@ [vs (filter vrational? vs)]) (cond [(empty? vs) (renderer3d #f #f #f #f)] [else + (unless (= 0 x-jitter y-jitter z-jitter) + (points-apply-jitters vs (vector x-jitter y-jitter z-jitter) + #:ivls (vector (ivl x-min x-max) + (ivl y-min y-max) + (ivl z-min z-max)))) (match-define (list (vector #{xs : (Listof Real)} #{ys : (Listof Real)} #{zs : (Listof Real)}) diff --git a/plot-lib/plot/private/utils-and-no-gui.rkt b/plot-lib/plot/private/utils-and-no-gui.rkt index 471a805c..3c5ef2a3 100644 --- a/plot-lib/plot/private/utils-and-no-gui.rkt +++ b/plot-lib/plot/private/utils-and-no-gui.rkt @@ -126,6 +126,9 @@ point-sym point-color point-size + point-x-jitter + point-y-jitter + point-z-jitter point-line-width point-alpha vector-field-samples diff --git a/plot-lib/plot/utils.rkt b/plot-lib/plot/utils.rkt index 4fc85bd0..7b950233 100644 --- a/plot-lib/plot/utils.rkt +++ b/plot-lib/plot/utils.rkt @@ -80,6 +80,7 @@ ivl-translate bounds->intervals clamp-real + points-apply-jitters ;; Rectangles Rect rect-meet