3

I am trying to create some functions that will avoid me to repeat plot options for different types of plot. I encounter some troubles as I try to create mechanisms to automatically handle Frameticks & Plot Range given the data considered within a given plot.

Module[{chartData},
        chartData = RandomInteger[20, 20];

BarChart[chartData,
                Frame -> {{True, True}, {True, True}},
                ImageSize -> 300,
                ChartStyle -> Black,
                FrameLabel -> {{"yName", None}, {None, "xName"}},
                ChartElementFunction -> "FadingRectangle",
                LabelStyle -> Directive[Black, Bold, 18],
                PlotRange -> {Automatic, 1.3*Max@chartData},
                FrameTicks -> {{{Round@Min@chartData, Round@(Max@chartData/2), 
                                Round@Max@chartData}, None}, 
                              {{0, Round@(Length@chartData/2), Length@chartData}, None}}
          ]]

enter image description here

Now Here is my attempt to simplify my life :

chartOptions[yName_, xName_] := {Frame -> {{True, True}, {True, True}},      
                                ImageSize -> 300,
                                ChartStyle -> Blue,
                                FrameLabel -> {{yName, None}, {None, xName}},
                                ChartElementFunction -> "FadingRectangle",
                                LabelStyle -> Directive[Black, Bold, 18],
                                FrameTicks -> {{{Round@Min@chartData, Round@(Max@chartData/2), 
                                                 Round@Max@chartData}, None}, 
                                              {{0,Round@(Length@chartData/2),     
                                               Length@chartData}, None}},
                                PlotRange -> {Automatic, 1.3*Max@chartData}}

This so that hopefully my actual Chart code is as such :

 Module[{chartData},
         chartData = RandomInteger[20, 20];

         BarChart[chartData,
         chartOptions["yName", "xName"]]]

But this does not work :

enter image description here

The idea is that the chartOptions function will be adjusted given the chart data of the actual plot where it is used.

I am now trying to only use things I trully understand so I hope such a problem does not require to much of your sophisticated skills :-) !

500
  • 6,509
  • 8
  • 46
  • 80

3 Answers3

14

Not a direct answer to the question (which has been answered already by @WReach), but I think that @500 touched on an important topic. We may often want to create something like a configuration containing certain custom option settings for several options. Changing options globally is IMO more often than not a bad idea (this is not to detract from the answer of @Verbeia. There are certainly cases where global options resetting is appropriate. My suggestions should be viewed as complementary). Here I will reproduce an implementation of a very light-weight option-configuration manager to create persistent option configurations, that I wrote when answering a similar question on Mathgroup, and a relevant discussion.


Usage

Let me start with the usage - the implementation for the option-configuration manager can be found at the bottom of my post. We will start with a test function with options:

In[54]:= 
ClearAll[bar];
Options[bar] = {barA -> 1, barB -> 2};
bar[x__, OptionsPattern[]] :=
  Module[{},
    {"barA:  " -> OptionValue[barA], "barB:  " -> OptionValue[barB]}];

At first, we just call it without any explicitly passed options. It displays the values of options that it currently uses - in this case, those that are set globally through Options (OptionValue takes care of it):

In[57]:= bar[1, 2]
Out[57]= {"barA:  " -> 1, "barB:  " -> 2}

We now call one of the functions from our manager API (see below for the implementation):

setOptionConfiguration[bar,"first", {barA -> 11, barB -> 22, fooA -> 1}];

This defines an option configuration for the function bar under name "first". To make use of this option configuration, we use another function: withOptionConfiguration, like so:

In[58]:= withOptionConfiguration[bar[1, 2], "first"]
Out[58]= {"barA:  " -> 11, "barB:  " -> 22}

This looks as if we did change options for bar globally, but in fact we did not. Now, using our option configuration in the presence of explicitly passed option(s):

In[59]:= withOptionConfiguration[bar[1,2,barA->"overriding_barA"],"first"]
Out[59]= {barA:  ->overriding_barA,barB:  ->22}

In this setup, option configuartions override Options[bar], but options explicitly passed to bar override both. One can also inspect the option configurations for a given function, by calling another function from our API:

In[60]:= getOptionConfiguration[bar,"first"]
Out[60]= {barA->11,barB->22}

With this construct, one can do some cool things like creating shortcuts such as this:

In[61]:= 
first=Function[code,withOptionConfiguration[code,"first"],HoldFirst];

In[62]:= first@bar[1,2,barA->"overriding_barA"]
Out[62]= {barA:  ->overriding_barA,barB:  ->22}

In this way, one can create any number of option configurations one wants, and it is quite clear to anybody reading this code, what is going on.


Implementation

ClearAll[setOptionConfiguration, getOptionConfiguration, withOptionConfiguration];
SetAttributes[withOptionConfiguration, HoldFirst];
Module[{optionConfiguration},
  optionConfiguration[_][_] = {};
  setOptionConfiguration[f_, tag_, {opts___?OptionQ}] :=
     optionConfiguration[f][tag] = FilterRules[{opts}, Options[f]];
  getOptionConfiguration[f_, tag_] := optionConfiguration[f][tag];
  withOptionConfiguration[f_[args___], tag_] :=
  f[args, Sequence @@ optionConfiguration[f][tag]];
]; 

