Notebook Manipulation: Making a Calendar

The new capabilities of Mathematica add many possibilities for notebook manipulation and interface design. This notebook describes a few of those tools by way of a calendar program.

Laying Out the Months

The first step doesn't involve notebook manipulation at all. We need to write the basic code to lay out the days of the month in a grid. Luckily, much of what we need can be found in the standard Mathematica package Miscellaneous`Calendar`.

Needs["Miscellaneous`Calendar`"]

We first ask, “What day of the week is the first day of the month?”. It will be convenient for us to number the days from 1 to 7.

monthstart[month_,  
        year_]  := 
DayOfWeek[{year, month, 
            1}] /. {Sunday  ->  1, Monday  ->  2, 
          Tuesday  ->  3, Wednesday  ->  4, 
          Thursday  ->  5, Friday  ->  6, 
          Saturday  ->  7}

Next ask, “How many days are in a month?” We could of course write a look–up table based on the famous ditty, but to make certain we handle leap years and oddities correctly, let us write a function.

dayspermonth[month_,  
        year_]  := 
DaysBetween[{year, month, 
          1}, 
{If[month == 12, year + 1, year], 
          If[month == 12, 1, month + 1], 1}]

A month can now be laid out by partitioning the entire list of days in chunks of seven, padding the ends with empty strings.

monthlayout[month_,  year_]  := 
      Partition[Range[dayspermonth[month, year]],  7, 
        7, {monthstart[month, year], 1},  "" ]

Let’s try this out.

monthlayout[1, 2003] // TableForm

Looks good! We’re most of the way there…

Creating the Grid

With the grid we’ve already created, we have the elements necessary for a bare-bones calendar. It would be nice to improve the appearance a bit, however. For instance, we might give it a 3D look with colored backgrounds, as well as names for the days and the months. To do this, it is convenient to work with GridBox objects.

We begin by noting that a GridBox accepts a list of lists for its argument; precisely what our layout function generates. Note that this grid must be rectangular. For irregular numbers of columns, we must nest grid boxes. For instance, we can say:

\(CellPrint[
      Cell[BoxData[
          GridBox[{{"January 2003"}, {GridBox[
                  Prepend[
                    monthlayout[1, 
                      2003], {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}]]}}]], \ "Output"]]\)

Here we see the first use of notebook functions. CellPrint is one of the most basic functions, which writes the cell expression it is given into the current notebook, in much the same way that Print works.

We can tell that we will need the abbreviations of the days of the week again; we will probably also need the names of the months. Let’s assign them to handy symbols.

\(\(alldays\  = \ {"Sun", \ "Mon", \ "Tue", \ "Wed", \ "Thu", "Fri", "Sat"};\)\)

\(\(allmonths\  = \ {"January", \ "February", \ "March", \ "April", \ "May", \ "June", "July", \ "August", \ "September", \ "October", \ "November", "December"};\)\)

Now let’s enhance the appearance of our grid. We do this in two ways: by use of button boxes and by use of style boxes. ButtonBox objects are normally used to attach an action that is triggered by a mouse click to some box expression. We will see an example of this later. However, button boxes also have an appealing 3D visual aspect. We will use that here by setting each button’s function to Null. StyleBox wrappers, in contrast, generically apply options to the enclosed boxes. Here, we will be concerned with those options controlling aspects of style: fonts, colors, and the like. Finally, we can use various options to the grid boxes that affect display to improve the look of the calendar.

For example, we can place button boxes around the various pieces and control their colors.

CellPrint[
      Cell[BoxData[
          GridBox[{{ButtonBox["January 2003", 
                  Background -> RGBColor[0.4,  0,  0.4], 
                  ButtonFunction -> Null]}, {GridBox[
                  Prepend[
                    monthlayout[1, 2003] /. 
                      i : (_Integer  |  _String) :> 
                        ButtonBox[i, 
                          Background -> RGBColor[1,  0.6,  0.8],  
                          ButtonFunction -> Null], 
                    alldays /. 
                      s_String :> 
                        ButtonBox[s, 
                          Background -> RGBColor[0.8,  0.2,  0.6], 
                          ButtonFunction -> 
                            Null]]]}}]], "Output"]]

