-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathtinyplot.R
More file actions
1526 lines (1404 loc) · 57.9 KB
/
tinyplot.R
File metadata and controls
1526 lines (1404 loc) · 57.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#' @title Lightweight extension of the base R plotting function
#'
#' @description
#' Enhances the base \code{\link[graphics]{plot}} function. Supported features
#' include automatic legends and facets for grouped data, additional plot types,
#' theme customization, and so on. Users can call either `tinyplot()`, or its
#' shorthand alias `plt()`.
#'
#' @md
#' @param x,y the x and y arguments provide the x and y coordinates for the
#' plot. Any reasonable way of defining the coordinates is acceptable; most
#' likely the names of existing vectors or columns of data frames. See the
#' 'Examples' section below, or the function
#' \code{\link[grDevices]{xy.coords}} for details. If supplied separately, `x`
#' and `y` must be of the same length.
#' @param xmin,xmax,ymin,ymax minimum and maximum coordinates of relevant area
#' or interval plot types. Only used when the `type` argument is one of
#' `"rect"` or `"segments"` (where all four min-max coordinates are required),
#' or `"pointrange"`, `"errorbar"`, or `"ribbon"` (where only `ymin` and
#' `ymax` required alongside `x`). In the formula method the arguments
#' can be specified as `ymin = var` if `var` is a variable in `data`.
#' @param by grouping variable(s). The default behaviour is for groups to be
#' represented in the form of distinct colours, which will also trigger an
#' automatic legend. (See `legend` below for customization options.) However,
#' groups can also be presented through other plot parameters (e.g., `pch`,
#' `lty`, or `cex`) by passing an appropriate `"by"` keyword; see Examples.
#' Note that continuous (i.e., gradient) colour legends are also supported if
#' the user passes a numeric or integer to `by`. To group by multiple
#' variables, wrap them with \code{\link[base]{interaction}}.
#' @param facet the faceting variable(s) that you want arrange separate plot
#' windows by. Can be specified in various ways:
#' - In "atomic" form, e.g. `facet = fvar`. To facet by multiple variables in
#' atomic form, simply interact them, e.g.
#' `interaction(fvar1, fvar2)` or `factor(fvar1):factor(fvar2)`.
#' - As a one-sided formula, e.g. `facet = ~fvar`. Multiple variables can be
#' specified in the formula RHS, e.g. `~fvar1 + fvar2` or `~fvar1:fvar2`. Note
#' that these multi-variable cases are _all_ treated equivalently and
#' converted to `interaction(fvar1, fvar2, ...)` internally. (No distinction
#' is made between different types of binary operators, for example, and so
#' `f1+f2` is treated the same as `f1:f2`, is treated the same as `f1*f2`,
#' etc.)
#' - As a two-side formula, e.g. `facet = fvar1 ~ fvar2`. In this case, the
#' facet windows are arranged in a fixed grid layout, with the formula LHS
#' defining the facet rows and the RHS defining the facet columns. At present
#' only single variables on each side of the formula are well supported. (We
#' don't recommend trying to use multiple variables on either the LHS or RHS
#' of the two-sided formula case.)
#' - As a special `"by"` convenience keyword, in which case facets will match
#' the grouping variable(s) passed to `by` above.
#' @param facet.args an optional list of arguments for controlling faceting
#' behaviour. (Ignored if `facet` is NULL.) Supported arguments are as
#' follows:
#' - `nrow`, `ncol` for overriding the default "square" facet window
#' arrangement. Only one of these should be specified, but `nrow` will take
#' precedence if both are specified together. Ignored if a two-sided formula
#' is passed to the main `facet` argument, since the layout is arranged in a
#' fixed grid.
#' - `free` a logical value indicating whether the axis limits (scales) for
#' each individual facet should adjust independently to match the range of
#' the data within that facet. Default is `FALSE`. Separate free scaling of
#' the x- or y-axis (i.e., whilst holding the other axis fixed) is not
#' currently supported.
#' - `fmar` a vector of form `c(b,l,t,r)` for controlling the base margin
#' between facets in terms of lines. Defaults to the value of `tpar("fmar")`,
#' which should be `c(1,1,1,1)`, i.e. a single line of padding around each
#' individual facet, assuming it hasn't been overridden by the user as part
#' their global \code{\link[tinyplot]{tpar}} settings. Note some automatic
#' adjustments are made for certain layouts, and depending on whether the plot
#' is framed or not, to reduce excess whitespace. See
#' \code{\link[tinyplot]{tpar}} for more details.
#' - `cex`, `font`, `col`, `bg`, `border` for adjusting the facet title text
#' and background. Default values for these arguments are inherited from
#' \code{\link[tinyplot]{tpar}} (where they take a "facet." prefix, e.g.
#' `tpar("facet.cex")`). The latter function can also be used to set these
#' features globally for all `tinyplot` plots.
#' @param formula a \code{\link[stats]{formula}} that optionally includes
#' grouping variable(s) after a vertical bar, e.g. `y ~ x | z`. One-sided
#' formulae are also permitted, e.g. `~ y | z`. Only a single `y` and `x`
#' variable (if any) must be specified but multiple grouping variables
#' can be included in different ways, e.g. `y ~ x | z1:z2` or
#' `y ~ x | z1 + z2`. (These two representations are treated as equivalent;
#' both are parsed as `interaction(z1, z2)` internally.) If arithmetic
#' operators are used for transforming variables, they should be wrapped in
#' `I()`, e.g., `I(y1/y2) ~ x`. Note that the `formula` and `x` arguments
#' should not be specified in the same call.
#' @param data a data.frame (or list) from which the variables in formula
#' should be taken. A matrix is converted to a data frame.
#' @param type character string or call to a `type_*()` function giving the
#' type of plot desired.
#' - NULL (default): Choose a sensible type for the type of `x` and `y` inputs
#' (i.e., usually `"p"`).
#' - 1-character values supported by \code{\link[graphics]{plot}}:
#' - `"p"` Points
#' - `"l"` Lines
#' - `"b"` Both points and lines
#' - `"c"` Empty points joined by lines
#' - `"o"` Overplotted points and lines
#' - `"s"` Stair steps
#' - `"S"` Stair steps
#' - `"h"` Histogram-like vertical lines
#' - `"n"` Empty plot over the extent of the data
#' - `tinyplot`-specific types. These fall into several categories:
#' - Shapes:
#' - `"area"` / [`type_area()`]: Plots the area under the curve from `y` = 0 to `y` = f(`x`).
#' - `"errorbar"` / [`type_errorbar()`]: Adds error bars to points; requires `ymin` and `ymax`.
#' - `"pointrange"` / [`type_pointrange()`]: Combines points with error bars.
#' - `"polygon"` / [`type_polygon()`]: Draws polygons.
#' - `"polypath"` / [`type_polypath()`]: Draws a path whose vertices are given in `x` and `y`.
#' - `"rect"` / [`type_rect()`]: Draws rectangles; requires `xmin`, `xmax`, `ymin`, and `ymax`.
#' - `"ribbon"` / [`type_ribbon()`]: Creates a filled area between `ymin` and `ymax`.
#' - `"segments"` / [`type_segments()`]: Draws line segments between pairs of points.
#' - `"text"` / [`type_text()`]: Add text annotations.
#' - Visualizations:
#' - `"barplot"` / [`type_barplot()`]: Creates a bar plot.
#' - `"boxplot"` / [`type_boxplot()`]: Creates a box-and-whisker plot.
#' - `"density"` / [`type_density()`]: Plots the density estimate of a variable.
#' - `"histogram"` / [`type_histogram()`]: Creates a histogram of a single variable.
#' - `"jitter"` / [`type_jitter()`]: Jittered points.
#' - `"qq"` / [`type_qq()`]: Creates a quantile-quantile plot.
#' - `"ridge"` / [`type_ridge()`]: Creates a ridgeline (aka joy) plot.
#' - `"rug"` / [`type_rug()`]: Adds a rug to an existing plot.
#' - `"spineplot"` / [`type_spineplot()`]: Creates a spineplot or spinogram.
#' - `"violin"` / [`type_violin()`]: Creates a violin plot.
#' - Models:
#' - `"loess"` / [`type_loess()`]: Local regression curve.
#' - `"lm"` / [`type_lm()`]: Linear regression line.
#' - `"glm"` / [`type_glm()`]: Generalized linear model fit.
#' - `"spline"` / [`type_spline()`]: Cubic (or Hermite) spline interpolation.
#' - Functions:
#' - [`type_abline()`]: line(s) with intercept and slope.
#' - [`type_hline()`]: horizontal line(s).
#' - [`type_vline()`]: vertical line(s).
#' - [`type_function()`]: arbitrary function.
#' - [`type_summary()`]: summarize `y` by unique values of `x`.
#' @param legend one of the following options:
#' - NULL (default), in which case the legend will be determined by the
#' grouping variable. If there is no group variable (i.e., `by` is NULL) then
#' no legend is drawn. If a grouping variable is detected, then an automatic
#' legend is drawn to the _outer_ right of the plotting area. Note that the
#' legend title and categories will automatically be inferred from the `by`
#' argument and underlying data.
#' - A convenience string indicating the legend position. The string should
#' correspond to one of the position keywords supported by the base `legend`
#' function, e.g. "right", "topleft", "bottom", etc. In addition, `tinyplot`
#' supports adding a trailing exclamation point to these keywords, e.g.
#' "right!", "topleft!", or "bottom!". This will place the legend _outside_
#' the plotting area and adjust the margins of the plot accordingly. Finally,
#' users can also turn off any legend printing by specifying "none".
#' - Logical value, where TRUE corresponds to the default case above (same
#' effect as specifying NULL) and FALSE turns the legend off (same effect as
#' specifying "none").
#' - A list or, equivalently, a dedicated `legend()` function with supported
#' legend arguments, e.g. "bty", "horiz", and so forth.
#' @param main a main title for the plot, see also `title`.
#' @param sub a subtitle for the plot.
#' @param xlab a label for the x axis, defaults to a description of x.
#' @param ylab a label for the y axis, defaults to a description of y.
#' @param ann a logical value indicating whether the default annotation (title
#' and x and y axis labels) should appear on the plot.
#' @param xlim the x limits (x1, x2) of the plot. Note that x1 > x2 is allowed
#' and leads to a ‘reversed axis’. The default value, NULL, indicates that
#' the range of the `finite` values to be plotted should be used.
#' @param ylim the y limits of the plot.
#' @param axes logical or character. Should axes be drawn (`TRUE` or `FALSE`)?
#' Or alternatively what type of axes should be drawn: `"standard"` (with
#' axis, ticks, and labels; equivalent to `TRUE`), `"none"` (no axes;
#' equivalent to `FALSE`), `"ticks"` (only ticks and labels without axis line),
#' `"labels"` (only labels without ticks and axis line), `"axis"` (only axis
#' line and labels but no ticks). To control this separately for the two
#' axes, use the character specifications for `xaxt` and/or `yaxt`.
#' @param xaxt,yaxt character specifying the type of x-axis and y-axis,
#' respectively. See `axes` for the possible values.
#' @param xaxs,yaxs character specifying the style of the interval calculation
#' used for the x-axis and y-axis, respectively. See
#' \code{\link[graphics]{par}} for the possible values.
#' @param xaxb,yaxb numeric vector (or character vector, if appropriate) giving
#' the break points at which the axis tick-marks are to be drawn. Break points
#' outside the range of the data will be ignored if the associated axis
#' variable is categorical, or an explicit `x/ylim` range is given.
#' @param xaxl,yaxl a function or a character keyword specifying the format of
#' the x- or y-axis tick labels. Note that this is a post-processing step that
#' affects the _appearance_ of the tick labels only; use in conjunction with
#' `x/yaxb` if you would like to adjust the position of the tick marks too. In
#' addition to user-supplied formatting functions (e.g., [`format`],
#' [`toupper`], [`abs`], or other custom function), several convenience
#' keywords (or their symbol equivalents) are available for common formatting
#' transformations: `"percent"` (`"%"`), `"comma"` (`","`), `"log"` (`"l"`),
#' `"dollar"` (`"$"`), `"euro"` (`"€"`), or `"sterling"` (`"£"`). See the
#' [`tinylabel`] documentation for examples.
#' @param log a character string which contains `"x"` if the x axis is to be
#' logarithmic, `"y"` if the y axis is to be logarithmic and `"xy"` or `"yx"`
#' if both axes are to be logarithmic.
#' @param flip logical. Should the plot orientation be flipped, so that the
#' y-axis is on the horizontal plane and the x-axis is on the vertical plane?
#' Default is FALSE.
#' @param frame.plot a logical indicating whether a box should be drawn around
#' the plot. Can also use `frame` as an acceptable argument alias.
#' The default is to draw a frame if both axis types (set via `axes`, `xaxt`,
#' or `yaxt`) include axis lines.
#' @param grid argument for plotting a background panel grid, one of either:
#' - a logical (i.e., `TRUE` to draw the grid), or
#' - a panel grid plotting function like `grid()`.
#' Note that this argument replaces the `panel.first` and `panel.last`
#' arguments from base `plot()` and tries to make the process more seamless
#' with better default behaviour. The default behaviour is determined by (and
#' can be set globally through) the value of `tpar("grid")`.
#' @param palette one of the following options:
#' - NULL (default), in which case the palette will be chosen according to
#' the class and cardinality of the "by" grouping variable. For non-ordered
#' factors or strings with a reasonable number of groups, this will inherit
#' directly from the user's default \code{\link[grDevices]{palette}} (e.g.,
#' "R4"). In other cases, including ordered factors and high cardinality, the
#' "Viridis" palette will be used instead. Note that a slightly restricted
#' version of the "Viridis" palette---where extreme color values have been
#' trimmed to improve visual perception---will be used for ordered factors
#' and continuous variables. In the latter case of a continuous grouping
#' variable, we also generate a gradient legend swatch.
#' - A convenience string corresponding to one of the many palettes listed by
#' either `palette.pals()` or `hcl.pals()`. Note that the string can be
#' case-insensitive (e.g., "Okabe-Ito" and "okabe-ito" are both valid).
#' - A palette-generating function. This can be "bare" (e.g.,
#' `palette.colors`) or "closed" with a set of named arguments (e.g.,
#' `palette.colors(palette = "Okabe-Ito", alpha = 0.5)`). Note that any
#' unnamed arguments will be ignored and the key `n` argument, denoting the
#' number of colours, will automatically be spliced in as the number of
#' groups.
#' - A vector or list of colours, e.g. `c("darkorange", "purple", "cyan4")`.
#' If too few colours are provided for a discrete (qualitative) set of
#' groups, then the colours will be recycled with a warning. For continuous
#' (sequential) groups, a gradient palette will be interpolated.
#' @param col plotting color. Character, integer, or vector of length equal to
#' the number of categories in the `by` variable. See `col`. Note that the
#' default behaviour in `tinyplot` is to vary group colors along any variables
#' declared in the `by` argument. Thus, specifying colors manually should not
#' be necessary unless users wish to override the automatic colors produced by
#' this grouping process. Typically, this would only be done if grouping
#' features are deferred to some other graphical parameter (i.e., passing the
#' "by" keyword to one of `pch`, `lty`, `lwd`, or `bg`; see below.)
#' @param pch plotting "character", i.e., symbol to use. Character, integer, or
#' vector of length equal to the number of categories in the `by` variable.
#' See `pch`. In addition, users can supply a special `pch = "by"` convenience
#' argument, in which case the characters will automatically loop over the
#' number groups. This automatic looping will begin at the global character
#' value (i.e., `par("pch")`) and recycle as necessary.
#' @param lty line type. Character, integer, or vector of length equal to the
#' number of categories in the `by` variable. See `lty`. In addition, users
#' can supply a special `lty = "by"` convenience argument, in which case the
#' line type will automatically loop over the number groups. This automatic
#' looping will begin at the global line type value (i.e., `par("lty")`) and
#' recycle as necessary.
#' @param lwd line width. Numeric scalar or vector of length equal to the
#' number of categories in the `by` variable. See `lwd`. In addition, users
#' can supply a special `lwd = "by"` convenience argument, in which case the
#' line width will automatically loop over the number of groups. This
#' automatic looping will be centered at the global line width value (i.e.,
#' `par("lwd")`) and pad on either side of that.
#' @param bg background fill color for the open plot symbols 21:25 (see
#' `points.default`), as well as ribbon and area plot types.
#' Users can also supply either one of two special convenience arguments that
#' will cause the background fill to inherit the automatic grouped coloring
#' behaviour of `col`:
#'
#' - `bg = "by"` will insert a background fill that inherits the main color
#' mappings from `col`.
#' - `by = <numeric[0,1]>` (i.e., a numeric in the range `[0,1]`) will insert
#' a background fill that inherits the main color mapping(s) from `col`, but
#' with added alpha-transparency.
#'
#' For both of these convenience arguments, note that the (grouped) `bg`
#' mappings will persist even if the (grouped) `col` defaults are themselves
#' overridden. This can be useful if you want to preserve the grouped palette
#' mappings by background fill but not boundary color, e.g. filled points. See
#' examples.
#' @param fill alias for `bg`. If non-NULL values for both `bg` and `fill` are
#' provided, then the latter will be ignored in favour of the former.
#' @param alpha a numeric in the range `[0,1]` for adjusting the alpha channel
#' of the color palette, where 0 means transparent and 1 means opaque. Use
#' fractional values, e.g. `0.5` for semi-transparency.
#' @param cex character expansion. A numerical vector (can be a single value)
#' giving the amount by which plotting characters and symbols should be scaled
#' relative to the default. Note that `NULL` is equivalent to 1.0, while `NA`
#' renders the characters invisible. There are two additional considerations,
#' specifically for points-alike plot types (e.g. `"p"`):
#'
#' - users can also supply a special `cex = "by"` convenience argument, in
#' which case the character expansion will automatically adjust by group
#' too. The range of this character expansion is controlled by the `clim`
#' argument in the respective types; see [`type_points()`] for example.
#' - passing a `cex` vector of equal length to the main `x` and `y` variables
#' (e.g., another column in the same dataset) will yield a "bubble"plot with
#' its own dedicated legend. This can provide a useful way to visualize an
#' extra dimension of the data; see Examples.
#' @param subset,na.action,drop.unused.levels arguments passed to `model.frame`
#' when extracting the data from `formula` and `data`.
#' @param add logical. If TRUE, then elements are added to the current plot rather
#' than drawing a new plot window. Note that the automatic legend for the
#' added elements will be turned off. See also [tinyplot_add], which provides
#' a convenient wrapper around this functionality for layering on top of an
#' existing plot without having to repeat arguments.
#' @param draw a function that draws directly on the plot canvas (before `x` and
#' `y` are plotted). The `draw` argument is primarily useful for adding common
#' elements to each facet of a faceted plot, e.g.
#' \code{\link[graphics]{abline}} or \code{\link[graphics]{text}}. Note that
#' this argument is somewhat experimental and that _no_ internal checking is
#' done for correctness; the provided argument is simply captured and
#' evaluated as-is within `tinyplot()` and thus has access to the local
#' definition of all variables such as `x`, `y`, etc. See Examples.
#' @param restore.par a logical value indicating whether the
#' \code{\link[graphics]{par}} settings prior to calling `tinyplot` should be
#' restored on exit. Defaults to FALSE, which makes it possible to add
#' elements to the plot after it has been drawn. However, note the the outer
#' margins of the graphics device may have been altered to make space for the
#' `tinyplot` legend. Users can opt out of this persistent behaviour by
#' setting to TRUE instead. See also [get_saved_par] for another option to
#' recover the original \code{\link[graphics]{par}} settings, as well as
#' longer discussion about the trade-offs involved.
#' @param empty logical indicating whether the interior plot region should be
#' left empty. The default is `FALSE`. Setting to `TRUE` has a similar effect
#' to invoking `type = "n"` above, except that any legend artifacts owing to a
#' particular plot type (e.g., lines for `type = "l"` or squares for
#' `type = "area"`) will still be drawn correctly alongside the empty plot. In
#' contrast,`type = "n"` implicitly assumes a scatterplot and so any legend
#' will only depict points.
#' @param file character string giving the file path for writing a plot to disk.
#' If specified, the plot will not be displayed interactively, but rather sent
#' to the appropriate external graphics device (i.e.,
#' \code{\link[grDevices]{png}}, \code{\link[grDevices]{jpeg}},
#' \code{\link[grDevices]{pdf}}, or \code{\link[grDevices]{svg}}). As a point
#' of convenience, note that any global parameters held in `(t)par` are
#' automatically carried over to the external device and don't need to be
#' reset (in contrast to the conventional base R approach that requires
#' manually opening and closing the device). The device type is determined by
#' the file extension at the end of the provided path, and must be one of
#' ".png", ".jpg" (".jpeg"), ".pdf", or ".svg". (Other file types may be
#' supported in the future.) The file dimensions can be controlled by the
#' corresponding `width` and `height` arguments below, otherwise will fall
#' back to the `"file.width"` and `"file.height"` values held in
#' \code{\link[tinyplot]{tpar}} (i.e., both defaulting to 7 inches, and where
#' the default resolution for bitmap files is also specified as 300
#' DPI).
#' @param width numeric giving the plot width in inches. Together with `height`,
#' typically used in conjunction with the `file` argument above, overriding the
#' default values held in `tpar("file.width", "file.height")`. If either `width`
#' or `height` is specified, but a corresponding `file` argument is not
#' provided as well, then a new interactive graphics device dimensions will be
#' opened along the given dimensions. Note that this interactive resizing may
#' not work consistently from within an IDE like RStudio that has an integrated
#' graphics windows.
#' @param height numeric giving the plot height in inches. Same considerations as
#' `width` (above) apply, e.g. will default to `tpar("file.height")` if not
#' specified.
#' @param asp the y/xy/x aspect ratio, see `plot.window`.
#' @param theme keyword string (e.g. `"clean"`) or list defining a theme. Passed
#' on to [`tinytheme`], but reset upon exit so that the theme effect is only
#' temporary. Useful for invoking ephemeral themes.
#' @param ... other graphical parameters. If `type` is a character specification
#' (such as `"hist"`) then any argument names that match those from the corresponding
#' `type_*()` function (such as \code{\link{type_hist}}) are passed on to that.
#' All remaining arguments from `...` can be further graphical parameters, see
#' \code{\link[graphics]{par}}).
#'
#' @returns No return value, called for side effect of producing a plot.
#'
#' @details
#' Disregarding the enhancements that it supports, `tinyplot` tries as far as
#' possible to mimic the behaviour and syntax logic of the original base
#' \code{\link[graphics]{plot}} function. Users should therefore be able to swap
#' out existing `plot` calls for `tinyplot` (or its shorthand alias `plt`),
#' without causing unexpected changes to the output.
#'
#' @importFrom grDevices axisTicks adjustcolor cairo_pdf colorRampPalette dev.cur dev.list dev.off dev.new extendrange hcl.colors hcl.pals jpeg palette palette.colors palette.pals pdf png svg xy.coords
#' @importFrom graphics abline arrows axis Axis axTicks box boxplot grconvertX grconvertY hist lines mtext par plot.default plot.new plot.window points polygon polypath segments rect text title
#' @importFrom utils modifyList head tail
#' @importFrom stats na.omit setNames
#' @importFrom tools file_ext
#'
#' @examples
#' aq = transform(
#' airquality,
#' Month = factor(Month, labels = month.abb[unique(Month)])
#' )
#'
#' # In most cases, `tinyplot` should be a drop-in replacement for regular
#' # `plot` calls. For example:
#'
#' op = tpar(mfrow = c(1, 2))
#' plot(0:10, main = "plot")
#' tinyplot(0:10, main = "tinyplot")
#' tpar(op) # restore original layout
#'
#' # Aside: `tinyplot::tpar()` is a (near) drop-in replacement for `par()`
#'
#' # Unlike vanilla plot, however, tinyplot allows you to characterize groups
#' # using either the `by` argument or equivalent `|` formula syntax.
#'
#' with(aq, tinyplot(Day, Temp, by = Month)) ## atomic method
#' tinyplot(Temp ~ Day | Month, data = aq) ## formula method
#'
#' # (Notice that we also get an automatic legend.)
#'
#' # You can also use the equivalent shorthand `plt()` alias if you'd like to
#' # save on a few keystrokes
#'
#' plt(Temp ~ Day | Month, data = aq) ## shorthand alias
#'
#' # Use standard base plotting arguments to adjust features of your plot.
#' # For example, change `pch` (plot character) to get filled points and `cex`
#' # (character expansion) to increase their size.
#'
#' tinyplot(
#' Temp ~ Day | Month,
#' data = aq,
#' pch = 16,
#' cex = 2
#' )
#'
#' # Use the special "by" convenience keyword if you would like to map these
#' # aesthetic features over groups too (i.e., in addition to the default
#' # colour grouping)
#'
#' tinyplot(
#' Temp ~ Day | Month,
#' data = aq,
#' pch = "by",
#' cex = "by"
#' )
#'
#' # We can add alpha transparency for overlapping points
#'
#' tinyplot(
#' Temp ~ Day | Month,
#' data = aq,
#' pch = 16,
#' cex = 2,
#' alpha = 0.3
#' )
#'
#' # To get filled points with a common solid background color, use an
#' # appropriate plotting character (21:25) and combine with one of the special
#' # `bg`/`fill` convenience arguments.
#' tinyplot(
#' Temp ~ Day | Month,
#' data = aq,
#' pch = 21, # use filled circles
#' cex = 2,
#' bg = 0.3, # numeric in [0,1] adds a grouped background fill with transparency
#' col = "black" # override default color mapping; give all points a black border
#' )
#'
#' # Aside: For "bubble" plots, pass an appropriate vector to the `cex` arg.
#' # This can be useful for depicting an additional dimension of the data (here:
#' # Wind).
#' tinyplot(
#' Temp ~ Day | Month,
#' data = aq,
#' pch = 21,
#' cex = aq$Wind, # map character size to another feature in the data
#' bg = 0.3,
#' col = "black"
#' )
#'
#' # Converting to a grouped line plot is a simple matter of adjusting the
#' # `type` argument.
#'
#' tinyplot(
#' Temp ~ Day | Month,
#' data = aq,
#' type = "l"
#' )
#'
#' # Similarly for other plot types, including some additional ones provided
#' # directly by tinyplot, e.g. density plots or internal plots (ribbons,
#' # pointranges, etc.)
#'
#' tinyplot(
#' ~ Temp | Month,
#' data = aq,
#' type = "density",
#' fill = "by"
#' )
#'
#' # Facet plots are supported too. Facets can be drawn on their own...
#'
#' tinyplot(
#' Temp ~ Day,
#' facet = ~Month,
#' data = aq,
#' type = "area",
#' main = "Temperatures by month"
#' )
#'
#' # ... or combined/contrasted with the by (colour) grouping.
#'
#' aq = transform(aq, Summer = Month %in% c("Jun", "Jul", "Aug"))
#' tinyplot(
#' Temp ~ Day | Summer,
#' facet = ~Month,
#' data = aq,
#' type = "area",
#' palette = "dark2",
#' main = "Temperatures by month and season"
#' )
#'
#' # Users can override the default square window arrangement by passing `nrow`
#' # or `ncol` to the helper facet.args argument. Note that we can also reduce
#' # axis label repetition across facets by turning the plot frame off.
#'
#' tinyplot(
#' Temp ~ Day | Summer,
#' facet = ~Month, facet.args = list(nrow = 1),
#' data = aq,
#' type = "area",
#' palette = "dark2",
#' frame = FALSE,
#' main = "Temperatures by month and season"
#' )
#'
#' # Use a two-sided formula to arrange the facet windows in a fixed grid.
#' # LHS -> facet rows; RHS -> facet columns
#'
#' aq$hot = ifelse(aq$Temp >= 75, "hot", "cold")
#' aq$windy = ifelse(aq$Wind >= 15, "windy", "calm")
#' tinyplot(
#' Temp ~ Day,
#' facet = windy ~ hot,
#' data = aq
#' )
#'
#' # To add common elements to each facet, use the `draw` argument
#'
#' tinyplot(
#' Temp ~ Day,
#' facet = windy ~ hot,
#' data = aq,
#' draw = abline(h = 75, lty = 2, col = "hotpink")
#' )
#'
#' # The (automatic) legend position and look can be customized using
#' # appropriate arguments. Note the trailing "!" in the `legend` position
#' # argument below. This tells `tinyplot` to place the legend _outside_ the plot
#' # area.
#'
#' tinyplot(
#' Temp ~ Day | Month,
#' data = aq,
#' type = "l",
#' legend = legend("bottom!", title = "Month of the year", bty = "o")
#' )
#'
#' # The default group colours are inherited from either the "R4" or "Viridis"
#' # palettes, depending on the number of groups. However, all palettes listed
#' # by `palette.pals()` and `hcl.pals()` are supported as convenience strings,
#' # or users can supply a valid palette-generating function for finer control
#'
#' tinyplot(
#' Temp ~ Day | Month,
#' data = aq,
#' type = "l",
#' palette = "tableau"
#' )
#'
#' # It's possible to customize the look of your plots by setting graphical
#' # parameters (e.g., via `(t)par`)... But a more convenient way is to just use
#' # built-in themes (see `?tinytheme`).
#'
#' tinytheme("clean2")
#' tinyplot(
#' Temp ~ Day | Month,
#' data = aq,
#' type = "b",
#' alpha = 0.5,
#' main = "Daily temperatures by month",
#' sub = "Brought to you by tinyplot"
#' )
#' # reset the theme
#' tinytheme()
#'
#' # For more examples and a detailed walkthrough, please see the introductory
#' # tinyplot tutorial available online:
#' # https://grantmcdermott.com/tinyplot/vignettes/introduction.html
#'
#' @rdname tinyplot
#' @export
tinyplot =
function(x, ...) {
UseMethod("tinyplot")
}
#' @rdname tinyplot
#' @export
tinyplot.default = function(
x = NULL,
y = NULL,
xmin = NULL,
xmax = NULL,
ymin = NULL,
ymax = NULL,
by = NULL,
facet = NULL,
facet.args = NULL,
data = NULL,
type = NULL,
legend = NULL,
main = NULL,
sub = NULL,
xlab = NULL,
ylab = NULL,
ann = par("ann"),
xlim = NULL,
ylim = NULL,
axes = TRUE,
xaxt = NULL,
yaxt = NULL,
xaxs = NULL,
yaxs = NULL,
xaxb = NULL,
yaxb = NULL,
xaxl = NULL,
yaxl = NULL,
log = "",
flip = FALSE,
frame.plot = NULL,
grid = NULL,
palette = NULL,
pch = NULL,
lty = NULL,
lwd = NULL,
col = NULL,
bg = NULL,
fill = NULL,
alpha = NULL,
cex = NULL,
add = FALSE,
draw = NULL,
empty = FALSE,
restore.par = FALSE,
file = NULL,
width = NULL,
height = NULL,
asp = NA,
theme = NULL,
...) {
# Force evaluation of legend if it's a symbol to avoid downstream promise
# issues. Let sanitize_legend handle it
if (!missing(legend) && is.symbol(substitute(legend))) {
legend = legend
}
#
## save parameters and calls -----
#
par_first = get_saved_par("first")
if (is.null(par_first)) set_saved_par("first", par())
# save for tinyplot_add()
assert_logical(add)
if (!add) {
calls = sys.calls()
is_tinyplot_call = function(x) identical(tinyplot, try(eval(x[[1L]]), silent = TRUE))
idx = which(vapply(calls, is_tinyplot_call, FALSE))
if (length(idx) > 0) {
set_environment_variable(.last_call = calls[[idx[1L]]])
}
}
# Save current graphical parameters
opar = par(no.readonly = TRUE)
if (restore.par || !is.null(facet)) {
if (!is.null(file) || !is.null(width) || !is.null(height)) {
opar$new = FALSE # catch for some interfaces
}
on.exit(par(opar), add = TRUE)
}
set_saved_par(when = "before", opar)
# Catch for adding to existing facet plot
if (!is.null(facet) && add) {
recordGraphics(
par(get_saved_par(when = "after")),
list = list(),
env = getNamespace('tinyplot')
)
}
# Ephemeral theme
if (!is.null(theme)) {
if (is.character(theme) && length(theme) == 1) {
tinytheme(theme)
} else if (is.list(theme)) {
do.call(tinytheme, theme)
} else {
warning('Argument `theme` must be a character of length 1 (e.g. "clean"), or a list. Ignoring.')
}
if (is.character(theme) && theme == "default") {
# Reset mar to pre-theme value so legend margin adjustment isn't
# clobbered. Only needed for "default" theme which uses hook = FALSE
# and thus sets par(mar) immediately. (#557)
par(mar = opar$mar)
on.exit(init_tpar(rm_hook = TRUE), add = TRUE)
} else {
dtheme = theme_default
otheme = opar[names(dtheme)]
on.exit(do.call(tinytheme, otheme), add = TRUE)
}
}
#
## settings container -----
#
dots = list(...)
settings_list = list(
# save call to check user input later
call = match.call(),
# save to file & device dimensions
file = file,
width = width,
height = height,
# deparsed input for use in labels
by_dep = deparse1(substitute(by)),
cex_dep = if (!is.null(cex)) deparse1(substitute(cex)) else NULL,
facet_dep = deparse1(substitute(facet)),
x_dep = if (is.null(x)) NULL else deparse1(substitute(x)),
xmax_dep = if (is.null(xmax)) NULL else deparse1(substitute(xmax)),
xmin_dep = if (is.null(xmin)) NULL else deparse1(substitute(xmin)),
y_dep = if (is.null(y)) NULL else deparse1(substitute(y)),
ymax_dep = if (is.null(ymax)) NULL else deparse1(substitute(ymax)),
ymin_dep = if (is.null(ymin)) NULL else deparse1(substitute(ymin)),
# types
type = type,
type_data = NULL,
type_draw = NULL,
type_name = NULL,
# type-specific settings
bubble = FALSE,
bubble_pch = NULL,
bubble_alpha = NULL,
bubble_bg_alpha = NULL,
ygroup = NULL, # for type_ridge()
# data points and labels
x = x,
xmax = xmax,
xmin = xmin,
xlab = xlab,
xlabs = NULL,
y = y,
ymax = ymax,
ymin = ymin,
ylab = ylab,
ylabs = NULL,
# axes
axes = axes,
xaxt = xaxt,
xaxb = xaxb,
xaxl = xaxl,
xaxs = xaxs,
yaxt = yaxt,
yaxb = yaxb,
yaxl = yaxl,
yaxs = yaxs,
frame.plot = frame.plot,
xlim = xlim,
ylim = ylim,
# flags to check user input (useful later on)
null_by = is.null(by),
null_xlim = is.null(xlim),
null_ylim = is.null(ylim),
# when palette functions need pre-processing this check raises error
null_palette = tryCatch(is.null(palette), error = function(e) FALSE),
x_by = identical(x, by), # for "boxplot", "spineplot" and "ridge"
# unevaluated expressions with side effects
draw = substitute(draw),
facet = facet,
facet.args = facet.args,
palette = substitute(palette),
legend = if (add) FALSE else substitute(legend),
# aesthetics
lty = lty,
lwd = lwd,
col = col,
bg = bg,
log = log,
fill = fill,
alpha = alpha,
cex = cex,
pch = if (is.null(pch)) get_tpar("pch", default = NULL) else pch,
# ribbon.alpha overwritten by some type_data() functions
# sanitize_ribbon_alpha: returns default alpha transparency for ribbon-type plots
ribbon.alpha = sanitize_ribbon_alpha(NULL),
# misc
add = add,
by = by,
dodge = NULL,
dots = dots,
flip = flip,
group_offsets = NULL,
offsets_axis = NULL,
type_info = list() # pass type-specific info from type_data to type_draw
)
settings = new.env()
list2env(settings_list, settings)
#
## devices and files -----
#
# Write plot to output file or window with fixed dimensions
setup_device(settings)
if (!is.null(settings$file)) on.exit(dev.off(), add = TRUE)
#
## sanitize arguments -----
#
# extract legend_args from dots
if ("legend_args" %in% names(dots)) {
settings$legend_args = settings$dots[["legend_args"]]
settings$dots$legend_args = NULL # avoid passing both directly and via ...
} else {
settings$legend_args = list(x = NULL)
}
# alias: bg = fill
if (is.null(bg) && !is.null(fill)) settings$bg = fill
# validate types and returns a list with name, data, and draw components
sanitize_type(settings)
# standardize axis arguments and returns consistent axes, xaxt, yaxt, frame.plot
sanitize_axes(settings)
# generate appropriate axis labels based on input data and plot type
sanitize_xylab(settings)
# palette default
if (is.null(settings$palette)) {
settings$palette = get_tpar("palette", default = NULL)
}
# by: coerce character groups to factor
if (!settings$null_by && is.character(settings$by)) {
settings$by = factor(settings$by)
}
# flag if x==by, currently only used for
#
# facet: parse facet formula and prepares variables when facet==by
sanitize_facet(settings)
# x / y processing
# convert characters to factors
# set x automatically when omitted (ex: rect)
# set y automatically when omitted (ex: boxplots)
# combine x, y, xmax, by, facet etc. into a single `datapoints` data.frame
sanitize_datapoints(settings)
#
## transform datapoints using type_data() -----
#
if (!is.null(settings$type_data)) {
settings$type_data(settings, ...)
}
# ensure axis aligment of any added layers
if (!add) {
assign("xlabs_orig", settings[["xlabs"]], envir = get(".tinyplot_env", envir = parent.env(environment())))
assign(".group_offsets", settings[["group_offsets"]], envir = get(".tinyplot_env", envir = parent.env(environment())))
assign(".offsets_axis", settings[["offsets_axis"]], envir = get(".tinyplot_env", envir = parent.env(environment())))
} else {
align_layer(settings)
}
# flip -> swap x and y after type_data, except for boxplots (which has its own bespoke flip logic)
flip_datapoints(settings)
#
## bubble plot -----
#
# Transform cex values for bubble charts. Handles size transformation, legend
# gotchas, and aesthetic sanitization.
# Currently limited to "p" and "text" types, but could expand to others.
bubble(settings)
#
## axis breaks and limits -----
#
# do this after computing yaxb because limits will depend on the previous calculations
if (!add) {
lim_args(settings)
}
#
## facets: count -----
#
# facet_layout processes facet simplification, attribute restoration, and layout
facet_layout(settings)
#
## aesthetics by group -----
#
by_aesthetics(settings)
#
## legends -----
#
prepare_legend(settings)
#
## make settings available in the environment directly -----
#
env2env(settings, environment())
if (legend_draw_flag) {
if (!multi_legend) {
## simple case: single legend only
if (is.null(lgnd_cex)) lgnd_cex = cex * cex_fct_adj
draw_legend(
legend = legend,
legend_args = legend_args,
by_dep = by_dep,
lgnd_labs = lgnd_labs,
type = type,
pch = pch,
lty = lty,
lwd = lwd,
col = col,
bg = bg,
gradient = by_continuous,
cex = lgnd_cex,
has_sub = has_sub
)
} else {
## multi-legend case...
prepare_legend_multi(settings)
env2env(settings, environment(), c("legend_args", "lgby", "lgbub"))
# draw multi-legend
draw_multi_legend(list(lgby, lgbub), position = legend_args[["x"]])
}
has_legend = TRUE
} else if (legend_args[["x"]] == "none" && !isTRUE(add)) {
omar = par("mar")
ooma = par("oma")
topmar_epsilon = 0.1
# Catch to avoid recursive offsets, e.g. repeated tinyplot calls with
# "bottom!" legend position.
restore_margin_inner(ooma)
# clean up for now
rm(omar, ooma, topmar_epsilon)
# Draw new plot
plot.new()
}
#
## title and subtitle -----
#
if (!add) {
draw_title(main, sub, xlab, ylab, legend, legend_args, opar)
}
#
## facets: draw -----
#
# Two-phase plotting logic: First determine and draw all exterior elements
# (facet windows, axes, grid, etc.), then circle back to each facet and