Principles of Dynamic Interfaces

Lou D’Andria
Wolfram Research

The Wolfram Language includes a rich set of primitives for building user interfaces which look and behave in desirable ways. But the best way to combine those primtives to satisfy a particular interface goal is not always clear. In this talk, we’ll discuss how to utilize Dynamic and other primitives to build responsive and efficient interfaces. We’ll pay special attention to progress indication and issues of synchronicity.

Join the Conversation #WolframTechConf

Talk Structure

We’ll outline some key principles to keep in mind while developing interfaces in the Wolfram Language, and along the way highligh some essential concepts and key techniques that should be in every interface programmer’s toolkit.

Principles

• The Principle of Programmatic Generation

• The Principle of Minimal Dynamics

• The Principle of Maximum Encapsulation

• The Principle of Evaluation Blocking

The Principle of Programmatic Generation

Every interface should be generated by a program.

The programmatic primitives

Control and dynamic primitives:

Dynamic
DynamicModule
Button
Checkbox
Slider
IntervalSlider
LocatorPane
EventHandler
MouseOver
MouseOver

Layout and styling:

Panel
Grid
Style
MouseOver

Deployment:

Deploy
CreatePalette
CreateDialog
MouseOver

Pre-generated

About box
Preferences
Help Viewer toolbar
Cloud Open / Save
Headers and footers
Palettes
lots more…

Generated on demand

Manipulate
WolframAlpha
SystemInformation
and more…

Dynamic wants to be current

The goal of Dynamic[expr] is to display expr as it is right now. And now. And now. You’re looking at now now.

Dynamic[x]

x=0.5;

When a Dynamic is evaluated for display, the kernel computes a full dependency graph.

Any change to any variable in that dependency graph triggers another update.

f[x_] := g[x]+a;
g[x_]:=h[x]+b;
h[x_]:=x^2;

Dynamic[f[3]]

a=2

g[x_]:=h[x]+2b

Bonus

Mel Brooks would have been great at Dynamic interface programming.

https://youtu.be/356faqb9JnU?t=33s

Dynamic really wants to be current

Dynamics that update too frequently are just as useless as dynamics that update too rarely.

When updates are too frequent

Restrict the scope of the Dynamic, either structurally, or via Refresh, TrackedSymbols, ControlActive, etc.

c=0;Dynamic[++c]

c=0;Dynamic[++c,TrackedSymbols :> {},UpdateInterval->1]

When updates are too rare

Make sure the symbols you think should be triggering an update actually are involved in the Dynamic.

Dynamic[DateString[],TrackedSymbols :> {c}]

Slider[Dynamic[c]]

Or, you can always use UpdateInterval.

Dynamic[DateString[]]

Dynamic[DateString[],UpdateInterval->1]

Dynamic wants to be seen

Dynamic does nothing unless it is typeset and rendered by the front end.

Dynamic[var=456];

This doesn’t set the value of var.

Dynamic[var]

What about this?

CreateDocument[CellGroup[{TextCell["example section", "Section"], ExpressionCell[Dynamic[var=123],"Output"]},Closed]];

Avoid unwanted rendering

There are constructs (PaneSelector, Mouseover, TabView, OpenerView, MenuView, …) that hold several expressions, and only display one at a time.

TabView[99^{11,55,99}]

But the front end needs to render every pane up front when ImageSize is All.

TabView[{1 -> Dynamic[Echo["first pane"]],2->Dynamic[Echo["second pane"]]},Dynamic[pane]]

Use ImageSize -> Automatic to avoid this.

TabView[{1 -> Dynamic[Echo["first pane"]],2->Dynamic[Echo["second pane"]]},Dynamic[pane],ImageSize -> Automatic]

The Principle of Minimal Dynamics

A Dynamic should never encompass unnecessary evaluation.

What is “unnecessary”?

Typical example of minimizing the scope of a Dynamic.

x=.

Around everything

Checkbox[Dynamic[frame]]
Dynamic[sol=NDSolve[{(∂_(t,t))u[t,x]==(∂_(x,x))u[t,x],u[0,x]==(E^(-(x^2))),Derivative[1,0][u][0,x]==0,u[t,-10]==u[t,10]},u,{t,0,40},{x,-10,10}];
DensityPlot[Evaluate[First[u[t,x]/.sol]],{t,0,40},{x,-10,10},PlotPoints->30, Frame -> frame]]