This will look better if we eliminate the space between the rows and columns; it will also help to change the colors of the fonts in the headers. Finally, this will probably look better if we use the Helvetica font. Font changes can be handled by StyleBox wrappers, while the grid alignment is handled by GridBox options.

\(CellPrint[
      Cell[BoxData[
          StyleBox[
            GridBox[{{ButtonBox[
                    StyleBox["January 2003", 
                      FontWeight -> "Bold", 
                      FontColor -> GrayLevel[1]], 
                    Background -> RGBColor[0.4, \ 0, \ 0.4], 
                    ButtonFunction -> Null]}, {GridBox[
                    Prepend[
                      monthlayout[1, 2003] /. 
                        i : \((_Integer\  | \ _String)\) :> 
                          ButtonBox[i, 
                            Background -> RGBColor[1, \ 0.6, \ 0.8], \ 
                            ButtonFunction -> Null], 
                      alldays /. 
                        s_String :> 
                          ButtonBox[
                            StyleBox[s, FontWeight -> "Bold", 
                              FontColor -> GrayLevel[0.8]], 
                            Background -> 
                              RGBColor[0.8, \ 0.2, \ 
                                0.6], 
ButtonFunction -> Null]], \ ColumnSpacings\  -> \ 
                      0, 
RowSpacings\  -> \ 
                      0, 
ColumnWidths -> 3, 
                    RowMinHeight -> 
                      1.5]}}, 
RowSpacings -> 0], 
            FontFamily -> "Helvetica", 
            FontWeight -> "Plain"]], "Output"]]\)

This progression of successively adding features to the layout as you develop the format you want is natural to Mathematica. It is very easy to test modifications by interactively adjusting the inputs. For instance, suppose we wanted a different color set; it is a matter of instants to change the RGB values in the preceding expression to test different possibilities. The incremental structure also allows us to test the core of our layout before adding less essential aspects (such as color and font) to the expression.

Now that we have a grid we like, let’s put it in a function.

monthgrid[month_,  year_]  := 
StyleBox[
        GridBox[{{ButtonBox[
                StyleBox[
                  allmonths[[
                      month]] <> " " <> 
                    ToString[year], FontWeight -> "Bold", 
                  FontColor -> GrayLevel[1]], 
                Background -> RGBColor[0.4,  0,  0.4], 
                ButtonFunction -> Null]}, {GridBox[
                Prepend[
                  monthlayout[month, year] /. 
                    i : (_Integer  |  _String) :> 
                      ButtonBox[i, 
                        Background -> RGBColor[1,  0.6,  0.8],  
                        ButtonFunction -> Null], 
                  alldays /. 
                    s_String :> 
                      ButtonBox[
                        StyleBox[s, FontWeight -> "Bold", 
                          FontColor -> GrayLevel[0.8]], 
                        Background -> RGBColor[0.8,  0.2,  0.6], 
                        ButtonFunction -> Null]],  
                ColumnSpacings  ->  0, 
 
                RowSpacings  ->  
                  0, 
ColumnWidths -> 
                  3, 
RowMinHeight -> 
                  1.5]}}, 
RowSpacings -> 
            0], 
FontFamily -> "Helvetica", 
        FontWeight -> "Plain"]

CellPrint[
      Cell[BoxData[
          monthgrid[3, 2002]], 
"Output"]]

Making Cells and Notebooks

The basic arrangement of a Mathematica document, or notebook, is a hierarchical nesting of cells (chunks of text, typesetting, graphics, etc.). This is easily representable as a Mathematica expression, and in fact, this is how the front end stores a notebook. For example:

If we create an expression of this form, we can direct the front end to turn it into a window with the command NotebookPut. Alternately, we could create an empty notebook window with NotebookCreate, then fill it by writing into it with NotebookWrite and setting its options with SetOptions.

In this case, we would like to create a palette-style window, and fill it as much as possible with the grid specifying the month. We do this by creating a notebook with one cell, setting the options of that cell and the notebook to control the overall appearance.

For example, in the simplest form, we can say:

NotebookPut[
      Notebook[{Cell[BoxData[monthgrid[1, 2003]],  
            NotebookDefault]}]]