Remarks

On a few occasions, I found this type of constructs useful. Their main advantage is that while option configurations are persistent, they work by passing options locally, so options are not reset globally. And global option resetting is a dangerous business, particularly for common (e.g. built-in) functions, and when you deliver your functionality to others. Also, the above implementation is so light-weight that it can easily be extended or modified to suit one's needs.

Community
  • 1
  • 1
Leonid Shifrin
  • 22,449
  • 4
  • 68
  • 100
  • I don`t feel comfortable yet to apply this. However I feel it actually address a situation I am experiencing while dealing with the tradeoff bet customization and automation if those are appropriate terms. Ok and definitely @Leonid gets cut. Given how bad OS Lion is, I would think it is on them ! – 500 Aug 25 '11 at 22:35
  • 1
    Leonid, you might consider putting the code for custom functions at the bottom of your answers. I say this because I realized that I lost track of what you were saying after trying to understand the first code block in your post. I think it would be more streamlined to show the usage first, then provide code for those who care. – Mr.Wizard Aug 26 '11 at 07:06
6

The problem is that in chartOptions the variable chartData is a free variable referring to a global definition. In the BarChart expression, the chartData variable is localized to the containing module and is thus invisible to chartOptions. Change chartOptions to accept chartData as a parameter:

chartOptions[chartData_, yName_, xName] := ...

and then adjust the BarChart expression accordingly:

Module[{chartData},chartData=RandomInteger[20,20];
  BarChart[chartData,chartOptions[chartData, "yName","xName"]]]
WReach
  • 18,098
  • 3
  • 49
  • 93
  • As the various scoping constructs often confuse me, would changing `Module` to `Block` help? – rcollyer Aug 25 '11 at 15:34
  • 2
    @rcollyer It should help, yes. – Leonid Shifrin Aug 25 '11 at 15:40
  • 2
    @rcollyer Using `Block` would indeed work since it temporarily changes the global definition of the symbol. However, I would generally advise one to stick to parameters and local variables as much as possible to avoid subtle scoping problems -- like the one identified by this question. In the case of `chartOptions`, it is not immediately obvious to the reader that the function has a dependency upon a global definition. That lack of transparency often does (and did) lead to trouble. – WReach Aug 25 '11 at 15:43
  • 1
    I agree with removing the dependence on a global variable. It makes the code brittle, and easily exposed to changes. I was just trying to illustrate that alternatives do exist. – rcollyer Aug 25 '11 at 16:11
  • 1
    I also second @WReach in his advice to avoid global dependencies. This SO question http://stackoverflow.com/questions/6236458/plot-using-with-versus-plot-using-block-mathematica/ is quite illustrative in that respect, and in my answer there I gave extended arguments and explanations of subtleties involved. – Leonid Shifrin Aug 25 '11 at 18:28
5

Creating a function like this one is the right way to handle options that depend on the data to be plotted. But some of these options can be set for all uses of BarChart in that Mathematica session using SetOptions. For your example you might choose to define:

 SetOptions[BarChart, Frame -> {{True, True}, {True, True}},
            ImageSize -> 300,
            ChartStyle -> Black,
            ChartElementFunction -> "FadingRectangle",
            LabelStyle -> Directive[Black, Bold, 18]]

And leave only the options that depend on the data in your chartOptions function.

EDIT - caveat I did this without a working copy of Mathematica nearby - typos might still remain

If you do not want to set options globally, which as @Leonid pointed out, can result in other unwanted effects, you could define a custom function:

myBarChart[data_List,yName_,xName_,
  opts:OptionsPattern[{myBarChart,BarChart,RectangleChart,Graphics}]]:=
  BarChart[data,opts,FrameLabel -> {{yName, None}, {None, xName}},
  FrameTicks -> {{{Round@Min@data, Round@(Max@data/2), 
      Round@Max@data}, None},  PlotRange -> {Automatic, 1.3*Max@data}}
]

And then define the options to that function:

Options[myBarChart] = {Frame -> True, ImageSize -> 300,
            ChartStyle -> Black,
            ChartElementFunction -> "FadingRectangle",
            LabelStyle -> Directive[Black, Bold, 18]};
SetOptions[BarChart, Frame -> True, ImageSize -> 300,
            ChartStyle -> Black,
            ChartElementFunction -> "FadingRectangle",
            LabelStyle -> Directive[Black, Bold, 18]]

I should also point out that you could avoid the customised PlotRange option by setting it to All and setting the PlotRangePadding option as required. Probably PlotRangePadding->{{Automatic, Automatic},{Scaled[0.23],0}}. Scaled gives the padding as a fraction of the plot dimensions. Since you want the whitespace to be 0.3 of the full range of data, that is probably 0.3/1.3= 0.23, but you might want to experiment.

Verbeia
  • 4,400
  • 2
  • 23
  • 44