What do people think of sharing ggplot2 objects as .rds files?

ggplot2

#1

Here's a bit of a philosophical hump day (in the eastern hemisphere!) thread. I'm taking on some projects next year that'll operationalise scientific plot delivery for people, probably using something Shiny or plumber.

There're plenty of web services like this that deliver plots in any of the typical rasterised or vector formats—PNG, PDF, SVG, whatever. But one option I've never seen, for those services that use ggplot2 to build the plots, is to deliver an unbuilt ggplot2 object as a serialised R data (RDS) file.

My thinking here is that it would be useful for professional users who might otherwise download a vector file and then finish it in something like Illustrator to instead download the RDS file, add plot elements or a company theme, and then render it themselves. The unbuilt ggplot2 object has the original data inside it, so (subject to data size constraints) it ought to be fairly portable.

Is there a reason this isn't done more often? Is this just overengineering a problem, in that most users of these plots are more likely to be familiar with graphic design software than analysis software? Any vague thoughts about this?


#2

The reproducibility/extensibility goal is a noble one that I applaud (insert emoticon).

The save as Rds approach doesn't get you more than the original ggplot object, however. Here's an example from the help page

> a_plot <- ggplot(linetypes, aes(0, y)) + 
+   geom_segment(aes(xend = 5, yend = y, linetype = lty)) + 
+   scale_linetype_identity() + 
+   geom_text(aes(label = lty), hjust = 0, nudge_y = 0.2) +
+   scale_x_continuous(NULL, breaks = NULL) + 
+   scale_y_reverse(NULL, breaks = NULL)

Where you serialize it, you get back