Here, all the cell and notebook options take on default values. We even specify a default style for the cell (which means that we don’t care to adopt any values from an existing cell style; this won’t always be the case).

We can write a variant of this that changes the appearance a bit.

\(\(NotebookPut[
        Notebook[{Cell[BoxData[monthgrid[1, 2003]], \ NotebookDefault, 
              CellMargins\  -> {{0, 0}, {0, 0}}, 
              ShowCellBracket -> False]}, WindowElements -> {}, 
          WindowFrameElements -> "CloseBox", 
          WindowSize -> {252, 134}, WindowFrame -> "Palette", 
          WindowTitle -> "Calendar", 
          Background -> 
            RGBColor[0.4, \ 0, \ 
              0.4], 
ScrollingOptions\  -> \ {"PagewiseScrolling" -> True, \ "PagewiseDisplay" -> 
                True}]];\)\)

Most of these options are fairly simple. The CellMargins option gives the space around the cell, expressed in points, in the order {{left, right}, {bottom, top}}. The WindowElements option controls things at the notebook level like scroll bars and the status area, while the WindowFrameElements are items that appear in the frame, such as the close box, resize area, and zoom box.

To this point, we've been writing cell expressions into the current notebook with CellPrint. However, we may want finer control in some situations, writing the expression into an arbitrary notebook. The standard tool for this is NotebookWrite, which in one form is expressed as NotebookWrite[nbobj, cellexpr]. Some of the standard notebook objects that can be used for nbobj include the SelectedNotebook[], the ButtonNotebook[], the EvaluationNotebook[], and the InputNotebook[]. If you know the specific notebook object (for instance, returned by NotebookPut or selected from the list returned by Notebooks), you can use that in the same place as one of these notebooks.

Other operations on notebooks include putting or getting entire notebook expressions (NotebookPut, NotebookGet), writing to or reading from existing notebooks (NotebookWrite, NotebookRead). There are a host of other operations for manipulating selections and notebook contents, which are documented in the Mathematica book.

We will put these concepts into a function after one more issue is handled: controlling the calendar.

Actions in the Interface

As intimated previously, buttons are commonly used to do things, not sit around looking pretty. One thing that we might like to do in this calendar is to change to different months. The simplest interface for this would let us scroll to the next or the previous month from the one currently displayed. So, we might create a “Next” and a “Previous” button.

A button is a box expression with a function attached to it. That function is in an option, called, naturally enough, ButtonFunction. That function can be any Mathematica function. Another option, called ButtonEvaluator, tells the button where to evaluate the function. A very limited set of functions (consisting solely of notebook operations) can be evaluated entirely in the front end, but most need to go to the current Mathematica  kernel to be effective. (Others might be directed to a dedicated kernel, or to another application connected via MathLink.) For example, here we use the technique demonstrated in the section on writing grids to write out a button that pops up our calendar.

\(CellPrint[
      Cell[BoxData[
          ButtonBox["Calendar for January 2002", 
ButtonEvaluator\  -> Automatic, \ 
            Active -> 
              True, 
ButtonFunction :> 
              NotebookPut[
                Notebook[{Cell[BoxData[monthgrid[1, 2002]], \ 
                      NotebookDefault, 
                      CellMargins\  -> {{0, 0}, {0, 0}}, 
                      ShowCellBracket -> False]}, 
                  WindowElements -> {}, 
                  WindowFrameElements -> "CloseBox", 
                  WindowSize -> {252, 134}, 
                  WindowFrame -> "Palette", 
                  WindowTitle -> "Calendar", 
                  Background -> 
                    RGBColor[0.4, \ 0, \ 
                      0.4]]]]], "Output", 
CellEditDuplicate -> False]]\)

A button function can actually be a Mathematica pure function, whose arguments come from the contents of the button box, the ButtonData option, or a location specified by the ButtonSource option. However, for our purposes, we can start with a simpler model, and take the data from global variables.

Actually, it is, properly speaking, poor form to store the program state in global variables when it might be stored in the notebook. Caching information in the notebook allows you to save it and reopen it between sessions without losing the state of the notebook. The next section will examine this alternative.

