swish/commit

New upstream files

authorJan Wielemaker
Fri Oct 16 10:45:27 2015 +0200
committerJan Wielemaker
Fri Oct 16 10:45:27 2015 +0200
commit199a4158ede59c57481d2930a87ed3a119e4246e
tree5944afb96bdbc09ca892ae7b48665c136f5ca9ef
parent5255e7e5dc75a95dc11b237af6bf2891201adef7
Diff style: patch stat
diff --git a/examples/render_c3.swinb b/examples/render_c3.swinb
new file mode 100644
index 0000000..a8fc501
--- /dev/null
+++ b/examples/render_c3.swinb
@@ -0,0 +1,172 @@
+<div class="notebook">
+
+<div class="nb-cell markdown">
+# Displaying results as charts
+
+Numerical results are often easier to understand when rendered as a chart than a list of numbers.  The SWISH web interface for SWI-Prolog allows rendering data through [C3.js](http://c3js.org).  C3 is a JavaScript library that used [D3.js](http://d3js.org) for the actual rendering.  However, C3.js can create many useful charts just from data, while D3.js typically requires writing JavaScript and CSS.
+
+## How it works
+
+Creating a C3 chart requires including the directive  `:- use_rendering(c3).` and binding a Prolog variable to a _dict_ with the _tag_ `c3` and the data needed by C3.  The `c3` renderer interferes in two ways with the data:
+
+  - If no size is specified, the width is set to 85% of the available width
+    and the height to `width/2+50`.  The chart is resized if the available
+    space changes.
+  - The renderer performs some basic sanity checks on the data and may
+    report an error.
+  
+## Our first chart
+
+As a first example, we will create a chart for the _sine_ function in the range `0..360` degrees.  The predicate sin/2 defines the sine relation for 37 datapoints on 10 degrees intervals.  We use findall/3 to create the required data rows.
+
+Now, we instantiate the C3 data structure, specifying the row names and the X-axis.
+</div>
+
+<div class="nb-cell program">
+:- use_rendering(c3).
+
+sin(X,Y) :-
+    between(0,36,I),
+    X is I*10,
+    Y is sin(X*pi/180).
+
+chart(Chart) :-
+    findall([X,Y], sin(X,Y), Data),
+    Chart = c3{data:_{x:x, rows:[[x,sine]|Data]}}.
+</div>
+
+<div class="nb-cell query">
+chart(Sine).
+</div>
+
+<div class="nb-cell markdown">
+## Alternatives for the the data
+
+By nature, Prolog is a relational language and the set of solutions naturally form a table, where each solution is a row.  C3 wants to see a JSON array-of-arrays, where each sub-array is a row and the first row defines the column names, much as you are used to in a spreadsheet.  The above creates the data as follows:
+
+```
+rows: [ [x, sine],
+        [0, 0.0],
+        [10, 0.173...],
+        ...
+      ]
+```
+
+The c3 rendering library allows the rows to be compound terms.  If we have
+a simple X-Y chart we can thus represent the same data as Prolog _pairs_,
+a representation that is more common in the Prolog world.
+
+```
+rows: [ x - sine,
+        0 - 0.0,
+        10 - 0.173...,
+        ...
+      ]
+```
+
+Finally, we can use _dicts_.  If we do so, we can omit the first row that 
+defines the column names as the dict _keys_ are used for that.  So, the data can be represented as:
+
+```
+rows: [ r{x:0, sin:0.0},
+        r{x:10, sin:0.173}
+        ...
+      ]
+```
+
+Predicates from library(dicts) can be used to combine series.  For example, dicts_join/4 can be used to combine two series on a common key.  The example below illustrates this.
+</div>
+
+<div class="nb-cell program">
+:- use_rendering(c3).
+
+sin(X,Y) :-
+    between(0,36,I),
+    X is I*10,
+    Y is sin(X*pi/180).
+
+cos(X,Y) :-
+    between(0,36,I),
+    X is I*10,
+    Y is cos(X*pi/180).
+
+chart(Chart) :-
+    findall(r{x:X,sin:Y}, sin(X,Y), SinData),	% create sin-series
+    findall(r{x:X,cos:Y}, cos(X,Y), CosData),	% create cos-series
+    dicts_join(x, SinData, CosData, Data),		% join on common 'x' key
+    Chart = c3{data:_{x:x, rows:Data}}.
+</div>
+
+<div class="nb-cell query">
+chart(Chart).
+</div>
+
+<div class="nb-cell markdown">
+## Creating pie and bar charts
+
+In this example we compute some simple statistics on HTML pages.  First, we create a program that created a sorted list of `[Tag,Count]` pairs for each element that appears on a page.  Note that the pairs are typically represented as `Tag-Count`, but this representation does not fit C3 well.  In this example we stay with the C3 representation.
+</div>
+
+<div class="nb-cell program">
+:- use_rendering(c3).
+:- use_module(library(sgml)).
+:- use_module(library(xpath)).
+
+popular_elements(Popular) :-
+    popular_elements('http://www.swi-prolog.org', Popular).
+
+popular_elements(URL, Popular) :-
+    findall(Elem, elem_in(URL, Elem), Elems),
+    frequency_count(Elems, Popular).
+
+frequency_count(Elems, Popular) :-
+    sort(0, =&lt;, Elems, Sorted),
+    count_same(Sorted, Counted),
+    sort(2, &gt;=, Counted, Popular).
+
+count_same([], []).
+count_same([H|T0], [H-C|T]) :-
+    count_same(H, T0, 1, C, T1),
+    count_same(T1, T).
+
+count_same(E, [E|T0], C0, C, T) :- !,
+    C1 is C0+1,
+    count_same(E, T0, C1, C, T).
+count_same(_, T, C, C, T).
+
+elem_in(URL, Elem) :-
+    load_html(URL, DOM, []),
+    xpath(DOM, //'*'(self), element(Elem,_,_)).
+</div>
+
+<div class="nb-cell markdown">
+Below, we run this program in three different queries.
+
+  1. Traditional Prolog.
+  2. Rendered as a C3 pie chart.
+  3. Rendered as a bar chart.  The latter requires us to name the rows, define the
+     x-axis and specify that the x-axis must be rendered as text.
+</div>
+
+<div class="nb-cell query">
+popular_elements(Pairs).
+</div>
+
+<div class="nb-cell query">
+popular_elements(_Pairs),
+Chart = c3{data:_{columns:_Pairs, type:pie}}.
+</div>
+
+<div class="nb-cell query">
+popular_elements(_Pairs),
+Chart = c3{data:_{x:elem, rows:[elem-count|_Pairs], type:bar},
+   	       axis:_{x:_{type:category}}}.
+</div>
+
+<div class="nb-cell markdown">
+## Further reading
+
+C3 defines most standard charts and many options to make the resulting charts look nice by providing labels for the axis, adding a legenda, choosing color schemes, etc. Please visit the [C3 examples](http://c3js.org/examples.html) for details.
+</div>
+
+</div>
diff --git a/examples/render_graphviz.swinb b/examples/render_graphviz.swinb
new file mode 100644
index 0000000..0e54205
--- /dev/null
+++ b/examples/render_graphviz.swinb
@@ -0,0 +1,194 @@
+<div class="notebook">
+
+<div class="nb-cell markdown">
+# The GraphViz renderer
+
+[GraphViz](http://www.graphviz.org) is a popular rendering program for graphs.  It takes the specification for a graph in the _dot_ language and produces a variety of output formats.  We are particularly interested in its ability to output SVG, Scalable Vector Graphics, which we can embed in the SWISH output window.
+
+The `graphviz` renderer needs to obtain a dot specification and it needs to know the layout algorithm to use.  It takes two terms:
+
+  - Using Layout(DotString), it simply renders a dot specification using the
+    layout engine Layout.  Provided layout engines are =dot= (most commonly
+    used), =neato=, =fdp=, =sfdp=, =twopi= and =circo=.  Please visit the
+    [GraphViz](http://www.graphviz.org) site for details.
+  - Using Layout(Graph), where `Graph` is a Prolog term representing the
+    dot AST (Abstract Syntax Tree).  The AST is translated into concrete
+    dot syntax using a DCG grammar.
+    
+Before we go into details, we do the _Hello World_ demo.  First, load the `graphviz` renderer:
+</div>
+
+<div class="nb-cell program">
+:- use_rendering(graphviz).
+</div>
+
+<div class="nb-cell markdown">
+First, we use the _Hello World_ example from the GrahViz demo pages using a dot string as specification:
+</div>
+
+<div class="nb-cell query">
+X = dot("digraph G { Hello-&gt;World }").
+</div>
+
+<div class="nb-cell markdown">
+Using a dot string as specification is of course not desirable because it makes
+it hard to generate a graph from computed data.  We introduce the *Prolog representation* for dot with the same example.  The only noticible difference is that the _render selection corner_ becomes visible because the graph is given 
+the attribute `bgcolor=transparent` if no background is given, while the graphviz
+default is `white`.
+</div>
+
+<div class="nb-cell query">
+X = dot(digraph(['Hello'-&gt;'World'])).
+</div>
+
+<div class="nb-cell markdown">
+The default `dot` program may be omitted.  This example also shows that you can use arbitrary terms as node identifiers and that you can add graph attributes.
+</div>
+
+<div class="nb-cell query">
+X = digraph([rankdir='LR', t(Y)-&gt;t(y)]).
+</div>
+
+<div class="nb-cell markdown">
+## A more complex example
+
+The following example is taken from the [Graphviz gallery](http://www.graphviz.org/Gallery.php),
+where the first query uses the dot syntax and the second represents the same graph as a Prolog
+term.
+</div>
+
+<div class="nb-cell query">
+X = dot("digraph G {
+
+	subgraph cluster_0 {
+		style=filled;
+		color=lightgrey;
+		node [style=filled,color=white];
+		a0 -&gt; a1 -&gt; a2 -&gt; a3;
+		label = \"process #1\";
+	}
+
+	subgraph cluster_1 {
+		node [style=filled];
+		b0 -&gt; b1 -&gt; b2 -&gt; b3;
+		label = \"process #2\";
+		color=blue
+	}
+	start -&gt; a0;
+	start -&gt; b0;
+	a1 -&gt; b3;
+	b2 -&gt; a3;
+	a3 -&gt; a0;
+	a3 -&gt; end;
+	b3 -&gt; end;
+
+	start [shape=Mdiamond];
+	end [shape=Msquare];
+}").
+</div>
+
+<div class="nb-cell query">
+X = dot(digraph([
+
+	subgraph(cluster_0,
+             [ style=filled,
+               color=lightgrey,
+               node([style=filled,color=white]),
+               a0 -&gt; a1 -&gt; a2 -&gt; a3,
+               label = "process #1"
+             ]),
+
+	subgraph(cluster_1,
+             [ node([style=filled]),
+               b0 -&gt; b1 -&gt; b2 -&gt; b3,
+               label = "process #2",
+               color=blue
+             ]),
+	start -&gt; a0,
+	start -&gt; b0,
+	a1 -&gt; b3,
+	b2 -&gt; a3,
+	a3 -&gt; a0,
+	a3 -&gt; end,
+	b3 -&gt; end,
+
+	node(start, [shape='Mdiamond']),
+	node(end, [shape='Msquare'])
+])).
+</div>
+
+<div class="nb-cell markdown">
+## Representing dot as Prolog
+
+The full dot language is represented as a Prolog term.  The shape of this term closely follows the
+[dot language ](http://www.graphviz.org/content/dot-language) and is informally defined by the
+grammar below:
+
+  ```
+  Graph      := graph(Statements)
+              | graph(Options, Statements)
+	      | digraph(Statements)
+	      | digraph(Options, Statements)
+  Options    := ID | [ID] | [strict, ID]
+  Statements := List of statements
+  Statement  := NodeStm | EdgeStm | AttrStm | Name = Value | SubGraph
+  NodeStm    := NodeID | node(NodeID, AttrList)
+  NodeID     := ID | ID:Port | ID:Port:CompassPT
+  CompassPT  := n | ne | e | se | s | sw | w | nw | c | _
+  EdgeStm    := (NodeID|SubGraph) (EdgeOp (NodeID|SubGraph))+
+  EdgeStm     | edge(NodeID|SubGraph) (EdgeOp (NodeID|SubGraph))+), AttrList)
+  EdgeOp     := - | -&gt;
+  AttrStm    := graph(AttrList)
+	      | node(AttrList)
+	      | edge(AttrList)
+  AttrList   := List of attributes
+  Attribute  := Name = Value
+	      | Name(Value)
+  SubGraph   := subgraph(ID, Statements)
+  ```
+</div>
+
+<div class="nb-cell markdown">
+## Generating Graphs from data
+
+Generating a graph from static data is of course not very interesting.  This section provides some examples for generating graphs from Prolog data.  This is where the Prolog representation of a _dot_ program comes in: it takes away the burden of generating the concrete dot syntax, avoiding issues such as proper escaping of labels.  In the example below we render an arbitrary (acyclic) Prolog term as a tree.  We do this by walking down the term, generating a node-id and a label for each node representing a compound term as well as each leaf.  Note that we cannot use the label as node id beause the same label may appear in multiple locations.  Thus, we generate dot statements like this:
+
+  - node(Id, [label=Label])
+  - `ChildID -&gt; ParentId`
+</div>
+
+<div class="nb-cell program">
+:- use_rendering(graphviz).
+
+tree(Compound, Root, Options0, Options) --&gt;
+    { compound(Compound), !,
+      atom_concat(n, Options0.id, Root),
+      compound_name_arguments(Compound, Name, Arguments),
+      format(string(Label), '~q', [Name]),
+      ID1 is Options0.id+1
+    },
+    [node(Root, [label=Label])],
+    children(Arguments, Root, Options0.put(id, ID1), Options).
+tree(Any, Leaf, Options0, Options) --&gt;
+    { atom_concat(n, Options0.id, Leaf),
+      format(string(Label), '~p', [Any]),
+      ID1 is Options0.id+1,
+      Options = Options0.put(id, ID1)
+    },
+    [ node(Leaf, [label=Label]) ].
+
+children([], _, Options, Options) --&gt; [].
+children([H|T], Parent, Options0, Options) --&gt;
+    [ Child -&gt; Parent ],
+    tree(H, Child, Options0, Options1),
+    children(T, Parent, Options1, Options).
+
+gvtree(Term, digraph([rankdir='BT',size=5|Statements])) :-
+    phrase(tree(Term, _, _{id:1}, _), Statements).
+</div>
+
+<div class="nb-cell query">
+A = f(1, a, `XY`, "XY", X), gvtree(A, T).
+</div>
+
+</div>
diff --git a/lib/swish/render/c3.pl b/lib/swish/render/c3.pl
index d59e9fc..069a340 100644
--- a/lib/swish/render/c3.pl
+++ b/lib/swish/render/c3.pl
@@ -30,10 +30,16 @@
 :- module(swish_render_c3,
 	  [ term_rendering//3			% +Term, +Vars, +Options
 	  ]).
+:- use_module(library(apply)).
+:- use_module(library(lists)).
 :- use_module(library(gensym)).
 :- use_module(library(error)).
+:- use_module(library(dif)).
 :- use_module(library(http/html_write)).
 :- use_module(library(http/js_write)).
+:- if(exists_source(library(dicts))).
+:- use_module(library(dicts)).
+:- endif.
 :- use_module('../render').
 
 :- register_renderer(c3, "Render data as tables").
@@ -48,9 +54,10 @@ Render data as a chart.
 %	Renders Term as a C3.js chart. This renderer recognises C3, as a
 %	dict with tag `c3`.
 
-term_rendering(C3, _Vars, _Options) -->
-	{ is_dict(C3, c3),
-	  valid_c3(C3),
+term_rendering(C30, _Vars, _Options) -->
+	{ is_dict(C30, Tag),
+	  Tag == c3,
+	  valid_c3(C30, C3),
 	  gensym('c3js', Id),
 	  atom_concat(#, Id, RefId),
 	  put_dict(bindto, C3, RefId, C3b)
@@ -105,22 +112,74 @@ term_rendering(C3, _Vars, _Options) -->
 		 ])).
 
 
-%%	valid_c3(+C3) is det.
+%%	valid_c3(+C3In, -C3Out) is det.
 %
 %	Perform sanity tests on the C3 representation.
 
-valid_c3(C3) :-
-	valid_c3_data(C3.data).
-
-valid_c3_data(C3) :-
-	valid_c3_array(C3.get(rows)), !.
-valid_c3_data(C3) :-
-	valid_c3_array(C3.get(columns)), !.
-valid_c3_data(C3) :-
-	throw(error(c3_no_data(C3), _)).
-
-valid_c3_array(Array) :-
-	must_be(list(list(ground)), Array).
+valid_c3(C30, C31) :-
+	Data0 = C30.data,
+	valid_c3_data(Data0, Data),
+	(   same_term(Data0, Data)
+	->  C31 = C30
+	;   C31 = C30.put(data,Data)
+	).
+
+valid_c3_data(Data0, Data) :-
+	Rows0 = Data0.get(rows), !,
+	must_be(acyclic, Rows0),
+	rows_to_matrix(Rows0, Rows),
+	must_be(list(ground), Rows),
+	(   same_term(Rows0, Rows)
+	->  Data0 = Data
+	;   Data = Data0.put(rows,Rows)
+	).
+valid_c3_data(Data0, Data) :-
+	Columns0 = Data0.get(columns), !,
+	must_be(acyclic, Columns0),
+	rows_to_matrix(Columns0, Columns),
+	must_be(list(ground), Columns),
+	(   same_term(Columns0, Columns)
+	->  Data0 = Data
+	;   Data = Data0.put(columns,Columns)
+	).
+valid_c3_data(Data, Data) :-
+	throw(error(c3_no_data(Data), _)).
+
+%%	rows_to_matrix(+RowsIn, -Rows) is semidet.
+%
+%	Translate alternative row representations into  a list of lists.
+%	Recognised input rows are:
+%
+%	  * Dicts having the same set of keys (if library(dicts) is
+%	    available)
+%	  * Compounds having same name and arity, e.g., pairs.
+%	  * Lists having the same length
+
+:- if(current_predicate(dicts_to_compounds/4)).
+rows_to_matrix(Dicts, [Keys|Rows]) :-
+	maplist(is_dict, Dicts), !,
+	maplist(dict_keys, Dicts, KeysList),
+	append(KeysList, Keys0),
+	sort(Keys0, Keys),
+	dicts_to_compounds(Dicts, Keys, dict_fill(undefined), Compounds),
+	maplist(compound_arguments, Compounds, Rows).
+:- endif.
+rows_to_matrix(Compounds, Rows) :-
+	dif(Name/Arity, []/2),		% avoid lists
+	maplist(name_arity_compound(Name, Arity), Compounds, Rows), !.
+rows_to_matrix(Lists, Lists) :-
+	maplist(length_list(_Columns), Lists).
+
+name_arity_compound(Name, Arity, Compound, Arguments) :-
+	compound(Compound),
+	compound_name_arity(Compound, Name, Arity),
+	compound_name_arguments(Compound, Name, Arguments).
+
+compound_arguments(Compound, Arguments) :-
+	compound_name_arguments(Compound, _, Arguments).
+
+length_list(Length, List) :-
+	length(List, Length).
 
 :- multifile
 	prolog:error_message//1.