> str(a_plot)
List of 9
 $ data       :'data.frame':	6 obs. of  2 variables:
  ..$ y  : int [1:6] 1 2 3 4 5 6
  ..$ lty: Factor w/ 6 levels "dashed","dotdash",..: 5 1 3 2 4 6
 $ layers     :List of 2
  ..$ :Classes 'LayerInstance', 'Layer', 'ggproto', 'gg' <ggproto object: Class LayerInstance, Layer, gg>
    aes_params: list
    compute_aesthetics: function
    compute_geom_1: function
    compute_geom_2: function
    compute_position: function
    compute_statistic: function
    data: waiver
    draw_geom: function
    finish_statistics: function
    geom: <ggproto object: Class GeomSegment, Geom, gg>
        aesthetics: function
        default_aes: uneval
        draw_group: function
        draw_key: function
        draw_layer: function
        draw_panel: function
        extra_params: na.rm
        handle_na: function
        non_missing_aes: linetype size shape
        optional_aes: 
        parameters: function
        required_aes: x y xend yend
        setup_data: function
        use_defaults: function
        super:  <ggproto object: Class Geom, gg>
    geom_params: list
    inherit.aes: TRUE
    layer_data: function
    map_statistic: function
    mapping: uneval
    position: <ggproto object: Class PositionIdentity, Position, gg>
        compute_layer: function
        compute_panel: function
        required_aes: 
        setup_data: function
        setup_params: function
        super:  <ggproto object: Class Position, gg>
    print: function
    show.legend: NA
    stat: <ggproto object: Class StatIdentity, Stat, gg>
        aesthetics: function
        compute_group: function
        compute_layer: function
        compute_panel: function
        default_aes: uneval
        extra_params: na.rm
        finish_layer: function
        non_missing_aes: 
        parameters: function
        required_aes: 
        retransform: TRUE
        setup_data: function
        setup_params: function
        super:  <ggproto object: Class Stat, gg>
    stat_params: list
    super:  <ggproto object: Class Layer, gg> 
  ..$ :Classes 'LayerInstance', 'Layer', 'ggproto', 'gg' <ggproto object: Class LayerInstance, Layer, gg>
    aes_params: list
    compute_aesthetics: function
    compute_geom_1: function
    compute_geom_2: function
    compute_position: function
    compute_statistic: function
    data: waiver
    draw_geom: function
    finish_statistics: function
    geom: <ggproto object: Class GeomText, Geom, gg>
        aesthetics: function
        default_aes: uneval
        draw_group: function
        draw_key: function
        draw_layer: function
        draw_panel: function
        extra_params: na.rm
        handle_na: function
        non_missing_aes: 
        optional_aes: 
        parameters: function
        required_aes: x y label
        setup_data: function
        use_defaults: function
        super:  <ggproto object: Class Geom, gg>
    geom_params: list
    inherit.aes: TRUE
    layer_data: function
    map_statistic: function
    mapping: uneval
    position: <ggproto object: Class PositionNudge, Position, gg>
        compute_layer: function
        compute_panel: function
        required_aes: 
        setup_data: function
        setup_params: function
        x: 0
        y: 0.2
        super:  <ggproto object: Class PositionNudge, Position, gg>
    print: function
    show.legend: NA
    stat: <ggproto object: Class StatIdentity, Stat, gg>
        aesthetics: function
        compute_group: function
        compute_layer: function
        compute_panel: function
        default_aes: uneval
        extra_params: na.rm
        finish_layer: function
        non_missing_aes: 
        parameters: function
        required_aes: 
        retransform: TRUE
        setup_data: function
        setup_params: function
        super:  <ggproto object: Class Stat, gg>
    stat_params: list
    super:  <ggproto object: Class Layer, gg> 
 $ scales     :Classes 'ScalesList', 'ggproto', 'gg' <ggproto object: Class ScalesList, gg>
    add: function
    clone: function
    find: function
    get_scales: function
    has_scale: function
    input: function
    n: function
    non_position_scales: function
    scales: list
    super:  <ggproto object: Class ScalesList, gg> 
 $ mapping    :List of 2
  ..$ x: num 0
  ..$ y: language ~y
  .. ..- attr(*, ".Environment")=<environment: R_GlobalEnv> 
  ..- attr(*, "class")= chr "uneval"
 $ theme      : list()
 $ coordinates:Classes 'CoordCartesian', 'Coord', 'ggproto', 'gg' <ggproto object: Class CoordCartesian, Coord, gg>
    aspect: function
    backtransform_range: function
    clip: on
    default: TRUE
    distance: function
    expand: TRUE
    is_free: function
    is_linear: function
    labels: function
    limits: list
    modify_scales: function
    range: function
    render_axis_h: function
    render_axis_v: function
    render_bg: function
    render_fg: function
    setup_data: function
    setup_layout: function
    setup_panel_params: function
    setup_params: function
    transform: function
    super:  <ggproto object: Class CoordCartesian, Coord, gg> 
 $ facet      :Classes 'FacetNull', 'Facet', 'ggproto', 'gg' <ggproto object: Class FacetNull, Facet, gg>
    compute_layout: function
    draw_back: function
    draw_front: function
    draw_labels: function
    draw_panels: function
    finish_data: function
    init_scales: function
    map_data: function
    params: list
    setup_data: function
    setup_params: function
    shrink: TRUE
    train_scales: function
    vars: function
    super:  <ggproto object: Class FacetNull, Facet, gg> 
 $ plot_env   :<environment: R_GlobalEnv> 
 $ labels     :List of 6
  ..$ x       : chr "x"
  ..$ y       : chr "y"
  ..$ xend    : chr "xend"
  ..$ yend    : chr "y"
  ..$ linetype: chr "lty"
  ..$ label   : chr "lty"
 - attr(*, "class")= chr [1:2] "gg" "ggplot"

With which, of course, you can tinker, but can't fully re-factor to your own data and presentation preferences.

Better, I think, is simply to create a package, combining the source data, the ggplot chain, and perhaps the type specimen output, and the user can substitute, inspect and modify to her heart's content.

Plus (here follows personal peeve), the package can include all necessary metadata.