Around some things

Checkbox[Dynamic[frame]]
sol=NDSolve[{(∂_(t,t))u[t,x]==(∂_(x,x))u[t,x],u[0,x]==(E^(-(x^2))),Derivative[1,0][u][0,x]==0,u[t,-10]==u[t,10]},u,{t,0,40},{x,-10,10}];
Dynamic[DensityPlot[Evaluate[First[u[t,x]/.sol]],{t,0,40},{x,-10,10},PlotPoints->30, Frame -> frame]]

Around one thing

Checkbox[Dynamic[frame]]
sol=NDSolve[{(∂_(t,t))u[t,x]==(∂_(x,x))u[t,x],u[0,x]==(E^(-(x^2))),Derivative[1,0][u][0,x]==0,u[t,-10]==u[t,10]},u,{t,0,40},{x,-10,10}];
DensityPlot[Evaluate[First[u[t,x]/.sol]],{t,0,40},{x,-10,10},PlotPoints->30, Frame -> Dynamic[frame]]

Nesting Dynamics

When nested, an inner Dynamic can update independently of whether outer Dynamics have been triggered.  However, an outer Dynamic always force inner Dynamics to re-evaluate.

Style[Dynamic[{Echo[x,"outer:"],Framed @ Dynamic[Echo[y,Style["inner:", Purple]]]}],72]

Slider[Dynamic[x]]

Slider[Dynamic[y]]

Avoiding Dynamics

Sometimes, it’s worth asking if Dynamics are really needed at all for a given task.

Object targets (BONUS)

A button that can destroy itself via EvaluationBox / BoxObject.

Button["now",NotebookWrite[EvaluationBox[], ToBoxes @ DateString[]]]

Table[Button["now",NotebookWrite[EvaluationBox[], ToBoxes @ DateString[]]],{10}]

Other targets (BONUS)

Writing incremementally to an arbitrary target.

targetObject /: MakeBoxes[t_targetObject, fmt_] := InterpretationBox["\"<targetObject>\"", t]

$defaultTargetDisplay = RawBoxes[StyleBox["✍", FontColor -> RGBColor[0,0,1]]];