What portion of the state do we want to preserve? The month and the year seem the logical canidates.

$CurrentCalendarMakerYear  =  2000;
$CurrentCalendarMakerMonth  =  9;

Then, one button increments the month and displays a calender, while the other decrements (both wrapping appropriately at year boundaries).

\(CellPrint[
      Cell[BoxData[
          RowBox[{
ButtonBox["Previous", 
ButtonEvaluator\  -> 
                  Automatic, 
Active -> 
                  True, 
ButtonFunction :> \((\(--$CurrentCalendarMakerMonth\); 
If[$CurrentCalendarMakerMonth == 
                        0, $CurrentCalendarMakerMonth = 
                        12; 
\(--$CurrentCalendarMakerYear\)]; 
NotebookPut[
                      Notebook[{Cell[
                            BoxData[
monthgrid[$CurrentCalendarMakerMonth, $CurrentCalendarMakerYear]], 
NotebookDefault, 
CellMargins\  -> {{0, 0}, {0, 0}}, 
ShowCellBracket -> 
                              False]}, 
WindowElements -> {}, WindowFrameElements -> "CloseBox", 
                        WindowSize -> {252, 134}, 
                        WindowFrame -> "Palette", 
                        WindowTitle -> "Calendar", 
                        Background -> 
                          RGBColor[0.4, \ 0, \ 
                            0.4]]])\)], 
ButtonBox["Next", 
ButtonEvaluator\  -> Automatic, \ Active -> True, 
                ButtonFunction :> \((\(++$CurrentCalendarMakerMonth\); 
                    If[$CurrentCalendarMakerMonth == 
                        13, $CurrentCalendarMakerMonth = 
                        1; \(++$CurrentCalendarMakerYear\)]; 
                    NotebookPut[
                      Notebook[{Cell[
                            BoxData[
monthgrid[$CurrentCalendarMakerMonth, $CurrentCalendarMakerYear]], \ NotebookDefault, 
                            CellMargins\  -> {{0, 0}, {0, 0}}, 
                            ShowCellBracket -> False]}, 
                        WindowElements -> {}, 
                        WindowFrameElements -> "CloseBox", 
                        WindowSize -> {252, 134}, 
                        WindowFrame -> "Palette", 
                        WindowTitle -> "Calendar", 
                        Background -> 
                          RGBColor[0.4, \ 0, \ 
                            0.4]
]])\)]}]], \ "Output"]]\)

Well, notwithstanding the redundant code, the principle is sound; but we are getting a new window with each button click! This is not good… However, we can resolve that issue and tie together all the pieces in the final section.

Making the Calendar

We now have most of the pieces we need to make our calendar application. The only remaining tool is one to replace the current calendar grid with a new one, rather than creating a new window. Several techniques are possible. For instance, NotebookPut can take an additional argument that is a notebook object; in that case, the named notebook will be replaced in toto. Another method is to move the current location to the top of the notebook, then select the first cell in the notebook. This will work if you can guarantee the structure of the notebook, but it is usually better (more robust) to use a third technique: tag the target cell when it is created, then locate it by the NotebookFind command. NotebookFind is used in general to search a notebook, but with the specific form NotebookFind[notebook object, tag, All, CellTags], it will select the tagged cell(s). Use the CellTags -> "tag" option at the cell level to specify a tag.

Now, putting everything together in a modular fashion, we start with a function that creates the grid cell. We add a fillip to color the current date red; we also place the buttons that change the month with the title. In principle, we should simply recode monthgrid, but as an example of working from an existing code base, we will transform our current result.

\(monthcell[month_, \ year_]\  := 
Cell[
        BoxData[monthgrid[month, year] /. 
            Join[{{ButtonBox[
                      title_, ___]}\  :> {GridBox[{{ButtonBox[
                            AdjustmentBox[
                              StyleBox["⟸", 
                                FontColor -> GrayLevel[1]], 
                              BoxMargins\  -> \ {{0.2, 3}, {0.8, 
                                    0.8}}], 
ButtonFrame -> None, 
Active -> True, 
                            ButtonEvaluator -> Automatic, 
                            ButtonFunction :> 
                              changemonth[
                                Decrement]], 
AdjustmentBox[
                            title, 
                            BoxMargins\  -> \ {{0, 0}, {0.7, 
                                  0.7}}], 
ButtonBox[
                            AdjustmentBox[
                              StyleBox["⟹", 
                                FontColor -> GrayLevel[1]], 
                              BoxMargins\  -> \ {{3, 0.2}, {0.8, 
                                    0.8}}], 
ButtonFrame -> None, 
Active -> True, 
                            ButtonEvaluator -> Automatic, 
                            ButtonFunction :> 
                              changemonth[Increment]]}}, 
                      ColumnWidths\  -> {5, 9, 
                          5}, 
RowMinHeight\  -> \ 
                        2, 
RowAlignments\  -> \ 
                        Top]}}, 
If[{year, month}\  === \ 
                  Take[Date[], 
                    2], {ButtonBox[
                      d : Evaluate[\(Date[]\)[[3]]], opts___] :> 
                    ButtonBox[d, 
                      Background -> RGBColor[1. , \ 0.2, \ 0.4], 
                      opts]}, {}]]], 
\ NotebookDefault, 
        CellMargins\  -> {{0, 0}, {0, 0}}, 
        ShowCellBracket -> False, 
Active -> 
          True, CellTags -> "TheCalendarMakerMonth"]\)

The button functions for incrementing and decrementing the current month can be combined into one function. Here, let op be Increment or Decrement, depending on which way you want to go.

We can also handle the issue of storing the state of the calendar properly, storing the current month with the notebook rather than depending on the global variables. This allows us to have multiple calendars; we can also save a calendar, and expect it to work once the supporting functions have been evaluated. There exists an option calling TaggingRules which can be used to store an arbitrary Mathematica expression at the notebook, cell, or style box levels. By convention, we usually store a list of "tagstring" -> value rules with the TaggingRules, allowing multiple overlapping uses. Here, we use the TaggingRules associated with the whole notebook, which can be accessed via Options[ButtonNotebook[], TaggingRules]. The tag string we will call "CalendarInformation".

\(changemonth[
        op_]\  := 
Module[{month, \ 
          year}, 
{month, \ 
            year}\  = \ "CalendarInformation" /. \((TaggingRules /. 
                Options[ButtonNotebook[], \ TaggingRules])\); 
        op[month]; 
Switch[month, 
0, 
          month = 12; op[year], 
13, month = 1; 
          op[year]]; 
NotebookFind[
          ButtonNotebook[], \ "TheCalendarMakerMonth", \ All, \ 
          CellTags]; 
SetOptions[
          ButtonNotebook[], 
TaggingRules\  -> \ {"CalendarInformation"\  -> \ {month, \ 
                  year}}]; 
NotebookWrite[ButtonNotebook[], 
          monthcell[month, year], AutoScroll -> False]]\)

Now we write the user–level function that initializes the state and creates the notebook.

\(MakeCalendar[month_, \ year_]\  := 
NotebookPut[
        Notebook[{monthcell[month, year]}, WindowElements -> {}, 
          WindowFrameElements -> "CloseBox", 
          WindowSize -> {Fit, 165}, WindowClickSelect -> False, 
          WindowFrame -> "Palette", 
          WindowTitle -> "Calendar", 
Active -> True, 
          Background -> 
            RGBColor[0.4, \ 0, \ 
              0.4], 
ScrollingOptions\  -> \ {"PagewiseScrolling" -> True, \ "PagewiseDisplay" -> 
                True}, 
TaggingRules\  -> \ {"CalendarInformation"\  -> \ {month, \ year}}]]\)

MakeCalendar should accept the standard Mathematica date format.

MakeCalendar[{y_, m_, ___}] := MakeCalendar[m, y]

MakeCalendar[] := MakeCalendar[Date[]]

Let’s try it out!

MakeCalendar[]

Of course, a very large number of variants are possible. For instance, since each day is a button, those buttons could be made to call up a scheduler; alternately, they could access data relevant to the date. The days could be color–coded for your favorite data set, e.g., stock performance or maximum temperature.

Many other notebook manipulation techniques exist, but this example hopefully gives a sense of a few of the tools that are available.