newTarget[display_:$defaultTargetDisplay] :=
	Block[{id, displayboxes, reference},
		id = CreateUUID[];
		displayboxes = PaneBox[ToBoxes[display], BoxID -> id];
		Print[{RawBoxes @ displayboxes}];
		reference = FE`BoxReference[EvaluationNotebook[], id];
		targetObject[reference, displayboxes]
	]

targetWrite[targetObject[reference_, displayboxes_], expr_] :=
	MathLink`CallFrontEnd[FrontEnd`BoxReferenceReplace[
		reference,
		RowBox[{ToBoxes[expr], ",", displayboxes}]
	]]

t=newTarget[]

targetWrite[t,foo]

targetWrite[t,Plot[Sin[x],{x,0,10}]]

targetWrite[t,DateString[]]

Do[targetWrite[t,i],{i,10}]

The Principle of Maximum Encapsultion

Interfaces should require no unnecessary initialization and produce no unnecessary side effects.

Two different approaches to deployment.

1: Deploy the result of running the code. (About box, Preferences, …)

2: Deploy the code itself, and run it on the user’s machine. (Manipulate, WolframAlpha, …)

Use DynamicModule

Reasons to use DynamicModule.

Scoping construct

Local variables in one instance of your structure are automatically isolated from any other instances.

Manipulate[x,{x,0,1}]

Saved state

Variable values will be preserved across sessions.

Initialization

Initialization code can be included manually, or automatically, or both.

DynamicModule[{x=1},Dynamic[myFunc[x]],Initialization :> (myFunc[t_] := t+1)]

SaveDefinitions

Note that SaveDefinitions is now an option to DynamicModule.

myFunc[t_] := t+1;
DynamicModule[{x=1},Dynamic[myFunc[x]],SaveDefinitions -> True]

FullDefinition[myFunc]

Passing variables by reference

When producing interfaces via program, it can quickly become awkward to develop everything inside of a single DynamicModule.

DynamicModule[{a=1},Column[{Slider[Dynamic[a],{1,5}],Dynamic[Plot[Sin[a x],{x,0,10}]]}]]

Here’s a more maintainable and extensible architecture… which doesn’t actually work!

controls[var_] := Slider[Dynamic[var],{1,5}];
display[var_]:=Dynamic[Plot[Sin[var x],{x,0,10}]];
DynamicModule[{a=1},Column[{controls[a],display[a]}],SaveDefinitions->True]

Why not?

Each utility is only getting access to the value of the local symbol, not a (preevaluated) reference to the local symbol.

Passing variables by reference

This fixed code relies on the fact that Dynamic doesn’t evaluate its first argument.

controls[Dynamic[var_]] := Slider[Dynamic[var],{1,5}];
display[Dynamic[var_]]:=Dynamic[Plot[Sin[var x],{x,0,10}]];
DynamicModule[{a=1},Column[{controls[Dynamic[a]],display[Dynamic[a]]}],SaveDefinitions->True]

You could also use holding attributes on these utilities.

The Principle of Evaluation Blocking

Only queued evaluations should ever block for user input. Most time-consuming evlauations should also be queued.

Terminology

Imgine you’re in a grocery store with only two checkout lanes open: the main checkout lane for everyone, and an express lane if you have 6-or-fewer items.

Personnel

Now imagine there's only one person working at the store, covering those two lanes.

CellPrint[ExpressionCell[Framed[Column[{Style["Hello",FontSize -> 36],"my name is",Rotate[Style[(*"Cornell"*)"Kernel",FontFamily -> "HanziPen TC",FontSize -> 64, FontColor -> Blue],10°]},Alignment -> Center,Dividers -> {False,{False,False,True,False}},Spacings -> {0,{0,0.5,1}}],RoundingRadius->10,FrameMargins -> {{50,50},{15,15}},FrameStyle -> Gray,BaseStyle -> {FontSize -> 24,FontFamily -> "Copperplate", FontColor -> GrayLevel[0.35]}],"Print",TextAlignment->Center]]

This clerk takes care of anyone in the express lane immediately, although if they have more than 6 items, the clerk kicks them out of line. When there is no one in the express lane, the clerk takes care of people in the main lane.

Terminology

TextGrid[{{Style["checkout lane:",FontSize -> 24,FontFamily -> "Copperplate",FontColor -> GrayLevel[0.35]],Style["express lane:", FontSize -> 24,FontFamily -> "Copperplate",FontColor -> GrayLevel[0.35]]},
{Column[{"main link","queued evaluations","asynchronous"}],Column[{"preemptive link","preemptive evaluation","synchronous"}]},
{Column[{Method -> "Queued",SynchronousUpdating->False,SynchronousInitialization->False, Defer[ControlActive[_,norm]],PerformanceGoal -> "Quality"},BaseStyle -> {"StandardForm",ShowStringCharacters->True}],Column[{Method -> "Preemptive",SynchronousUpdating->True,SynchronousInitialization->True,Defer[ControlActive[act,_]],PerformanceGoal -> "Speed"},BaseStyle -> {"StandardForm",ShowStringCharacters->True}]}
},ItemSize -> Scaled[0.49],Frame -> {All,True},BaseStyle -> Larger]//Print

Take home

If you have more than six items, stay out of the express lane!

Let slow things be slow

Asking the user for blocking input

Button["click",SystemDialogInput["Color"]]

Button["click",SystemDialogInput["Color"],Method -> "Queued"]

Getting data from a network

Dynamic[Keys @ Import["http://wtc15.pathable.com/meetings.json","JSON"]]

Dynamic[Keys @ Import["http://wtc15.pathable.com/meetings.json","JSON"], SynchronousUpdating->False,TrackedSymbols:>{}]

Rendering a complicated graphic or box structure

Assembling an extensive report (BONUS)

calculateResultsList[args___] :=
CheckAbort[
	(* fake call to a time-consuming thing, which occasionally sets $status. *)
	Do[Pause[.1];$status = Plot[Sin[n x],{x,0,10}],{n,20}];
	$status="";
	{args}
	,
	$status="";
	{0,0,0,0,0,0}
]

fullReport[results_] := CreateDocument[results]

briefReport[{0..}] := "No results"

briefReport[results_] :=
DynamicModule[{theResults = results},
	Dynamic[Column[{
		TabView[theResults, ImageSize -> Automatic],
		Button["Show full report", fullReport[theResults], ImageSize -> Automatic]
	}]]
]

Manipulate[
	If[TrueQ[running],
		Column[{
			Button["Abort", FrontEndExecute[FrontEnd`EvaluatorAbort[Automatic]], Evaluator->None, Method->"Queued", ImageSize -> Automatic],
			$status
		}],
		result
	],

	{mode,{"mode 1","mode 2","mode 3"}},
	PaneSelector[{
		"mode 1" -> TabView[{
			"tab 1" -> Column[{
				Control[{a,0.345}],
				Control[{b,0.123}],
				Control[{c,0.789}]}],
			"tab 2" -> Column[{
				Control[{{d,0.234},InputField[#,Number]&}],
				Control[{{e,0.678},InputField[#,Number]&}],
				Control[{{f,0.012},InputField[#,Number]&}]}]
			}],
		"mode 2" -> "to do",
		"mode 3" -> "to do"
		},
		Dynamic[mode],
		ImageSize -> Automatic
	],
	{{result, "No results"}, ControlType -> None},
	{running, {False, True}, ControlType -> None},
	Button["Run", (
		running = True;
		result = briefReport[calculateResultsList[mode,a,b,c,d,e,f]];
		running = False),
		ImageSize -> Automatic,
		Method -> "Queued",
		Enabled -> Dynamic[Not @ running]
	],
	SynchronousUpdating -> True
]

Any evaluation that may take more than a few seconds

Dynamic[Pause[10]]

Dynamic[Pause[10],SynchronousUpdating->False]

DynamicModule[{x=0},Dynamic[x],Initialization:>(Pause[10];x=1)]

DynamicModule[{x=0},Dynamic[x],Initialization:>(Pause[10];x=1),SynchronousInitialization->False]

Showing progress

What to do when you have a long running evaluation that you want to keep tabs on?

Monitor

Monitor[Do[i,{i,10^8}],i]

Monitor[Do[i,{i,10^7}],ProgressIndicator[i,{1,10^7}]]

Monitor[Pause[3];"done",ProgressIndicator[Appearance -> "Necklace"]]

See also: PrintTemporary, StatusArea, Annotation, …

What’s really happening here?

A queued evaluation happens to trigger a synchronous dynamic to update during the course of the queued evaluation.

{Button["click",c=0;Do[++c,10^7],Method -> "Queued"],Dynamic[c]}

On the other hand, preemptive evaluations will only trigger dynamics to update when the entire preemptive evaluation is complete.

{Button["click",c=0;Do[++c,10^7]],Dynamic[c]}

More Button examples

DynamicModule[{result=None,running=False},{
Button["click",running=True;Pause[2];result=DateString[];running=False,Method -> "Queued"],
Dynamic[If[TrueQ[running],ProgressIndicator[Appearance -> "Necklace"],result]]}]

DynamicModule[{result=None,running=False,progress=0},{
Button["click",running=True;progress=0;Do[++progress,10^7];result="done";running=False,Method -> "Queued"],
Dynamic[If[TrueQ[running],progress,result]]}]

DynamicModule[{result=None,running=False,progress=0},{
Button["click",running=True;progress=0;Do[++progress,10^7];result="done";running=False,Method -> "Queued"],
Dynamic[If[TrueQ[running],ProgressIndicator[progress,{1,10^7}],result]]}]

EventHandler Method

Starting in 10.2, EventHandler supports Method -> “Queued”.

And because Button uses EventHandler in Graphics, we can now do things like this:

DynamicModule[{c=Black,running = False},Overlay[{Graphics[{Dynamic[c],Button[Disk[],running=True;Pause[1];c=RandomColor[];running=False,Method-> "Queued"]}],Dynamic[If[TrueQ[running],ProgressIndicator[Appearance -> "Percolate"],""]]},{1,2},1,Alignment -> Center]]

ControlActive

Manipulate[Plot3D[Sin[a x y],{x,0,3},{y,0,3}],{a,1,5}]

When ControlActive appears in a dynamic that has SynchronousUpdating set to Automatic, then the draft renderings (1st arg) are done as preemptive evaluations, and the final rendering (2nd arg) uses a queued evaluation.

DynamicModule[{x=0},{Slider[Dynamic[x]],Dynamic[ControlActive[x,Pause[1];Panel[x]],SynchronousUpdating->Automatic]}]

Advanced example

http://stackoverflow.com/questions/5087443/mathematica-asynchronous-incremental-generation-of-dynamical-graphics/

Manipulate[ControlActive[Graphics[{Purple,Circle[{0,0},r]},PlotRange->{{-1,1},{-1,1}}],DynamicModule[{exprs={AbsoluteThickness[20],Gray,Circle[{0,0},r]},rr=r},Graphics[Dynamic[exprs],PlotRange->{{-1,1},{-1,1}}],Initialization:>(If[rr=!=r,Abort[]];Pause[1];
AppendTo[exprs,{Orange,Disk[{0,0},rr]}];If[rr=!=r,Abort[]];
Pause[1];AppendTo[exprs,{AbsoluteThickness[20],Black,Circle[{0,0},rr]}]),SynchronousInitialization->False]],{{r,0.5},0,1}]

Don’t be afraid to use the slow lane

It’s there for a reason. There’s no shame in using it.

Don’t change the DynamicEvaluationTimeout!

Using sync dynamics to trigger async dynanics

Not many controls can directly run queued evaluations. Button, EventHandler, ActionMenu. That’s about it.

Even though you can’t use a synchronous control to perform a long running evaluation, you can use a synchronous control to kick off a long-running evaluation, by having it trigger an asynchronous dynamic.

example

DynamicModule[{b=False,result=0, working=False},
Row[{DynamicWrapper[Checkbox[Dynamic[b]],result=If[b,First @ AbsoluteTiming[working=True;Pause[4];working=False],"-"],SynchronousUpdating->False,TrackedSymbols :>{b}],Dynamic[If[TrueQ[working],ProgressIndicator[Appearance -> "Percolate"],result]]}]]

PreemptProtect

Todd Gayley’s Essay on preemptive evaluation

http://mathematica.stackexchange.com/a/25829

Read this to understand details of preemption, PreemptProtect, and preemption-safe code.

Abort-safe code

It is especially important for interfaces to handle aborts without getting totally confused.

Previous example

DynamicModule[{result=None,running=False},{
Button["click",(running=True;Pause[2];result=DateString[];running=False),Method -> "Queued"],
Dynamic[If[TrueQ[running],ProgressIndicator[Appearance -> "Necklace"],result]]}]

Abort-safe version

DynamicModule[{result=None,running=False},{
Button["click",CheckAbort[(running=True;Pause[2];result=DateString[];running=False),AbortProtect[result="cleaning up…";running=False; Pause[1];result="-aborted-"]],Method -> "Queued"],
Dynamic[If[TrueQ[running],ProgressIndicator[Appearance -> "Necklace"],result]]}]

Estimating time remaining (BONUS)

If your computation is relatively uniform, then you can estimate the time remaining based on the average time taken per step so far.

implementation

durationDisplay[seconds_Real]:=Block[{h,m,s},
{h,m,s}=List@@QuantityMagnitude[DateDifference[{1900,1,1,0,0,0},DateList[seconds],{"Hour","Minute","Second"}]];Row[Flatten[{h,":",IntegerDigits[m,10,2],":",IntegerDigits[Floor[s],10,2]}]]]
durationDisplay[other_]:=If[NumericQ[other],durationDisplay[N[other]],"calculating…"]

timeRemainingDisplay[starttime_,currenttime_,currentamount_,totalamount_]:=Panel[Row[{"estimated time remaining: ", durationDisplay[1 + (currenttime - starttime) (totalamount/ currentamount - 1.)]}]]

SetAttributes[MonitoredDo,HoldAll];
MonitoredDo[expr_, {var_, min_,max_,step_}] := With[{start = AbsoluteTime[]},
Monitor[Do[expr,{var,min,max,step}],Refresh[timeRemainingDisplay[start,AbsoluteTime[],var,max],UpdateInterval -> 1, TrackedSymbols -> {}]]]

use

MonitoredDo[i,{i,1,10^8,1}]

MonitoredDo[i,{i,1,10^10,1}]

Summary

The Principle of Programmatic Generation

Every interface should be generated by a program.

The Principle of Minimal Dynamics

A Dynamic should never encompass unnecessary evaluation.

The Principle of Maximum Encapsulation

Interfaces should require no unnecessary initialization and produce no unnecessary side effects.

The Principle of Evaluation Blocking

Only queued evaluations should ever block for user input. Most time-consuming evlauations should also be queued.