diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,10 @@
+0.11.0 release 2024-02-06
+
+* Add sorting by numbers [#183](https://github.com/mpickering/eventlog2html/pull/183)
+* Refactoring and modernization of the UI [#178](https://github.com/mpickering/eventlog2html/pull/178)
+* Compatibility with 9.0, 9.2, 9.4, 9.6 and 9.8 which are now also checked in CI.
+  ([#180](https://github.com/mpickering/eventlog2html/pull/180) and [#181](https://github.com/mpickering/eventlog2html/pull/181))
+
 0.10.0 release 2023-03-31
 
 * Add support for rendering ticky profiles
@@ -6,7 +13,7 @@
 
 * Add "--version" flag for eventlog2html which prints the version,
   git commit and git branch used to build eventlog2html.
-* Compatability with 9.4.* and 9.2.* compilers.
+* Compatibility with 9.4.* and 9.2.* compilers.
 
 0.9.2 release 2021-11-24
 
diff --git a/eventlog2html.cabal b/eventlog2html.cabal
--- a/eventlog2html.cabal
+++ b/eventlog2html.cabal
@@ -1,6 +1,6 @@
 cabal-version:       2.4
 Name:                eventlog2html
-Version:             0.10.0
+Version:             0.11.0
 Synopsis:            Visualise an eventlog
 Description:         eventlog2html is a library for visualising eventlogs.
                      At the moment, the intended use is to visualise eventlogs
@@ -28,29 +28,31 @@
   javascript/generated/vega-embed@6.14.0
   javascript/generated/vega@5.17.0
   javascript/generated/vega-lite@4.17.0
+  javascript/generated/popper.js@1.14.3
+  inline-docs/*.html
 extra-doc-files: README.md
                  CHANGELOG
-Tested-With:         GHC ==9.2.5, GHC ==9.4.3
+Tested-With:         GHC ==9.0.2, GHC ==9.2.7, GHC ==9.4.7, GHC ==9.6.2, GHC ==9.8.1
 
 Library
   Build-depends:
-    aeson                >= 1.4.3 && < 1.6 || >= 2.0 && < 2.2,
+    aeson                >= 1.4.3 && < 1.6 || >= 2.0 && < 2.3,
     attoparsec           >= 0.13.2 && < 0.15,
     array                >= 0.5.3 && < 0.6,
     base                 >= 4 && < 5,
     blaze-html           >= 0.9.1 && < 0.10,
-    bytestring           >= 0.10.8 && < 0.12,
+    bytestring           >= 0.10.8 && < 0.13,
     blaze-markup,
-    containers           >= 0.5.0 && < 0.7,
+    containers           >= 0.5.0 && < 0.8,
     file-embed           >= 0.0.11 && < 0.1,
-    filepath             >= 1.4.2 && < 1.5,
+    filepath             >= 1.4.2 && < 1.6,
     ghc-events           >= 0.19.0 && < 0.20,
     hashtables           >= 1.2.3 && < 1.4,
     hvega                >= 0.11.0 && < 0.13,
-    mtl                  >= 2.2.2 && < 2.3,
-    optparse-applicative >= 0.14.3 && < 0.18,
+    mtl                  >= 2.2.2 && < 2.4,
+    optparse-applicative >= 0.14.3 && < 0.19,
     semigroups           >= 0.18 && < 0.21,
-    text                 >= 1.2.3 && < 1.3 || >= 2.0 && < 2.1,
+    text                 >= 1.2.3 && < 1.3 || >= 2.0 && < 2.2,
     time                 >= 1.8.0 && < 2.0,
     vector               >= 0.11,
     trie-simple          >= 0.4,
@@ -74,6 +76,8 @@
                        Eventlog.HeapProf
                        Eventlog.Vega
                        Eventlog.HtmlTemplate
+                       Eventlog.Rendering.Bootstrap
+                       Eventlog.Rendering.Types
                        Eventlog.VegaTemplate
                        Eventlog.AssetVersions
                        Eventlog.Detailed
diff --git a/inline-docs/heap.html b/inline-docs/heap.html
new file mode 100644
--- /dev/null
+++ b/inline-docs/heap.html
@@ -0,0 +1,31 @@
+<dl>
+  <dt style="color: red">Heap Size</dt>
+  <dd>Total size of <emph>megablocks</emph> allocated by the RTS</dd>
+  <dt style="color: blue">Blocks Size</dt>
+  <dd>Total size of <emph>blocks</emph> allocated by the RTS</dd>
+  <dt style="color: green">Live bytes</dt>
+  <dd>Total size of objects live on the heap (should match top of area pane)</dd>
+</dl>
+
+<p>Process memory usage (as reported by the OS) should correspond roughly to the <span style="color: red">heap size</span>,
+except for data allocated through the FFI (and the RTS also calls <tt>malloc()</tt> itself in a few places).
+</p>
+
+<p>
+When heap profiling is enabled, <span style="color: blue">blocks
+size</span> and <span style="color: green">live bytes</span>
+will typically differ by the size of the nursery.
+For understanding the relationship between <span style="color: blue">blocks
+size</span> and <span style="color: green">live bytes</span>,
+this <a href="https://www.well-typed.com/blog/2021/03/memory-return/">blog
+post</a> contains more information.</p>
+
+<p>
+When heap profiling is not enabled, <span style="color: green">live bytes</span> will be updated only on a major GC, but <span style="color: red">heap size</span> and <span style="color: blue">blocks size</span> are updated more frequently. (Enabling heap profiling causes more frequent major GCs to collect heap samples.)
+</p>
+
+<p>
+This view can also be useful in identifying
+fragmentation. A very badly fragmented heap will have low <span style="color: green">live bytes</span> but
+much higher <span style="color: blue">blocks size</span>.
+</p>
diff --git a/inline-docs/no-cost-centres.html b/inline-docs/no-cost-centres.html
new file mode 100644
--- /dev/null
+++ b/inline-docs/no-cost-centres.html
@@ -0,0 +1,3 @@
+<p>
+The cost centres view is available only when executing the program with <tt>+RTS -hc</tt>.
+</p>
diff --git a/inline-docs/no-detailed.html b/inline-docs/no-detailed.html
new file mode 100644
--- /dev/null
+++ b/inline-docs/no-detailed.html
@@ -0,0 +1,3 @@
+<p>
+No detailed data to display.
+</p>
diff --git a/inline-docs/no-heap-profile.html b/inline-docs/no-heap-profile.html
new file mode 100644
--- /dev/null
+++ b/inline-docs/no-heap-profile.html
@@ -0,0 +1,14 @@
+<p>
+This eventlog was generated without heap profiling.
+</p>
+
+<p>
+To create a heap profile, you need to run the program with one of the <tt>+RTS
+-h...</tt> options.  The <tt>-hT</tt> (closure type) and <tt>-hi</tt> (info
+table) heap profiling options are available for all programs.  If you compile
+with profiling enabled, more options are available.
+</p>
+
+<p>
+See the <a href="https://downloads.haskell.org/ghc/latest/docs/users_guide/profiling.html#profiling-memory-usage">GHC user's guide on profiling memory usage</a> for more information.
+</p>
diff --git a/inline-docs/no-ticky.html b/inline-docs/no-ticky.html
new file mode 100644
--- /dev/null
+++ b/inline-docs/no-ticky.html
@@ -0,0 +1,3 @@
+<p>
+No ticky data to display.
+</p>
diff --git a/javascript/generated/fancyTable.min.js b/javascript/generated/fancyTable.min.js
--- a/javascript/generated/fancyTable.min.js
+++ b/javascript/generated/fancyTable.min.js
@@ -1,9 +1,9 @@
 /*!
- * jQuery fancyTable plugin v1.0.26
+ * jQuery fancyTable plugin v1.0.34
  * https://github.com/myspace-nu
  *
  * Copyright 2018 Johan Johansson
  * Released under the MIT license
  */
 
-!function(l){l.fn.fancyTable=function(a){var s=l.extend({inputStyle:"",inputPlaceholder:"Search...",pagination:!1,paginationClass:"btn btn-light",paginationClassActive:"active",pagClosest:3,perPage:10,sortable:!0,searchable:!0,matchCase:!1,exactMatch:!1,onInit:function(){},onUpdate:function(){},sortFunction:function(a,e,t){return"numeric"==t.sortAs[t.sortColumn]?0<t.sortOrder?parseFloat(a)-parseFloat(e):parseFloat(e)-parseFloat(a):a<e?-t.sortOrder:e<a?t.sortOrder:0},testing:!1},a),o=this;return this.settings=s,this.tableUpdate=function(n){if(n.fancyTable.matches=0,l(n).find("tbody tr").each(function(){var a=0,e=!0,t=!1;l(this).find("td").each(function(){s.globalSearch||!n.fancyTable.searchArr[a]||o.isSearchMatch(l(this).html(),n.fancyTable.searchArr[a])?!s.globalSearch||n.fancyTable.search&&!o.isSearchMatch(l(this).html(),n.fancyTable.search)||Array.isArray(s.globalSearchExcludeColumns)&&s.globalSearchExcludeColumns.includes(a+1)||(t=!0):e=!1,a++}),s.globalSearch&&t||!s.globalSearch&&e?(n.fancyTable.matches++,!s.pagination||n.fancyTable.matches>n.fancyTable.perPage*(n.fancyTable.page-1)&&n.fancyTable.matches<=n.fancyTable.perPage*n.fancyTable.page?l(this).show():l(this).hide()):l(this).hide()}),n.fancyTable.pages=Math.ceil(n.fancyTable.matches/n.fancyTable.perPage),s.pagination){var a=n.fancyTable.paginationElement?l(n.fancyTable.paginationElement):l(n).find(".pag");a.empty();for(var e,t=1;t<=n.fancyTable.pages;t++)(1==t||t>n.fancyTable.page-(s.pagClosest+1)&&t<n.fancyTable.page+(s.pagClosest+1)||t==n.fancyTable.pages)&&(e=l("<a>",{html:t,"data-n":t,style:"margin:0.2em",class:s.paginationClass+" "+(t==n.fancyTable.page?s.paginationClassActive:"")}).css("cursor","pointer").bind("click",function(){n.fancyTable.page=l(this).data("n"),o.tableUpdate(n)}),t==n.fancyTable.pages&&n.fancyTable.page<n.fancyTable.pages-s.pagClosest-1&&a.append(l("<span>...</span>")),a.append(e),1==t&&n.fancyTable.page>s.pagClosest+2&&a.append(l("<span>...</span>")))}s.onUpdate.call(this,n)},this.isSearchMatch=function(a,e){s.matchCase||(a=a.toUpperCase(),e=e.toUpperCase());var t=s.exactMatch;return"auto"==t&&e.match(/^\".*?\"$/)?(t=!0,e=e.substring(1,e.length-1)):t=!1,t?a==e:new RegExp(e).test(a)},this.reinit=function(a){l(this).each(function(){l(this).find("th a").contents().unwrap(),l(this).find("tr.fancySearchRow").remove()}),l(this).fancyTable(this.settings)},this.tableSort=function(t){var a,e;void 0!==t.fancyTable.sortColumn&&t.fancyTable.sortColumn<t.fancyTable.nColumns&&(a=0,l(t).find("thead th").each(function(){l(this).attr("aria-sort",a==t.fancyTable.sortColumn?1==t.fancyTable.sortOrder?"ascending":-1==t.fancyTable.sortOrder?"descending":"other":null),a++}),l(t).find("thead th div.sortArrow").each(function(){l(this).remove()}),(e=l("<div>",{class:"sortArrow"}).css({margin:"0.1em",display:"inline-block",width:0,height:0,"border-left":"0.4em solid transparent","border-right":"0.4em solid transparent"})).css(0<t.fancyTable.sortOrder?{"border-top":"0.4em solid #000"}:{"border-bottom":"0.4em solid #000"}),l(t).find("thead th a").eq(t.fancyTable.sortColumn).append(e),e=l(t).find("tbody tr").toArray().sort(function(a,e){a=l(a).find("td").eq(t.fancyTable.sortColumn),e=l(e).find("td").eq(t.fancyTable.sortColumn),a=l(a).data("sortvalue")?l(a).data("sortvalue"):a.html(),e=l(e).data("sortvalue")?l(e).data("sortvalue"):e.html();return"case-insensitive"==t.fancyTable.sortAs[t.fancyTable.sortColumn]&&(a=a.toLowerCase(),e=e.toLowerCase()),s.sortFunction.call(this,a,e,t.fancyTable)}),l(t).find("tbody").empty().append(e))},this.each(function(){if("TABLE"!==l(this).prop("tagName"))return console.warn("fancyTable: Element is not a table."),!0;var t,a,e,n,r=this;r.fancyTable={nColumns:l(r).find("td").first().parent().find("td").length,nRows:l(this).find("tbody tr").length,perPage:s.perPage,page:1,pages:0,matches:0,searchArr:[],search:"",sortColumn:s.sortColumn,sortOrder:void 0!==s.sortOrder&&(new RegExp("desc","i").test(s.sortOrder)||-1==s.sortOrder)?-1:1,sortAs:[],paginationElement:s.paginationElement},0==l(r).find("tbody").length&&(e=l(r).html(),l(r).empty(),l(r).append("<tbody>").append(l(e))),0==l(r).find("thead").length&&l(r).prepend(l("<thead>")),s.sortable&&(n=0,l(r).find("thead th").each(function(){r.fancyTable.sortAs.push("numeric"==l(this).data("sortas")?"numeric":"case-insensitive"==l(this).data("sortas")?"case-insensitive":null);var a=l(this).html(),a=l("<a>",{href:"#","aria-label":"Sort by "+l(this).text(),html:a,"data-n":n,class:""}).css({cursor:"pointer",color:"inherit","text-decoration":"none"}).bind("click",function(){return r.fancyTable.sortColumn==l(this).data("n")?r.fancyTable.sortOrder=-r.fancyTable.sortOrder:r.fancyTable.sortOrder=1,r.fancyTable.sortColumn=l(this).data("n"),o.tableSort(r),o.tableUpdate(r),!1});l(this).empty(),l(this).append(a),n++})),s.searchable&&(t=l("<tr>").addClass("fancySearchRow"),s.globalSearch?(a=l("<input>",{"aria-label":"Search table",placeholder:s.inputPlaceholder,style:"width:100%;box-sizing:border-box;"+s.inputStyle}).bind("change paste keyup",function(){r.fancyTable.search=l(this).val(),r.fancyTable.page=1,o.tableUpdate(r)}),e=l("<th>",{style:"padding:2px;"}).attr("colspan",r.fancyTable.nColumns),l(a).appendTo(l(e)),l(e).appendTo(l(t))):(n=0,l(r).find("td").first().parent().find("td").each(function(){r.fancyTable.searchArr.push("");var a=l("<input>",{"aria-label":"Search column","data-n":n,placeholder:s.inputPlaceholder,style:"width:100%;box-sizing:border-box;"+s.inputStyle}).bind("change paste keyup",function(){r.fancyTable.searchArr[l(this).data("n")]=l(this).val(),r.fancyTable.page=1,o.tableUpdate(r)}),e=l("<th>",{style:"padding:2px;"});l(a).appendTo(l(e)),l(e).appendTo(l(t)),n++})),t.appendTo(l(r).find("thead"))),o.tableSort(r),s.pagination&&!s.paginationElement&&(l(r).find("tfoot").remove(),l(r).append(l("<tfoot><tr></tr></tfoot>")),l(r).find("tfoot tr").append(l("<td class='pag'></td>",{}).attr("colspan",r.fancyTable.nColumns))),o.tableUpdate(r),s.onInit.call(this,r)}),this}}(jQuery);
+!function(i){i.fn.fancyTable=function(a){var o=i.extend({inputStyle:"",inputPlaceholder:"Search...",pagination:!1,paginationClass:"btn btn-light",paginationClassActive:"active",pagClosest:3,perPage:10,sortable:!0,searchable:!0,matchCase:!1,exactMatch:!1,localeCompare:!1,onInit:function(){},beforeUpdate:function(){},onUpdate:function(){},sortFunction:function(a,e,t,n,r){return a==e&&n&&r?t.rowSortOrder[i(n).data("rowid")]>t.rowSortOrder[i(r).data("rowid")]:"numeric"==t.sortAs[t.sortColumn]?0<t.sortOrder?(parseFloat(a)||0)-(parseFloat(e)||0):(parseFloat(e)||0)-(parseFloat(a)||0):o.localeCompare?a.localeCompare(e)<0?-t.sortOrder:0<a.localeCompare(e)?t.sortOrder:0:a<e?-t.sortOrder:e<a?t.sortOrder:0},testing:!1},a),l=this;return this.settings=o,this.tableUpdate=function(n){if(o.beforeUpdate.call(this,n),n.fancyTable.matches=0,i(n).find("tbody tr").each(function(){var a=0,e=!0,t=!1;i(this).find("td").each(function(){o.globalSearch||!n.fancyTable.searchArr[a]||l.isSearchMatch(i(this).html(),n.fancyTable.searchArr[a])?!o.globalSearch||n.fancyTable.search&&!l.isSearchMatch(i(this).html(),n.fancyTable.search)||Array.isArray(o.globalSearchExcludeColumns)&&o.globalSearchExcludeColumns.includes(a+1)||(t=!0):e=!1,a++}),o.globalSearch&&t||!o.globalSearch&&e?(n.fancyTable.matches++,!o.pagination||n.fancyTable.matches>n.fancyTable.perPage*(n.fancyTable.page-1)&&n.fancyTable.matches<=n.fancyTable.perPage*n.fancyTable.page?i(this).show():i(this).hide()):i(this).hide()}),n.fancyTable.pages=Math.ceil(n.fancyTable.matches/n.fancyTable.perPage),o.pagination){var a=n.fancyTable.paginationElement?i(n.fancyTable.paginationElement):i(n).find(".pag");a.empty();for(var e,t=1;t<=n.fancyTable.pages;t++)(1==t||t>n.fancyTable.page-(o.pagClosest+1)&&t<n.fancyTable.page+(o.pagClosest+1)||t==n.fancyTable.pages)&&(e=i("<a>",{html:t,"data-n":t,style:"margin:0.2em",class:o.paginationClass+" "+(t==n.fancyTable.page?o.paginationClassActive:"")}).css("cursor","pointer").bind("click",function(){n.fancyTable.page=i(this).data("n"),l.tableUpdate(n)}),t==n.fancyTable.pages&&n.fancyTable.page<n.fancyTable.pages-o.pagClosest-1&&a.append(i("<span>...</span>")),a.append(e),1==t&&n.fancyTable.page>o.pagClosest+2&&a.append(i("<span>...</span>")))}o.onUpdate.call(this,n)},this.isSearchMatch=function(a,e){if(o.matchCase||(a=a.toUpperCase(),e=e.toUpperCase()),"auto"==o.exactMatch&&e.match(/^".*?"$/))return a==(e=e.substring(1,e.length-1));if("auto"==o.exactMatch&&e.replace(/\s+/g,"").match(/^[<>]=?/)){var t=e.replace(/\s+/g,"").match(/^[<>]=?/)[0],n=e.replace(/\s+/g,"").substring(t.length);return">"==t&&+n<+a||"<"==t&&+a<+n||">="==t&&+n<=+a||"<="==t&&+a<=+n}if("auto"==o.exactMatch&&e.replace(/\s+/g,"").match(/^.+(\.\.|-).+$/)){n=e.replace(/\s+/g,"").split(/\.\.|-/);return+a>=+n[0]&&+a<=+n[1]}try{return!0===o.exactMatch?a==e:new RegExp(e).test(a)}catch{return!1}},this.reinit=function(){i(this).each(function(){i(this).find("th a").contents().unwrap(),i(this).find("tr.fancySearchRow").remove()}),i(this).fancyTable(this.settings)},this.tableSort=function(r){var a,e;void 0!==r.fancyTable.sortColumn&&r.fancyTable.sortColumn<r.fancyTable.nColumns&&(a=0,i(r).find("thead th").each(function(){i(this).attr("aria-sort",a==r.fancyTable.sortColumn?1==r.fancyTable.sortOrder?"ascending":-1==r.fancyTable.sortOrder?"descending":"other":null),a++}),i(r).find("thead th div.sortArrow").each(function(){i(this).remove()}),(e=i("<div>",{class:"sortArrow"}).css({margin:"0.1em",display:"inline-block",width:0,height:0,"border-left":"0.4em solid transparent","border-right":"0.4em solid transparent"})).css(0<r.fancyTable.sortOrder?{"border-top":"0.4em solid #000"}:{"border-bottom":"0.4em solid #000"}),i(r).find("thead th a").eq(r.fancyTable.sortColumn).append(e),e=i(r).find("tbody tr").toArray().sort(function(a,e){var t=i(a).find("td").eq(r.fancyTable.sortColumn),n=i(e).find("td").eq(r.fancyTable.sortColumn),t=i(t).attr("data-sortvalue")?i(t).data("sortvalue"):t.html(),n=i(n).attr("data-sortvalue")?i(n).data("sortvalue"):n.html();return"case-insensitive"==r.fancyTable.sortAs[r.fancyTable.sortColumn]&&(t=t.toLowerCase(),n=n.toLowerCase()),o.sortFunction.call(this,t,n,r.fancyTable,a,e)}),i(e).each(function(a){r.fancyTable.rowSortOrder[i(this).data("rowid")]=a}),i(r).find("tbody").empty().append(e))},this.each(function(){if("TABLE"!==i(this).prop("tagName"))return console.warn("fancyTable: Element is not a table."),!0;var e,t,a,n,r,s=this;s.fancyTable={nColumns:i(s).find("td").first().parent().find("td").length,nRows:i(this).find("tbody tr").length,perPage:o.perPage,page:1,pages:0,matches:0,searchArr:[],search:"",sortColumn:o.sortColumn,sortOrder:void 0!==o.sortOrder&&(new RegExp("desc","i").test(o.sortOrder)||-1==o.sortOrder)?-1:1,sortAs:[],paginationElement:o.paginationElement},s.fancyTable.rowSortOrder=new Array(s.fancyTable.nRows),0==i(s).find("tbody").length&&(n=i(s).html(),i(s).empty(),i(s).append("<tbody>").append(i(n))),0==i(s).find("thead").length&&i(s).prepend(i("<thead>")),i(s).find("tbody tr").each(function(a){i(this).data("rowid",a)}),o.sortable&&(e=0,i(s).find("thead th").each(function(){s.fancyTable.sortAs.push("numeric"==i(this).data("sortas")?"numeric":"case-insensitive"==i(this).data("sortas")?"case-insensitive":null);var a=i(this).html(),a=i("<a>",{href:"#","aria-label":"Sort by "+i(this).text(),html:a,"data-n":e,class:""}).css({cursor:"pointer",color:"inherit","text-decoration":"none","white-space":"nowrap"}).bind("click",function(){return s.fancyTable.sortColumn==i(this).data("n")?s.fancyTable.sortOrder=-s.fancyTable.sortOrder:s.fancyTable.sortOrder=1,s.fancyTable.sortColumn=i(this).data("n"),l.tableSort(s),l.tableUpdate(s),!1});i(this).empty(),i(this).append(a),e++})),o.searchable&&(t=i("<tr>").addClass("fancySearchRow"),o.globalSearch?(a=i("<input>",{"aria-label":"Search table",placeholder:o.inputPlaceholder,style:"width:100%;box-sizing:border-box;"+o.inputStyle}).bind("change paste keyup",function(){s.fancyTable.search=i(this).val(),s.fancyTable.page=1,l.tableUpdate(s)}),n=i("<th>",{style:"padding:2px;"}).attr("colspan",s.fancyTable.nColumns),i(a).appendTo(i(n)),i(n).appendTo(i(t))):(r=0,i(s).find("td").first().parent().find("td").each(function(){s.fancyTable.searchArr.push("");var a=i("<input>",{"aria-label":"Search column","data-n":r,placeholder:o.inputPlaceholder,style:"width:100%;box-sizing:border-box;"+o.inputStyle}).bind("change paste keyup",function(){s.fancyTable.searchArr[i(this).data("n")]=i(this).val(),s.fancyTable.page=1,l.tableUpdate(s)}),e=i("<th>",{style:"padding:2px;"});i(a).appendTo(i(e)),i(e).appendTo(i(t)),r++})),t.appendTo(i(s).find("thead"))),l.tableSort(s),o.pagination&&!o.paginationElement&&(i(s).find("tfoot").remove(),i(s).append(i("<tfoot><tr></tr></tfoot>")),i(s).find("tfoot tr").append(i("<td class='pag'></td>",{}).attr("colspan",s.fancyTable.nColumns))),l.tableUpdate(s),o.onInit.call(this,s)}),this}}(jQuery);
diff --git a/javascript/generated/popper.js@1.14.3 b/javascript/generated/popper.js@1.14.3
new file mode 100644
--- /dev/null
+++ b/javascript/generated/popper.js@1.14.3
@@ -0,0 +1,5 @@
+/*
+ Copyright (C) Federico Zivolo 2018
+ Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT).
+ */(function(e,t){'object'==typeof exports&&'undefined'!=typeof module?module.exports=t():'function'==typeof define&&define.amd?define(t):e.Popper=t()})(this,function(){'use strict';function e(e){return e&&'[object Function]'==={}.toString.call(e)}function t(e,t){if(1!==e.nodeType)return[];var o=getComputedStyle(e,null);return t?o[t]:o}function o(e){return'HTML'===e.nodeName?e:e.parentNode||e.host}function n(e){if(!e)return document.body;switch(e.nodeName){case'HTML':case'BODY':return e.ownerDocument.body;case'#document':return e.body;}var i=t(e),r=i.overflow,p=i.overflowX,s=i.overflowY;return /(auto|scroll|overlay)/.test(r+s+p)?e:n(o(e))}function r(e){return 11===e?re:10===e?pe:re||pe}function p(e){if(!e)return document.documentElement;for(var o=r(10)?document.body:null,n=e.offsetParent;n===o&&e.nextElementSibling;)n=(e=e.nextElementSibling).offsetParent;var i=n&&n.nodeName;return i&&'BODY'!==i&&'HTML'!==i?-1!==['TD','TABLE'].indexOf(n.nodeName)&&'static'===t(n,'position')?p(n):n:e?e.ownerDocument.documentElement:document.documentElement}function s(e){var t=e.nodeName;return'BODY'!==t&&('HTML'===t||p(e.firstElementChild)===e)}function d(e){return null===e.parentNode?e:d(e.parentNode)}function a(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return document.documentElement;var o=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,n=o?e:t,i=o?t:e,r=document.createRange();r.setStart(n,0),r.setEnd(i,0);var l=r.commonAncestorContainer;if(e!==l&&t!==l||n.contains(i))return s(l)?l:p(l);var f=d(e);return f.host?a(f.host,t):a(e,d(t).host)}function l(e){var t=1<arguments.length&&void 0!==arguments[1]?arguments[1]:'top',o='top'===t?'scrollTop':'scrollLeft',n=e.nodeName;if('BODY'===n||'HTML'===n){var i=e.ownerDocument.documentElement,r=e.ownerDocument.scrollingElement||i;return r[o]}return e[o]}function f(e,t){var o=2<arguments.length&&void 0!==arguments[2]&&arguments[2],n=l(t,'top'),i=l(t,'left'),r=o?-1:1;return e.top+=n*r,e.bottom+=n*r,e.left+=i*r,e.right+=i*r,e}function m(e,t){var o='x'===t?'Left':'Top',n='Left'==o?'Right':'Bottom';return parseFloat(e['border'+o+'Width'],10)+parseFloat(e['border'+n+'Width'],10)}function h(e,t,o,n){return $(t['offset'+e],t['scroll'+e],o['client'+e],o['offset'+e],o['scroll'+e],r(10)?o['offset'+e]+n['margin'+('Height'===e?'Top':'Left')]+n['margin'+('Height'===e?'Bottom':'Right')]:0)}function c(){var e=document.body,t=document.documentElement,o=r(10)&&getComputedStyle(t);return{height:h('Height',e,t,o),width:h('Width',e,t,o)}}function g(e){return le({},e,{right:e.left+e.width,bottom:e.top+e.height})}function u(e){var o={};try{if(r(10)){o=e.getBoundingClientRect();var n=l(e,'top'),i=l(e,'left');o.top+=n,o.left+=i,o.bottom+=n,o.right+=i}else o=e.getBoundingClientRect()}catch(t){}var p={left:o.left,top:o.top,width:o.right-o.left,height:o.bottom-o.top},s='HTML'===e.nodeName?c():{},d=s.width||e.clientWidth||p.right-p.left,a=s.height||e.clientHeight||p.bottom-p.top,f=e.offsetWidth-d,h=e.offsetHeight-a;if(f||h){var u=t(e);f-=m(u,'x'),h-=m(u,'y'),p.width-=f,p.height-=h}return g(p)}function b(e,o){var i=2<arguments.length&&void 0!==arguments[2]&&arguments[2],p=r(10),s='HTML'===o.nodeName,d=u(e),a=u(o),l=n(e),m=t(o),h=parseFloat(m.borderTopWidth,10),c=parseFloat(m.borderLeftWidth,10);i&&'HTML'===o.nodeName&&(a.top=$(a.top,0),a.left=$(a.left,0));var b=g({top:d.top-a.top-h,left:d.left-a.left-c,width:d.width,height:d.height});if(b.marginTop=0,b.marginLeft=0,!p&&s){var y=parseFloat(m.marginTop,10),w=parseFloat(m.marginLeft,10);b.top-=h-y,b.bottom-=h-y,b.left-=c-w,b.right-=c-w,b.marginTop=y,b.marginLeft=w}return(p&&!i?o.contains(l):o===l&&'BODY'!==l.nodeName)&&(b=f(b,o)),b}function y(e){var t=1<arguments.length&&void 0!==arguments[1]&&arguments[1],o=e.ownerDocument.documentElement,n=b(e,o),i=$(o.clientWidth,window.innerWidth||0),r=$(o.clientHeight,window.innerHeight||0),p=t?0:l(o),s=t?0:l(o,'left'),d={top:p-n.top+n.marginTop,left:s-n.left+n.marginLeft,width:i,height:r};return g(d)}function w(e){var n=e.nodeName;return'BODY'===n||'HTML'===n?!1:'fixed'===t(e,'position')||w(o(e))}function E(e){if(!e||!e.parentElement||r())return document.documentElement;for(var o=e.parentElement;o&&'none'===t(o,'transform');)o=o.parentElement;return o||document.documentElement}function v(e,t,i,r){var p=4<arguments.length&&void 0!==arguments[4]&&arguments[4],s={top:0,left:0},d=p?E(e):a(e,t);if('viewport'===r)s=y(d,p);else{var l;'scrollParent'===r?(l=n(o(t)),'BODY'===l.nodeName&&(l=e.ownerDocument.documentElement)):'window'===r?l=e.ownerDocument.documentElement:l=r;var f=b(l,d,p);if('HTML'===l.nodeName&&!w(d)){var m=c(),h=m.height,g=m.width;s.top+=f.top-f.marginTop,s.bottom=h+f.top,s.left+=f.left-f.marginLeft,s.right=g+f.left}else s=f}return s.left+=i,s.top+=i,s.right-=i,s.bottom-=i,s}function x(e){var t=e.width,o=e.height;return t*o}function O(e,t,o,n,i){var r=5<arguments.length&&void 0!==arguments[5]?arguments[5]:0;if(-1===e.indexOf('auto'))return e;var p=v(o,n,r,i),s={top:{width:p.width,height:t.top-p.top},right:{width:p.right-t.right,height:p.height},bottom:{width:p.width,height:p.bottom-t.bottom},left:{width:t.left-p.left,height:p.height}},d=Object.keys(s).map(function(e){return le({key:e},s[e],{area:x(s[e])})}).sort(function(e,t){return t.area-e.area}),a=d.filter(function(e){var t=e.width,n=e.height;return t>=o.clientWidth&&n>=o.clientHeight}),l=0<a.length?a[0].key:d[0].key,f=e.split('-')[1];return l+(f?'-'+f:'')}function L(e,t,o){var n=3<arguments.length&&void 0!==arguments[3]?arguments[3]:null,i=n?E(t):a(t,o);return b(o,i,n)}function S(e){var t=getComputedStyle(e),o=parseFloat(t.marginTop)+parseFloat(t.marginBottom),n=parseFloat(t.marginLeft)+parseFloat(t.marginRight),i={width:e.offsetWidth+n,height:e.offsetHeight+o};return i}function T(e){var t={left:'right',right:'left',bottom:'top',top:'bottom'};return e.replace(/left|right|bottom|top/g,function(e){return t[e]})}function C(e,t,o){o=o.split('-')[0];var n=S(e),i={width:n.width,height:n.height},r=-1!==['right','left'].indexOf(o),p=r?'top':'left',s=r?'left':'top',d=r?'height':'width',a=r?'width':'height';return i[p]=t[p]+t[d]/2-n[d]/2,i[s]=o===s?t[s]-n[a]:t[T(s)],i}function D(e,t){return Array.prototype.find?e.find(t):e.filter(t)[0]}function N(e,t,o){if(Array.prototype.findIndex)return e.findIndex(function(e){return e[t]===o});var n=D(e,function(e){return e[t]===o});return e.indexOf(n)}function P(t,o,n){var i=void 0===n?t:t.slice(0,N(t,'name',n));return i.forEach(function(t){t['function']&&console.warn('`modifier.function` is deprecated, use `modifier.fn`!');var n=t['function']||t.fn;t.enabled&&e(n)&&(o.offsets.popper=g(o.offsets.popper),o.offsets.reference=g(o.offsets.reference),o=n(o,t))}),o}function k(){if(!this.state.isDestroyed){var e={instance:this,styles:{},arrowStyles:{},attributes:{},flipped:!1,offsets:{}};e.offsets.reference=L(this.state,this.popper,this.reference,this.options.positionFixed),e.placement=O(this.options.placement,e.offsets.reference,this.popper,this.reference,this.options.modifiers.flip.boundariesElement,this.options.modifiers.flip.padding),e.originalPlacement=e.placement,e.positionFixed=this.options.positionFixed,e.offsets.popper=C(this.popper,e.offsets.reference,e.placement),e.offsets.popper.position=this.options.positionFixed?'fixed':'absolute',e=P(this.modifiers,e),this.state.isCreated?this.options.onUpdate(e):(this.state.isCreated=!0,this.options.onCreate(e))}}function W(e,t){return e.some(function(e){var o=e.name,n=e.enabled;return n&&o===t})}function B(e){for(var t=[!1,'ms','Webkit','Moz','O'],o=e.charAt(0).toUpperCase()+e.slice(1),n=0;n<t.length;n++){var i=t[n],r=i?''+i+o:e;if('undefined'!=typeof document.body.style[r])return r}return null}function H(){return this.state.isDestroyed=!0,W(this.modifiers,'applyStyle')&&(this.popper.removeAttribute('x-placement'),this.popper.style.position='',this.popper.style.top='',this.popper.style.left='',this.popper.style.right='',this.popper.style.bottom='',this.popper.style.willChange='',this.popper.style[B('transform')]=''),this.disableEventListeners(),this.options.removeOnDestroy&&this.popper.parentNode.removeChild(this.popper),this}function A(e){var t=e.ownerDocument;return t?t.defaultView:window}function M(e,t,o,i){var r='BODY'===e.nodeName,p=r?e.ownerDocument.defaultView:e;p.addEventListener(t,o,{passive:!0}),r||M(n(p.parentNode),t,o,i),i.push(p)}function I(e,t,o,i){o.updateBound=i,A(e).addEventListener('resize',o.updateBound,{passive:!0});var r=n(e);return M(r,'scroll',o.updateBound,o.scrollParents),o.scrollElement=r,o.eventsEnabled=!0,o}function F(){this.state.eventsEnabled||(this.state=I(this.reference,this.options,this.state,this.scheduleUpdate))}function R(e,t){return A(e).removeEventListener('resize',t.updateBound),t.scrollParents.forEach(function(e){e.removeEventListener('scroll',t.updateBound)}),t.updateBound=null,t.scrollParents=[],t.scrollElement=null,t.eventsEnabled=!1,t}function U(){this.state.eventsEnabled&&(cancelAnimationFrame(this.scheduleUpdate),this.state=R(this.reference,this.state))}function Y(e){return''!==e&&!isNaN(parseFloat(e))&&isFinite(e)}function j(e,t){Object.keys(t).forEach(function(o){var n='';-1!==['width','height','top','right','bottom','left'].indexOf(o)&&Y(t[o])&&(n='px'),e.style[o]=t[o]+n})}function K(e,t){Object.keys(t).forEach(function(o){var n=t[o];!1===n?e.removeAttribute(o):e.setAttribute(o,t[o])})}function q(e,t,o){var n=D(e,function(e){var o=e.name;return o===t}),i=!!n&&e.some(function(e){return e.name===o&&e.enabled&&e.order<n.order});if(!i){var r='`'+t+'`';console.warn('`'+o+'`'+' modifier is required by '+r+' modifier in order to work, be sure to include it before '+r+'!')}return i}function G(e){return'end'===e?'start':'start'===e?'end':e}function z(e){var t=1<arguments.length&&void 0!==arguments[1]&&arguments[1],o=me.indexOf(e),n=me.slice(o+1).concat(me.slice(0,o));return t?n.reverse():n}function V(e,t,o,n){var i=e.match(/((?:\-|\+)?\d*\.?\d*)(.*)/),r=+i[1],p=i[2];if(!r)return e;if(0===p.indexOf('%')){var s;switch(p){case'%p':s=o;break;case'%':case'%r':default:s=n;}var d=g(s);return d[t]/100*r}if('vh'===p||'vw'===p){var a;return a='vh'===p?$(document.documentElement.clientHeight,window.innerHeight||0):$(document.documentElement.clientWidth,window.innerWidth||0),a/100*r}return r}function _(e,t,o,n){var i=[0,0],r=-1!==['right','left'].indexOf(n),p=e.split(/(\+|\-)/).map(function(e){return e.trim()}),s=p.indexOf(D(p,function(e){return-1!==e.search(/,|\s/)}));p[s]&&-1===p[s].indexOf(',')&&console.warn('Offsets separated by white space(s) are deprecated, use a comma (,) instead.');var d=/\s*,\s*|\s+/,a=-1===s?[p]:[p.slice(0,s).concat([p[s].split(d)[0]]),[p[s].split(d)[1]].concat(p.slice(s+1))];return a=a.map(function(e,n){var i=(1===n?!r:r)?'height':'width',p=!1;return e.reduce(function(e,t){return''===e[e.length-1]&&-1!==['+','-'].indexOf(t)?(e[e.length-1]=t,p=!0,e):p?(e[e.length-1]+=t,p=!1,e):e.concat(t)},[]).map(function(e){return V(e,i,t,o)})}),a.forEach(function(e,t){e.forEach(function(o,n){Y(o)&&(i[t]+=o*('-'===e[n-1]?-1:1))})}),i}function X(e,t){var o,n=t.offset,i=e.placement,r=e.offsets,p=r.popper,s=r.reference,d=i.split('-')[0];return o=Y(+n)?[+n,0]:_(n,p,s,d),'left'===d?(p.top+=o[0],p.left-=o[1]):'right'===d?(p.top+=o[0],p.left+=o[1]):'top'===d?(p.left+=o[0],p.top-=o[1]):'bottom'===d&&(p.left+=o[0],p.top+=o[1]),e.popper=p,e}for(var J=Math.min,Q=Math.round,Z=Math.floor,$=Math.max,ee='undefined'!=typeof window&&'undefined'!=typeof document,te=['Edge','Trident','Firefox'],oe=0,ne=0;ne<te.length;ne+=1)if(ee&&0<=navigator.userAgent.indexOf(te[ne])){oe=1;break}var i=ee&&window.Promise,ie=i?function(e){var t=!1;return function(){t||(t=!0,window.Promise.resolve().then(function(){t=!1,e()}))}}:function(e){var t=!1;return function(){t||(t=!0,setTimeout(function(){t=!1,e()},oe))}},re=ee&&!!(window.MSInputMethodContext&&document.documentMode),pe=ee&&/MSIE 10/.test(navigator.userAgent),se=function(e,t){if(!(e instanceof t))throw new TypeError('Cannot call a class as a function')},de=function(){function e(e,t){for(var o,n=0;n<t.length;n++)o=t[n],o.enumerable=o.enumerable||!1,o.configurable=!0,'value'in o&&(o.writable=!0),Object.defineProperty(e,o.key,o)}return function(t,o,n){return o&&e(t.prototype,o),n&&e(t,n),t}}(),ae=function(e,t,o){return t in e?Object.defineProperty(e,t,{value:o,enumerable:!0,configurable:!0,writable:!0}):e[t]=o,e},le=Object.assign||function(e){for(var t,o=1;o<arguments.length;o++)for(var n in t=arguments[o],t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e},fe=['auto-start','auto','auto-end','top-start','top','top-end','right-start','right','right-end','bottom-end','bottom','bottom-start','left-end','left','left-start'],me=fe.slice(3),he={FLIP:'flip',CLOCKWISE:'clockwise',COUNTERCLOCKWISE:'counterclockwise'},ce=function(){function t(o,n){var i=this,r=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{};se(this,t),this.scheduleUpdate=function(){return requestAnimationFrame(i.update)},this.update=ie(this.update.bind(this)),this.options=le({},t.Defaults,r),this.state={isDestroyed:!1,isCreated:!1,scrollParents:[]},this.reference=o&&o.jquery?o[0]:o,this.popper=n&&n.jquery?n[0]:n,this.options.modifiers={},Object.keys(le({},t.Defaults.modifiers,r.modifiers)).forEach(function(e){i.options.modifiers[e]=le({},t.Defaults.modifiers[e]||{},r.modifiers?r.modifiers[e]:{})}),this.modifiers=Object.keys(this.options.modifiers).map(function(e){return le({name:e},i.options.modifiers[e])}).sort(function(e,t){return e.order-t.order}),this.modifiers.forEach(function(t){t.enabled&&e(t.onLoad)&&t.onLoad(i.reference,i.popper,i.options,t,i.state)}),this.update();var p=this.options.eventsEnabled;p&&this.enableEventListeners(),this.state.eventsEnabled=p}return de(t,[{key:'update',value:function(){return k.call(this)}},{key:'destroy',value:function(){return H.call(this)}},{key:'enableEventListeners',value:function(){return F.call(this)}},{key:'disableEventListeners',value:function(){return U.call(this)}}]),t}();return ce.Utils=('undefined'==typeof window?global:window).PopperUtils,ce.placements=fe,ce.Defaults={placement:'bottom',positionFixed:!1,eventsEnabled:!0,removeOnDestroy:!1,onCreate:function(){},onUpdate:function(){},modifiers:{shift:{order:100,enabled:!0,fn:function(e){var t=e.placement,o=t.split('-')[0],n=t.split('-')[1];if(n){var i=e.offsets,r=i.reference,p=i.popper,s=-1!==['bottom','top'].indexOf(o),d=s?'left':'top',a=s?'width':'height',l={start:ae({},d,r[d]),end:ae({},d,r[d]+r[a]-p[a])};e.offsets.popper=le({},p,l[n])}return e}},offset:{order:200,enabled:!0,fn:X,offset:0},preventOverflow:{order:300,enabled:!0,fn:function(e,t){var o=t.boundariesElement||p(e.instance.popper);e.instance.reference===o&&(o=p(o));var n=B('transform'),i=e.instance.popper.style,r=i.top,s=i.left,d=i[n];i.top='',i.left='',i[n]='';var a=v(e.instance.popper,e.instance.reference,t.padding,o,e.positionFixed);i.top=r,i.left=s,i[n]=d,t.boundaries=a;var l=t.priority,f=e.offsets.popper,m={primary:function(e){var o=f[e];return f[e]<a[e]&&!t.escapeWithReference&&(o=$(f[e],a[e])),ae({},e,o)},secondary:function(e){var o='right'===e?'left':'top',n=f[o];return f[e]>a[e]&&!t.escapeWithReference&&(n=J(f[o],a[e]-('right'===e?f.width:f.height))),ae({},o,n)}};return l.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';f=le({},f,m[t](e))}),e.offsets.popper=f,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,o=t.popper,n=t.reference,i=e.placement.split('-')[0],r=Z,p=-1!==['top','bottom'].indexOf(i),s=p?'right':'bottom',d=p?'left':'top',a=p?'width':'height';return o[s]<r(n[d])&&(e.offsets.popper[d]=r(n[d])-o[a]),o[d]>r(n[s])&&(e.offsets.popper[d]=r(n[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,o){var n;if(!q(e.instance.modifiers,'arrow','keepTogether'))return e;var i=o.element;if('string'==typeof i){if(i=e.instance.popper.querySelector(i),!i)return e;}else if(!e.instance.popper.contains(i))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var r=e.placement.split('-')[0],p=e.offsets,s=p.popper,d=p.reference,a=-1!==['left','right'].indexOf(r),l=a?'height':'width',f=a?'Top':'Left',m=f.toLowerCase(),h=a?'left':'top',c=a?'bottom':'right',u=S(i)[l];d[c]-u<s[m]&&(e.offsets.popper[m]-=s[m]-(d[c]-u)),d[m]+u>s[c]&&(e.offsets.popper[m]+=d[m]+u-s[c]),e.offsets.popper=g(e.offsets.popper);var b=d[m]+d[l]/2-u/2,y=t(e.instance.popper),w=parseFloat(y['margin'+f],10),E=parseFloat(y['border'+f+'Width'],10),v=b-e.offsets.popper[m]-w-E;return v=$(J(s[l]-u,v),0),e.arrowElement=i,e.offsets.arrow=(n={},ae(n,m,Q(v)),ae(n,h,''),n),e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(W(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var o=v(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement,e.positionFixed),n=e.placement.split('-')[0],i=T(n),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case he.FLIP:p=[n,i];break;case he.CLOCKWISE:p=z(n);break;case he.COUNTERCLOCKWISE:p=z(n,!0);break;default:p=t.behavior;}return p.forEach(function(s,d){if(n!==s||p.length===d+1)return e;n=e.placement.split('-')[0],i=T(n);var a=e.offsets.popper,l=e.offsets.reference,f=Z,m='left'===n&&f(a.right)>f(l.left)||'right'===n&&f(a.left)<f(l.right)||'top'===n&&f(a.bottom)>f(l.top)||'bottom'===n&&f(a.top)<f(l.bottom),h=f(a.left)<f(o.left),c=f(a.right)>f(o.right),g=f(a.top)<f(o.top),u=f(a.bottom)>f(o.bottom),b='left'===n&&h||'right'===n&&c||'top'===n&&g||'bottom'===n&&u,y=-1!==['top','bottom'].indexOf(n),w=!!t.flipVariations&&(y&&'start'===r&&h||y&&'end'===r&&c||!y&&'start'===r&&g||!y&&'end'===r&&u);(m||b||w)&&(e.flipped=!0,(m||b)&&(n=p[d+1]),w&&(r=G(r)),e.placement=n+(r?'-'+r:''),e.offsets.popper=le({},e.offsets.popper,C(e.instance.popper,e.offsets.reference,e.placement)),e=P(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport'},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,o=t.split('-')[0],n=e.offsets,i=n.popper,r=n.reference,p=-1!==['left','right'].indexOf(o),s=-1===['top','left'].indexOf(o);return i[p?'left':'top']=r[o]-(s?i[p?'width':'height']:0),e.placement=T(t),e.offsets.popper=g(i),e}},hide:{order:800,enabled:!0,fn:function(e){if(!q(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,o=D(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottom<o.top||t.left>o.right||t.top>o.bottom||t.right<o.left){if(!0===e.hide)return e;e.hide=!0,e.attributes['x-out-of-boundaries']=''}else{if(!1===e.hide)return e;e.hide=!1,e.attributes['x-out-of-boundaries']=!1}return e}},computeStyle:{order:850,enabled:!0,fn:function(e,t){var o=t.x,n=t.y,i=e.offsets.popper,r=D(e.instance.modifiers,function(e){return'applyStyle'===e.name}).gpuAcceleration;void 0!==r&&console.warn('WARNING: `gpuAcceleration` option moved to `computeStyle` modifier and will not be supported in future versions of Popper.js!');var s,d,a=void 0===r?t.gpuAcceleration:r,l=p(e.instance.popper),f=u(l),m={position:i.position},h={left:Z(i.left),top:Q(i.top),bottom:Q(i.bottom),right:Z(i.right)},c='bottom'===o?'top':'bottom',g='right'===n?'left':'right',b=B('transform');if(d='bottom'==c?-f.height+h.bottom:h.top,s='right'==g?-f.width+h.right:h.left,a&&b)m[b]='translate3d('+s+'px, '+d+'px, 0)',m[c]=0,m[g]=0,m.willChange='transform';else{var y='bottom'==c?-1:1,w='right'==g?-1:1;m[c]=d*y,m[g]=s*w,m.willChange=c+', '+g}var E={"x-placement":e.placement};return e.attributes=le({},E,e.attributes),e.styles=le({},m,e.styles),e.arrowStyles=le({},e.offsets.arrow,e.arrowStyles),e},gpuAcceleration:!0,x:'bottom',y:'right'},applyStyle:{order:900,enabled:!0,fn:function(e){return j(e.instance.popper,e.styles),K(e.instance.popper,e.attributes),e.arrowElement&&Object.keys(e.arrowStyles).length&&j(e.arrowElement,e.arrowStyles),e},onLoad:function(e,t,o,n,i){var r=L(i,t,e,o.positionFixed),p=O(o.placement,r,t,e,o.modifiers.flip.boundariesElement,o.modifiers.flip.padding);return t.setAttribute('x-placement',p),j(t,{position:o.positionFixed?'fixed':'absolute'}),o},gpuAcceleration:void 0}}},ce});
+//# sourceMappingURL=popper.min.js.map
diff --git a/javascript/stylesheet.css b/javascript/stylesheet.css
--- a/javascript/stylesheet.css
+++ b/javascript/stylesheet.css
@@ -9,9 +9,7 @@
   background-color: transparent;
   color: black;
 }
-.container { width: 90%;
-           ; max-width: 90%;
-           }
+.container-fluid { padding: 1em 2em; }
 
 /* Reset milligram table styling so tooltips render correctly */
 table, caption, tbody, tfoot, thead, tr, th, td {
@@ -51,4 +49,16 @@
 
 .cheader{
   margin-left:10px;
+}
+
+h1 {
+  font-size: 1em;
+}
+
+.not-available {
+  color: grey;
+}
+
+.custom-tab {
+  margin-top: 1em;
 }
diff --git a/javascript/tablogic.js b/javascript/tablogic.js
--- a/javascript/tablogic.js
+++ b/javascript/tablogic.js
@@ -1,26 +1,5 @@
-function changeTab(tabName,elmnt) {
-    var i, tabviz,  tablinks;
-
-    // Hide all elements with class="tabviz" by default */
-    tabviz = document.getElementsByClassName("tabviz");
-    for (i = 0; i < tabviz.length; i++) {
-	    tabviz[i].style.display = "none";
-    }
-
-    // Remove the background color of all tablinks/buttons
-    tablinks = document.getElementsByClassName("tablink");
-    for (i = 0; i < tablinks.length; i++) {
-	    tablinks[i].classList.remove('button-outline');
-    }
-
-    // Show the specific tab content
-    document.getElementById(tabName).style.display = "block";
-
-    // Add the specific color to the button used to open the tab content
-    elmnt.classList.add('button-outline');
-    $.sparkline_display_visible()
-
-}
-
-// Get the element with id="defaultOpen" and click on it
-document.getElementById("defaultOpen").click();
+$(document).ready(function () {
+    $("#closures-tab").on("shown.bs.tab", function (e) {
+        $.sparkline_display_visible();
+    });
+});
diff --git a/main/Main.hs b/main/Main.hs
--- a/main/Main.hs
+++ b/main/Main.hs
@@ -17,7 +17,6 @@
 import Eventlog.Data
 import Eventlog.Types
 import Paths_eventlog2html (version)
-import Eventlog.Ticky
 
 main :: IO ()
 main = do
@@ -58,17 +57,13 @@
 
 doOneJson :: Args -> FilePath -> FilePath -> IO ()
 doOneJson a fin fout = do
-  HeapProfile (_, val, _, _) <- generateJson fin a
+  Just (HeapProfileData val _ _) <- eventlogHeapProfile <$> generateJson fin a
   encodeFile fout val
 
 doOneHtml :: Args -> FilePath -> FilePath -> IO ()
 doOneHtml a fin fout = do
   prof_type <- generateJsonValidate checkTraces fin a
-  let html = case prof_type of
-                HeapProfile (header, data_json, descs, closure_descs) ->
-                 templateString header data_json descs closure_descs a
-                TickyProfile (header, tallocs, ticked_per, dat) ->
-                  tickyTemplateString header tallocs ticked_per dat a
+  let html = templateString prof_type a
   writeFile fout html
   where
     checkTraces :: ProfData -> IO ()
diff --git a/src/Eventlog/Args.hs b/src/Eventlog/Args.hs
--- a/src/Eventlog/Args.hs
+++ b/src/Eventlog/Args.hs
@@ -21,7 +21,7 @@
   = ShowVersion
   | Run Args
 
-data Sort = Size | StdDev | Name | Gradient
+data Sort = Size | StdDev | Name | Gradient | Number
 
 data Args = Args
   {
@@ -47,7 +47,7 @@
 argParser = Run <$> (Args
       <$> option parseSort
           ( long "sort"
-         <> help "How to sort the bands.  One of: size (default), stddev, name, gradient."
+         <> help "How to sort the bands.  One of: size (default), stddev, name, number, gradient."
          <> value Size
          <> metavar "FIELD" )
       <*> switch
@@ -118,8 +118,9 @@
   "size" -> Right Size
   "stddev" -> Right StdDev
   "name" -> Right Name
+  "number" -> Right Number
   "gradient" -> Right Gradient
-  _ -> Left "expected one of: size, stddev, name"
+  _ -> Left "expected one of: size, stddev, name, number"
 
 args :: IO Option
 args = execParser argsInfo
diff --git a/src/Eventlog/AssetVersions.hs b/src/Eventlog/AssetVersions.hs
--- a/src/Eventlog/AssetVersions.hs
+++ b/src/Eventlog/AssetVersions.hs
@@ -39,3 +39,6 @@
 datatablesButtonsHTML5Version,datatablesButtonsHTML5URL :: String
 datatablesButtonsHTML5Version = "2.0.1"
 datatablesButtonsHTML5URL = "https://cdn.datatables.net/buttons/2.0.1/js/buttons.html5.min.js"
+popperVersion,popperURL :: String
+popperVersion = "1.14.3"
+popperURL = "https://cdn.jsdelivr.net/npm/popper.js@1.14.3"
diff --git a/src/Eventlog/Data.hs b/src/Eventlog/Data.hs
--- a/src/Eventlog/Data.hs
+++ b/src/Eventlog/Data.hs
@@ -1,10 +1,18 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE TupleSections #-}
-module Eventlog.Data (generateJson, generateJsonValidate, generateJsonData, EventlogType(..) ) where
+module Eventlog.Data
+  ( generateJson
+  , generateJsonValidate
+  , generateJsonData
+  , EventlogType(..)
+  , HeapProfileData(..)
+  , TickyProfileData(..)
+  ) where
 
 import Prelude hiding (readFile)
-import Data.Aeson (Value(..), (.=), object)
+import Data.Aeson ((.=), object)
 import qualified Data.Map as Map
+import Data.Maybe
 
 import Eventlog.Args (Args(..))
 import Eventlog.Bands (bands)
@@ -12,25 +20,19 @@
 import qualified Eventlog.HeapProf as H
 import Eventlog.Prune
 import Eventlog.Vega
-import Eventlog.Types (Header(..), ProfData(..), HeapProfBreakdown(..))
+import Eventlog.Types
 import Data.List
 import Data.Ord
 import Eventlog.Trie
 import Eventlog.Detailed
-import Text.Blaze.Html
 import Eventlog.Ticky
-import Data.Word
 
-
-data EventlogType = HeapProfile  (Header, Value, Maybe Value, Maybe Html)
-                  | TickyProfile (Header, Word64, Double, Html)
-
-generateJsonData :: Args -> ProfData -> IO (Header, Value, Maybe Value, Maybe Html)
-generateJsonData a (ProfData h binfo ccMap fs traces heap_info ipes _ticky_counter _ticky_samples _total_allocs) = do
+generateJsonData :: Args -> ProfData -> HeapProfileData
+generateJsonData a (ProfData h binfo ccMap fs traces heap_info ipes _ticky_counter _ticky_samples _total_allocs) =
   let keeps = pruneBands a binfo
       bs = bands h (Map.map fst keeps) fs
       combinedJson = object [
-          "samples" .= bandsToVega keeps bs
+          "samples" .= bandsToVega bucket_desc keeps bs
         , "traces"  .= tracesToVega traces
         , "heap"    .= heapToVega heap_info
         ]
@@ -41,16 +43,27 @@
                 Just HeapProfBreakdownCostCentre -> Just (outputTree ccMap mdescs)
                 _ -> Nothing
 
-  let use_ipes = case hHeapProfileType h of
+      use_ipes = case hHeapProfileType h of
                    Just HeapProfBreakdownInfoTable -> Just ipes
                    _ -> Nothing
+
+      -- If we have IPE info, try to translate info table pointers to names
+      bucket_desc bucket_info = fromMaybe desc $ do
+          ipe_map <- use_ipes
+          iptr <- toItblPointer_maybe (Bucket desc)
+          itl <- Map.lookup iptr ipe_map
+          pure (itlName itl <> " (" <> desc <> ")")
+        where
+          desc = shortDescription bucket_info
+
       desc_buckets = pruneDetailed a binfo
       bs' = bands h (Map.map fst desc_buckets) fs
       closure_table =
         case detailedLimit a of
           Just 0 ->  Nothing
+          _ | null desc_buckets -> Nothing
           _ -> Just (renderClosureInfo bs' use_ipes desc_buckets)
-  return (h, combinedJson, cc_descs, closure_table)
+  in HeapProfileData combinedJson cc_descs closure_table
 
 generateJson :: FilePath -> Args -> IO EventlogType
 generateJson = generateJsonValidate (const (return ()))
@@ -61,14 +74,12 @@
   let chunk = if heapProfile a then H.chunk else E.chunk a
   dat <- chunk file
   validate dat
-  case profTickySamples dat of
-    [] -> HeapProfile <$> generateJsonData a dat
-    -- If there are any ticky samples then generate a ticky profile
-    _  -> do
-      let (percen, html) = renderTicky (profTotalAllocations dat) (profTickyCounters dat) (profItl dat) (profTickySamples dat)
-      return $ TickyProfile ( profHeader dat
-                                , profTotalAllocations dat
-                                , percen
-                                , html )
-
+  pure $ EventlogType (profHeader dat)
+                      (Just (generateJsonData a dat))
+                      (if not (null (profTickySamples dat)) then Just (generateTickyData dat) else Nothing)
+                      -- If there are any ticky samples then generate a ticky profile
 
+generateTickyData :: ProfData -> TickyProfileData
+generateTickyData dat =
+      let (percen, html) = renderTicky (profTotalAllocations dat) (profTickyCounters dat) (profItl dat) (profTickySamples dat)
+      in TickyProfileData (profTotalAllocations dat) percen html
diff --git a/src/Eventlog/Detailed.hs b/src/Eventlog/Detailed.hs
--- a/src/Eventlog/Detailed.hs
+++ b/src/Eventlog/Detailed.hs
@@ -29,7 +29,7 @@
              Just ipes -> mkClosureInfo (\k _ -> toItblPointer k) raw_bs ipes
              Nothing   -> Map.map (\v -> (None, v)) raw_bs
 
-  H.table ! A.id "closure_table" ! A.class_ "table table-striped closureTable" ! A.hidden "true" $ do
+  H.table ! A.id "closure_table" ! A.class_ "table table-striped closureTable" $ do
     H.thead $ H.tr $ do
       H.th "Profile"
       numTh "n"
@@ -114,7 +114,6 @@
         \$.fn.sparkline.defaults.common.chartRangeMin = 0;\
         \$.fn.sparkline.defaults.common.width = 200;\
         \$('.linechart').sparkline();\
-        \$(\".closureTable\").removeAttr(\"hidden\")\
 \});"
 
 getBandValues :: Int
diff --git a/src/Eventlog/HtmlTemplate.hs b/src/Eventlog/HtmlTemplate.hs
--- a/src/Eventlog/HtmlTemplate.hs
+++ b/src/Eventlog/HtmlTemplate.hs
@@ -1,4 +1,5 @@
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell #-}
 module Eventlog.HtmlTemplate where
 
 import Data.Aeson (Value, encode)
@@ -6,38 +7,42 @@
 import Data.String
 import Data.Text (Text, append)
 import qualified Data.Text as T
-import qualified Data.Text.Lazy.Encoding as T
+import qualified Data.Text.Encoding as T
+import qualified Data.Text.Lazy.Encoding as TL
 import qualified Data.Text.Lazy as TL
 --import Text.Blaze.Html
 import Text.Blaze.Html5            as H
 import Text.Blaze.Html5.Attributes as A
 import Text.Blaze.Html.Renderer.String
 
+import Data.FileEmbed
+import Eventlog.Data
 import Eventlog.Javascript
 import Eventlog.Args
 import Eventlog.Types (Header(..), HeapProfBreakdown(..))
+import Eventlog.Rendering.Bootstrap
+import Eventlog.Rendering.Types
 import Eventlog.VegaTemplate
 import Eventlog.AssetVersions
+import Eventlog.Ticky (tickyTab)
 import Paths_eventlog2html
 import Data.Version
 import Control.Monad
 import Data.Maybe
 
-type VizID = Int
-
 insertJsonData :: Value -> Html
 insertJsonData dat = preEscapedToHtml $ T.unlines [
     "data_json= " `append` dat' `append` ";"
   , "console.log(data_json);" ]
   where
-    dat' = TL.toStrict (T.decodeUtf8 (encode dat))
+    dat' = TL.toStrict (TL.decodeUtf8 (encode dat))
 
 insertJsonDesc :: Value -> Html
 insertJsonDesc dat = preEscapedToHtml $ T.unlines [
     "desc_json= " `append` dat' `append` ";"
   , "console.log(desc_json);" ]
   where
-    dat' = TL.toStrict (T.decodeUtf8 (encode dat))
+    dat' = TL.toStrict (TL.decodeUtf8 (encode dat))
 
 -- Dynamically bound in ccs tree
 insertColourScheme :: Text -> Html
@@ -51,16 +56,14 @@
  where
   line t = "res.view.insert(\"data_json_" <> t <>"\", data_json."<> t <>");"
 
-data IncludeTraceData = TraceData | NoTraceData
-
-encloseScript :: [Text] -> VizID -> Text -> Html
+encloseScript :: [Text] -> TabID -> Text -> Html
 encloseScript = encloseScriptX
 
-encloseRawVegaScript :: VizID -> Text -> Html
+encloseRawVegaScript :: TabID -> Text -> Html
 encloseRawVegaScript = encloseScriptX []
 
-encloseScriptX :: [Text] -> VizID -> Text -> Html
-encloseScriptX insert_data_sets vid vegaspec = preEscapedToHtml $ T.unlines ([
+encloseScriptX :: [Text] -> TabID -> Text -> Html
+encloseScriptX insert_data_sets (TabID vidt) vegaspec = preEscapedToHtml $ T.unlines ([
   "var yourVlSpec" `append` vidt `append`"= " `append` vegaspec  `append` ";"
   , "vegaEmbed('#vis" `append` vidt `append` "', yourVlSpec" `append` vidt `append` ")"
   , ".then((res) => { " ]
@@ -71,24 +74,24 @@
   [ "; res.view.resize()"
   , "; res.view.runAsync()"
   , "})" ])
-  where
-    vidt = T.pack $ show vid
 
 jsScript :: String -> Html
 jsScript url = script ! src (fromString $ url) $ ""
 css :: AttributeValue -> Html
 css url = link ! rel "stylesheet" ! href url
 
-htmlHeader :: Value -> Maybe Value -> Args -> Html
-htmlHeader dat desc as =
+htmlHeader :: Maybe HeapProfileData -> Maybe TickyProfileData -> Args -> Html
+htmlHeader mb_hpd mb_ticky as =
     H.head $ do
     H.title "eventlog2html - Heap Profile"
     meta ! charset "UTF-8"
-    script $ insertJsonData dat
-    maybe (return ()) (script . insertJsonDesc) desc
+    forM_ mb_hpd $ \ (HeapProfileData dat desc _) -> do
+      script $ insertJsonData dat
+      maybe (return ()) (script . insertJsonDesc) desc
     script $ insertColourScheme (userColourScheme as)
     if not (noIncludejs as)
       then do
+        script $ preEscapedToHtml popper
         script $ preEscapedToHtml vegaLite
         script $ preEscapedToHtml vega
         script $ preEscapedToHtml vegaEmbed
@@ -97,81 +100,73 @@
         script $ preEscapedToHtml bootstrap
         script $ preEscapedToHtml fancytable
         script $ preEscapedToHtml sparkline
+        when has_ticky $ do
+          H.style  $ preEscapedToHtml datatablesCSS
+          H.style  $ preEscapedToHtml datatablesButtonsCSS
+          script $ preEscapedToHtml datatables
+          script $ preEscapedToHtml datatablesButtons
+          script $ preEscapedToHtml datatablesHtml5
+          H.style $ preEscapedToHtml imagesCSS
       else do
+        jsScript popperURL
         jsScript vegaURL
         jsScript vegaLiteURL
         jsScript vegaEmbedURL
         jsScript jqueryURL
         css (preEscapedStringValue bootstrapCSSURL)
         jsScript bootstrapURL
-        css "//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic"
         jsScript fancyTableURL
         jsScript sparklinesURL
+        when has_ticky $ do
+          css (preEscapedStringValue datatablesCSSURL)
+          css (preEscapedStringValue datatablesButtonsCSSURL)
+          jsScript datatablesURL
+          jsScript datatablesButtonsURL
+          jsScript datatablesButtonsHTML5URL
+    when has_ticky $
+      script $ preEscapedToHtml datatablesEllipsis
     -- Include this last to overwrite some milligram styling
     H.style $ preEscapedToHtml stylesheet
-
+  where
+    has_ticky = isJust mb_ticky
 
-template :: Header -> Value -> Maybe Value -> Maybe Html -> Args -> Html
-template header' dat cc_descs closure_descs as = docTypeHtml $ do
+template :: EventlogType
+         -> Args
+         -> [TabGroup]
+         -> Html
+template (EventlogType header' x y) as tab_groups = docTypeHtml $ do
   H.stringComment $ "Generated with eventlog2html-" <> showVersion version
-  htmlHeader dat cc_descs as
-  body $ H.div ! class_ "container" $ do
-    H.div ! class_ "row" $ do
-      H.div ! class_ "column" $ do
-        h1 $ a ! href "https://mpickering.github.io/eventlog2html" $ "eventlog2html"
-
-    H.div ! class_ "row" $ do
-      H.div ! class_ "column" $ do
-        "Options: "
-        code $ toHtml $ hJob header'
-
+  htmlHeader x y as
+  body $ H.div ! class_ "container-fluid" $ do
+    H.div ! class_ "row" $ navbar tab_groups
     H.div ! class_ "row" $ do
-      H.div ! class_ "column" $ do
-        "Created at: "
-        code $ toHtml $ hDate header'
+      H.div ! class_ "col tab-content custom-tab" $ do
+        forM_ tab_groups $ \group -> do
+          case group of
+            SingleTab tab -> renderTab header' tab
+            ManyTabs _ tabs -> mapM_ (renderTab header') tabs
 
-    forM_ (hHeapProfileType header') $ \prof_type -> do
-      H.div ! class_ "row" $ do
-        H.div ! class_ "column" $ do
-          "Type of profile: "
-          code $ toHtml $ ppHeapProfileType prof_type
+    script $ preEscapedToHtml tablogic
 
-    H.div ! class_ "row" $ do
-      H.div ! class_ "column" $ do
-        "Sampling rate in seconds: "
-        code $ toHtml $ hSamplingRate header'
+renderTab :: Header -> Tab -> Html
+renderTab header' tab =
+  H.div ! A.id (toValue (tabIDToTabID (tabId tab))) ! class_ ("tab-pane tabviz " <> status) $ H.div ! class_ "row" $ do
+    forM_ (tabContent tab) $ \stuff -> H.div ! class_ "col" $ do
+      stuff
+      perTabFooter header'
+    forM_ (tabDocs tab) $ \docs -> H.div ! class_ "col" $ docs
+  where
+    status = if tabActive tab then "show active" else ""
 
-    H.div ! class_ "row" $ do
-      H.div ! class_ "column" $ do
-        button ! class_ "tablink button-black" ! onclick "changeTab('areachart', this)" ! A.id "defaultOpen" $ "Area Chart"
-        button ! class_ "tablink button-black" ! onclick "changeTab('normalizedchart', this)" $ "Normalized"
-        button ! class_ "tablink button-black" ! onclick "changeTab('streamgraph', this)" $ "Streamgraph"
-        button ! class_ "tablink button-black" ! onclick "changeTab('linechart', this)" $ "Linechart"
-        button ! class_ "tablink button-black" ! onclick "changeTab('heapchart', this)" $ "Heap"
-        when (isJust cc_descs) $ do
-          button ! class_ "tablink button-black" ! onclick "changeTab('cost-centres', this)" $ "Cost Centres"
-        when (isJust closure_descs) $ do
-          button ! class_ "tablink button-black" ! onclick "changeTab('closures', this)" $ "Detailed"
+perTabFooter :: Header -> Html
+perTabFooter header' = do
     H.div ! class_ "row" $ do
-      H.div ! class_ "column" $ do
-        let itd = if (noTraces as) then NoTraceData else TraceData
-        mapM_ (\(vid, chartname, conf) ->
-                  H.div ! A.id chartname ! class_ "tabviz" $ do
-                    renderChart itd conf True vid
-                      (TL.toStrict (encodeToLazyText (vegaJson (htmlConf as conf)))))
-          [(1, "areachart",  AreaChart Stacked)
-          ,(2, "normalizedchart", AreaChart Normalized)
-          ,(3, "streamgraph", AreaChart StreamGraph)
-          ,(4, "linechart", LineChart)
-          ,(5, "heapchart", HeapChart) ]
-
-        when (isJust cc_descs) $ do
-          H.div ! A.id "cost-centres" ! class_ "tabviz" $ do
-            renderChart itd LineChart False 6 treevega
-        forM_ closure_descs $ \v -> do
-          H.div ! A.id "closures" ! class_ "tabviz" $ do
-            v
-    script $ preEscapedToHtml tablogic
+      H.div ! class_ "col" $ do
+          toHtml $ maybe "No heap profile" ppHeapProfileType (hHeapProfileType header')
+          ", created at "
+          code $ toHtml $ hDate header'
+          " by "
+          code $ toHtml $ hJob header'
 
 
 select_data :: IncludeTraceData -> ChartType -> [Text]
@@ -186,27 +181,39 @@
 
 
 htmlConf :: Args -> ChartType -> ChartConfig
-htmlConf as ct = ChartConfig 1200 1000 (not (noTraces as)) (userColourScheme as) "set1" ct (fromIntegral <$> (fixedYAxis as))
+htmlConf as ct =
+  ChartConfig
+    { cwidth = 1200
+    , cheight = 1000
+    , traces = not (noTraces as)
+    , colourScheme = userColourScheme as
+    , lineColourScheme = "set1"
+    , chartType = ct
+    , fixedYAxisExtent = fromIntegral <$> fixedYAxis as
+    }
 
-renderChart :: IncludeTraceData -> ChartType -> Bool -> VizID -> Text -> Html
+renderChart :: IncludeTraceData -> ChartType -> Bool -> TabID -> Text -> Html
 renderChart itd ct vega_lite vid vegaSpec = do
     let fields = select_data itd ct
-    H.div ! A.id (fromString $ "vis" ++ show vid) ! class_ "chart" $ ""
+    H.div ! A.id (toValue (tabIDToVizID vid)) ! class_ "chart" $ ""
     script ! type_ "text/javascript" $ do
       if vega_lite
         then encloseScript fields vid vegaSpec
         else encloseRawVegaScript vid vegaSpec
 
-renderChartWithJson :: IncludeTraceData -> ChartType -> Int -> Value -> Text -> Html
+renderChartWithJson :: IncludeTraceData -> ChartType -> TabID -> Value -> Text -> Html
 renderChartWithJson itd ct k dat vegaSpec = do
     script $ insertJsonData dat
     renderChart itd ct True k vegaSpec
 
 
-templateString :: Header -> Value -> Maybe Value -> Maybe Html -> Args -> String
-templateString header' dat cc_descs closure_descs as =
-  renderHtml $ template header' dat cc_descs closure_descs as
+templateString :: EventlogType
+               -> Args
+               -> String
+templateString x as =
+  renderHtml $ template x as $ allTabs x as
 
+
 ppHeapProfileType :: HeapProfBreakdown -> Text
 ppHeapProfileType (HeapProfBreakdownCostCentre) = "Cost centre profiling (implied by -hc)"
 ppHeapProfileType (HeapProfBreakdownModule) = "Profiling by module (implied by -hm)"
@@ -216,3 +223,95 @@
 ppHeapProfileType (HeapProfBreakdownBiography) = "Biographical profiling (implied by -hb)"
 ppHeapProfileType (HeapProfBreakdownClosureType) = "Basic heap profile (implied by -hT)"
 ppHeapProfileType (HeapProfBreakdownInfoTable) = "Info table profile (implied by -hi)"
+
+
+allTabs :: EventlogType
+        -> Args
+        -> [TabGroup]
+allTabs (EventlogType h x y) as =
+    [SingleTab (metaTab h as)] ++
+    maybe [] (allHeapTabs h as) x ++
+    [tickyProfileTabs y]
+
+metaTab :: Header -> Args -> Tab
+metaTab header' _as =
+    (mkTab "Meta" "meta" metadata Nothing) { tabActive = True }
+  where
+    metadata = do
+      "Rendered by "
+      a ! href "https://mpickering.github.io/eventlog2html" $ "eventlog2html " <> toHtml (showVersion version)
+      when (has_heap_profile header') $
+        H.div ! class_ "row" $ do
+          H.div ! class_ "col" $ do
+            "Sampling rate: "
+            code $ toHtml $ hSamplingRate header'
+            " seconds between heap samples"
+
+has_heap_profile :: Header -> Bool
+has_heap_profile h = isJust (hHeapProfileType h)
+
+allHeapTabs :: Header -> Args -> HeapProfileData -> [TabGroup]
+allHeapTabs header' as x =
+    [ heapTab as
+    , heapProfileTabs header' as x
+    , costCentresTab as x
+    , detailedTab x
+    ]
+
+heapTab :: Args -> TabGroup
+heapTab as = SingleTab $ mkTab "Heap" tabid (mk as HeapChart tabid) (Just heapDocs)
+  where
+    tabid = "heapchart"
+
+heapDocs :: Html
+heapDocs = H.div $ preEscapedToHtml $ T.decodeUtf8 $(embedFile "inline-docs/heap.html")
+
+
+heapProfileTabs :: Header -> Args -> HeapProfileData -> TabGroup
+heapProfileTabs header' as _
+  | has_heap_profile header' = ManyTabs "Heap Profile" $
+    [ mkTab "Area Chart"  "areachart"       (mk as (AreaChart Stacked)     "areachart")       noDocs
+    , mkTab "Normalized"  "normalizedchart" (mk as (AreaChart Normalized)  "normalizedchart") noDocs
+    , mkTab "Streamgraph" "streamgraph"     (mk as (AreaChart StreamGraph) "streamgraph")     noDocs
+    , mkTab "Linechart"   "linechart"       (mk as LineChart               "linechart")       noDocs
+    ]
+  | otherwise = SingleTab noHeapProfileTab
+
+noHeapProfileTab :: Tab
+noHeapProfileTab = mkUnavailableTab "Heap Profile" "heap_profile"  noHeapProfileDocs
+
+noHeapProfileDocs :: Html
+noHeapProfileDocs = H.div $ preEscapedToHtml $ T.decodeUtf8 $(embedFile "inline-docs/no-heap-profile.html")
+
+
+mk :: Args -> ChartType -> TabID -> Html
+mk as conf vid = renderChart itd conf True vid
+                      (TL.toStrict (encodeToLazyText (vegaJson (htmlConf as conf))))
+  where
+    itd = if noTraces as then NoTraceData else TraceData
+
+detailedTab :: HeapProfileData -> TabGroup
+detailedTab (HeapProfileData _dat _cc_descs closure_descs) =
+    SingleTab $ mkOptionalTab "Detailed" "closures" Prelude.id noDocs noDetailedDocs closure_descs
+
+noDetailedDocs :: Html
+noDetailedDocs = H.div $ preEscapedToHtml $ T.decodeUtf8 $(embedFile "inline-docs/no-detailed.html")
+
+
+costCentresTab :: Args -> HeapProfileData -> TabGroup
+costCentresTab as (HeapProfileData _dat cc_descs _) =
+    SingleTab $ mkOptionalTab "Cost Centres" "costcentres" (const stuff) noDocs noCostCentresDocs cc_descs
+  where
+    tabIx = "costcentres"
+    itd = if noTraces as then NoTraceData else TraceData
+    stuff = renderChart itd LineChart False tabIx treevega
+
+noCostCentresDocs :: Html
+noCostCentresDocs = H.div $ preEscapedToHtml $ T.decodeUtf8 $(embedFile "inline-docs/no-cost-centres.html")
+
+
+tickyProfileTabs :: Maybe TickyProfileData -> TabGroup
+tickyProfileTabs = SingleTab . mkOptionalTab "Ticky" "ticky" tickyTab noDocs noTickyDocs
+
+noTickyDocs :: Html
+noTickyDocs = H.div $ preEscapedToHtml $ T.decodeUtf8 $(embedFile "inline-docs/no-ticky.html")
diff --git a/src/Eventlog/Javascript.hs b/src/Eventlog/Javascript.hs
--- a/src/Eventlog/Javascript.hs
+++ b/src/Eventlog/Javascript.hs
@@ -21,6 +21,8 @@
   , stylesheet
   , tablogic
   , treevega
+
+  , popper
   ) where
 
 import Data.Text
@@ -81,3 +83,6 @@
 
 treevega :: Text
 treevega = decodeUtf8 $(embedFile "javascript/ccmap.vg")
+
+popper :: Text
+popper = decodeUtf8 $(embedFile $ "javascript/generated/popper.js@" <> popperVersion)
diff --git a/src/Eventlog/Prune.hs b/src/Eventlog/Prune.hs
--- a/src/Eventlog/Prune.hs
+++ b/src/Eventlog/Prune.hs
@@ -1,5 +1,6 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE ViewPatterns #-}
+{-# LANGUAGE TypeApplications #-}
 module Eventlog.Prune
   ( pruneBands, pruneDetailed
   ) where
@@ -11,6 +12,9 @@
 
 import Eventlog.Args (Args(..), Sort(..))
 import Data.Maybe
+import Data.Word (Word64)
+import Text.Read (readMaybe)
+import qualified Data.Text as T
 
 type Compare a = a -> a -> Ordering
 
@@ -21,15 +25,20 @@
 getComparison Args { sorting = StdDev, reversing = True } = cmpStdDevAscending
 getComparison Args { sorting = Name,   reversing = True }  = cmpNameDescending
 getComparison Args { sorting = Name,   reversing = False } = cmpNameAscending
+getComparison Args { sorting = Number,   reversing = True }  = cmpNumberDescending
+getComparison Args { sorting = Number,   reversing = False } = cmpNumberAscending
 getComparison Args { sorting = Gradient,   reversing = True }  = cmpGradientAscending
 getComparison Args { sorting = Gradient,   reversing = False } = cmpGradientDescending
 
 cmpNameAscending, cmpNameDescending,
+  cmpNumberAscending, cmpNumberDescending,
   cmpStdDevAscending, cmpStdDevDescending,
   cmpSizeAscending, cmpSizeDescending,
   cmpGradientAscending, cmpGradientDescending :: Compare (Bucket, BucketInfo)
 cmpNameAscending = comparing fst
 cmpNameDescending = flip cmpNameAscending
+cmpNumberAscending (Bucket a, _) (Bucket b, _) = comparing (readMaybe @Word64 . T.unpack) a b <> compare a b
+cmpNumberDescending = flip cmpNumberAscending
 cmpStdDevAscending = comparing (bucketStddev . snd)
 cmpStdDevDescending = flip cmpStdDevAscending
 cmpSizeAscending = comparing (bucketTotal . snd)
diff --git a/src/Eventlog/Rendering/Bootstrap.hs b/src/Eventlog/Rendering/Bootstrap.hs
new file mode 100644
--- /dev/null
+++ b/src/Eventlog/Rendering/Bootstrap.hs
@@ -0,0 +1,65 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Eventlog.Rendering.Bootstrap where
+
+import Eventlog.Rendering.Types
+
+import Control.Monad
+import Data.String
+import qualified Data.Text as T
+import Text.Blaze.Html5            as H
+import Text.Blaze.Html5.Attributes as A
+import Text.Blaze.Internal (attribute)
+
+dataToggle :: AttributeValue -> Attribute
+dataToggle = attribute "data-toggle" " data-toggle=\""
+{-# INLINE dataToggle #-}
+
+dataTarget :: AttributeValue -> Attribute
+dataTarget = attribute "data-target" " data-target=\""
+{-# INLINE dataTarget #-}
+
+ariaControls :: AttributeValue -> Attribute
+ariaControls = attribute "aria-controls" " aria-controls=\""
+{-# INLINE ariaControls #-}
+
+ariaExpanded :: AttributeValue -> Attribute
+ariaExpanded = attribute "aria-expanded" " aria-expanded=\""
+{-# INLINE ariaExpanded #-}
+
+ariaLabel :: AttributeValue -> Attribute
+ariaLabel = attribute "aria-label" " aria-label=\""
+{-# INLINE ariaLabel #-}
+
+navbar :: [TabGroup] -> Html
+navbar tab_groups = do
+  H.ul ! A.id "vizTabs" ! class_ "nav nav-tabs" $ do
+    forM_ tab_groups $ \group -> do
+      case group of
+        SingleTab tab ->
+          H.li ! class_ "nav-item" $
+            H.a ! A.id (toValue (tabIDToNavItemID (tabId tab)))
+                ! class_ (tabClasses tab)
+                ! href (toValue (tabIDToHref (tabId tab)))
+                ! dataToggle "tab"
+                ! dataTarget (toValue (tabIDToHref (tabId tab)))
+                $ fromString (tabName tab)
+        ManyTabs group_name tabs ->
+          H.li ! class_ "nav-item dropdown" $ do
+            H.a ! class_ "nav-link dropdown-toggle"
+                ! href "#"
+                ! dataToggle "dropdown"
+                $ fromString group_name
+            H.div ! class_ "dropdown-menu" $
+              forM_ tabs $ \tab ->
+                H.a ! A.id (toValue (tabIDToNavItemID (tabId tab)))
+                    ! class_ "dropdown-item"
+                    ! href (toValue (tabIDToHref (tabId tab)))
+                    ! dataToggle "tab"
+                    ! dataTarget (toValue (tabIDToHref (tabId tab)))
+                    $ fromString (tabName tab)
+
+tabClasses :: Tab -> AttributeValue
+tabClasses tab = toValue $ T.intercalate " " $
+  "nav-link" :
+  [ "active" | tabActive tab ] ++
+  [ "not-available" | tabDisabled tab ]
diff --git a/src/Eventlog/Rendering/Types.hs b/src/Eventlog/Rendering/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Eventlog/Rendering/Types.hs
@@ -0,0 +1,65 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Eventlog.Rendering.Types where
+
+import Data.Char
+import Data.String
+import qualified Data.Text as T
+import Text.Blaze.Html5
+
+data IncludeTraceData
+  = TraceData
+  | NoTraceData
+
+-- | Tab IDs must be usable as HTML and Javascript identifiers, so we allow a
+-- limited selection of characters. This is enforced only at runtime by
+-- 'mkTabID' and the 'IsString' instance, but that seems good enough for now.
+newtype TabID = TabID T.Text
+
+instance IsString TabID where
+  fromString = mkTabID
+
+mkTabID :: String -> TabID
+mkTabID s
+    | all valid s = TabID (T.pack s)
+    | otherwise   = error $ "mkTabID: invalid tab ID: " ++ s
+    where
+      valid c = isAscii c && (isAlpha c || isDigit c || c == '_')
+
+tabIDToVizID :: TabID -> T.Text
+tabIDToVizID (TabID t) = "vis" <> t
+
+tabIDToNavItemID :: TabID -> T.Text
+tabIDToNavItemID (TabID t) = t <> "-tab"
+
+tabIDToTabID :: TabID -> T.Text
+tabIDToTabID (TabID t) = t
+
+tabIDToHref :: TabID -> T.Text
+tabIDToHref (TabID t) = "#" <> t
+
+
+data TabGroup = ManyTabs String [Tab]
+              | SingleTab Tab
+
+data Tab = Tab { tabName     :: String
+               , tabId       :: TabID
+               , tabContent  :: Maybe Html
+               , tabDocs     :: Maybe Html
+               , tabActive   :: Bool -- ^ Active by default?
+               , tabDisabled :: Bool
+               }
+
+mkTab :: String -> TabID -> Html -> Maybe Html -> Tab
+mkTab name id_ content docs = Tab name id_ (Just content) docs False False
+
+mkUnavailableTab :: String -> TabID -> Html -> Tab
+mkUnavailableTab name id_ docs = Tab name id_ Nothing (Just docs) False True
+
+mkOptionalTab :: String -> TabID -> (a -> Html) -> Maybe Html -> Html -> Maybe a -> Tab
+mkOptionalTab name id_ mk_content docs no_docs mb = case mb of
+    Nothing -> mkUnavailableTab name id_ no_docs
+    Just v  -> mkTab name id_ (mk_content v) docs
+
+noDocs :: Maybe Html
+noDocs = Nothing
diff --git a/src/Eventlog/Ticky.hs b/src/Eventlog/Ticky.hs
--- a/src/Eventlog/Ticky.hs
+++ b/src/Eventlog/Ticky.hs
@@ -3,13 +3,11 @@
 -- Functions for rendering ticky sample information
 {-# LANGUAGE RecordWildCards #-}
 {-# LANGUAGE QuasiQuotes #-}
-module Eventlog.Ticky where
+module Eventlog.Ticky (tickyTab, renderTicky) where
 
-import Eventlog.Types
 import qualified Data.Map as Map
 import Data.Word
 
-import Data.String
 import qualified Data.Text as T
 --import Text.Blaze.Html
 import qualified Text.Blaze.Html5            as H
@@ -17,38 +15,20 @@
     ( preEscapedToHtml,
       toHtml,
       dataAttribute,
-      preEscapedStringValue,
-      stringComment,
       Html,
       (!),
-      AttributeValue,
-      body,
-      button,
       code,
       div,
-      docTypeHtml,
-      h1,
-      head,
-      link,
-      meta,
       script,
-      style,
       table,
       td,
       th,
       thead,
-      title,
       tr )
 import Text.Blaze.Html5.Attributes as A
-    ( charset, class_, hidden, href, id, onclick, rel, src)
-import Text.Blaze (customAttribute)
-import Text.Blaze.Html.Renderer.String
+    ( class_, id )
 
-import Eventlog.Javascript
-import Eventlog.Args
-import Eventlog.AssetVersions
-import Paths_eventlog2html
-import Data.Version ( showVersion )
+import Eventlog.Types
 import Text.RawString.QQ
 import Data.Fixed
 import Control.Monad
@@ -86,67 +66,8 @@
   (sortBy (comparing tickySampleTime) samples)
 
 
-jsScript :: String -> Html
-jsScript url = script ! src (fromString $ url) $ ""
-css :: AttributeValue -> Html
-css url = link ! rel "stylesheet" ! href url
-
-htmlHeader :: Args -> Html
-htmlHeader as =
-    H.head $ do
-    H.title "eventlog2html - Ticky Profile"
-    meta ! charset "utf-8"
-    if not (noIncludejs as)
-      then do
-        script $ preEscapedToHtml jquery
-        H.style  $ preEscapedToHtml bootstrapCSS
-        script $ preEscapedToHtml bootstrap
-        H.style  $ preEscapedToHtml datatablesCSS
-        H.style  $ preEscapedToHtml datatablesButtonsCSS
-        script $ preEscapedToHtml datatables
-        script $ preEscapedToHtml datatablesButtons
-        script $ preEscapedToHtml datatablesHtml5
-        H.style $ preEscapedToHtml imagesCSS
-        script $ preEscapedToHtml sparkline
-      else do
-        jsScript vegaURL
-        jsScript vegaLiteURL
-        jsScript vegaEmbedURL
-        jsScript jqueryURL
-        css (preEscapedStringValue bootstrapCSSURL)
-        jsScript bootstrapURL
-        css "https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic"
-        jsScript fancyTableURL
-        css (preEscapedStringValue datatablesCSSURL)
-        css (preEscapedStringValue datatablesButtonsCSSURL)
-        jsScript datatablesURL
-        jsScript datatablesButtonsURL
-        jsScript datatablesButtonsHTML5URL
-        jsScript sparklinesURL
-    script $ preEscapedToHtml datatablesEllipsis
-    -- Include this last to overwrite other styling
-    H.style $ preEscapedToHtml stylesheet
-
-
-template :: Header -> Word64 -> Double ->  Html -> Args -> Html
-template header' total ticked_percen v as = docTypeHtml $ do
-  H.stringComment $ "Generated with eventlog2html-" <> showVersion version
-  htmlHeader as
-  body $ H.div ! class_ "container" $ do
-    H.div ! class_ "row" $ do
-      H.div ! class_ "column" $ do
-        h1 $ H.a ! href "https://mpickering.github.io/eventlog2html" $ "eventlog2html"
-
-    H.div ! class_ "row" $ do
-      H.div ! class_ "column" $ do
-        "Options: "
-        code $ toHtml $ hJob header'
-
-    H.div ! class_ "row" $ do
-      H.div ! class_ "column" $ do
-        "Created at: "
-        code $ toHtml $ hDate header'
-
+tickyTab :: TickyProfileData -> Html
+tickyTab (TickyProfileData total ticked_percen v) = do
     H.div ! class_ "row" $ do
       H.div ! class_ "column" $ do
         "Total Allocations: "
@@ -154,19 +75,9 @@
       H.div ! class_ "column cheader" $ do
         "Allocations Ticked (%): "
         code $ toHtml $ toHtml (render  $ trunc (ticked_percen * 100))
-
     H.div ! class_ "row" $ do
-      H.div ! class_ "column" $ do
-        button ! class_ "tablink button-black" ! onclick "changeTab('table', this)" ! A.id "defaultOpen" $ "Table"
-    H.div ! class_ "row" $ do
           H.div ! A.id "table" ! class_ "tabviz" $ v
-    script $ preEscapedToHtml tablogic
 
-
-tickyTemplateString :: Header -> Word64 -> Double -> Html -> Args -> String
-tickyTemplateString header' tot_allocs ticked_per ticky_table as =
-  renderHtml $ template header' tot_allocs ticked_per ticky_table as
-
 -- Table rendering
 trunc :: Double -> Fixed E2
 trunc = realToFrac
@@ -178,7 +89,7 @@
                   -> Map.Map TickyCounterId (InfoTableLocStatus, (TickyCounter, AccumStats, Double))
                   -> Html
 renderTickyInfo with_ipe ticky_samples = do
-  H.table ! A.id "closure_table" ! A.class_ "table table-striped closureTable" ! A.hidden "true" $ do
+  H.table ! A.id "closure_table" ! A.class_ "table table-striped closureTable" $ do
     H.thead $ H.tr $ headFoot
 --      H.th "Profile"
 --      numTh "n"
@@ -266,21 +177,6 @@
   | cl_args == 0 = (2 + fvs) * 8
   | otherwise  = (1 + fvs) * 8
 
-
-
-renderSpark :: Int -> [(Double, Word64, Word64)] -> Html
-renderSpark size vs = H.span ! A.class_ "linechart"
-  ! customAttribute "data-allocd" (H.preEscapedTextValue $ T.intercalate "," (map renderLine vs))
-  ! customAttribute "data-entries" (H.preEscapedTextValue $ T.intercalate "," (map renderLineEntries vs))
-  ! customAttribute "sparkChartRangeMax" (H.toValue max_alloc_n)
-  $ mempty
-  where
-    rdouble = T.pack . showFixed True . realToFrac @Double @(Fixed E2)
-    renderLine (x,w, _) = rdouble x <> ":" <> T.pack (show (w `Prelude.div` fromIntegral size))
-    renderLineEntries (x,_, e) = rdouble x <> ":" <> T.pack (show e)
-
-    max_alloc_n = last_allocd `Prelude.div` (fromIntegral size)
-    (_, last_allocd, _) = Prelude.head vs
 
 initTable :: Bool -> T.Text
 initTable ipe =
diff --git a/src/Eventlog/Types.hs b/src/Eventlog/Types.hs
--- a/src/Eventlog/Types.hs
+++ b/src/Eventlog/Types.hs
@@ -13,6 +13,7 @@
 import Numeric
 import qualified Data.Text as T
 import qualified Data.Map as Map
+import Text.Blaze.Html
 
 data Header =
   Header
@@ -103,12 +104,18 @@
   show (InfoTablePtr p) =  "0x" ++ showHex p ""
 
 toItblPointer :: Bucket -> InfoTablePtr
-toItblPointer (Bucket t) =
+toItblPointer = either error id . toItblPointer_either
+
+toItblPointer_maybe :: Bucket -> Maybe InfoTablePtr
+toItblPointer_maybe = either (const Nothing) Just . toItblPointer_either
+
+toItblPointer_either :: Bucket -> Either String InfoTablePtr
+toItblPointer_either (Bucket t) =
     let s = drop 2 (T.unpack t)
         w64 = case readHex s of
-                ((n, ""):_) -> n
-                _ -> error (show t)
-    in InfoTablePtr w64
+                ((n, ""):_) -> pure (InfoTablePtr n)
+                _ -> Left (show t)
+    in w64
 
 data InfoTableLocStatus = None -- None of the entries have InfoTableLoc
                         | Missing -- This one is just missing
@@ -124,3 +131,15 @@
               -> Map.Map k (InfoTableLocStatus, a)
 mkClosureInfo f b ipes =
   Map.mapWithKey (\k v -> (mkMissing $ Map.lookup (f k v) ipes, v)) b
+
+
+data EventlogType =
+    EventlogType
+        { eventlogHeader       :: Header
+        , eventlogHeapProfile  :: Maybe HeapProfileData
+        , eventlogTickyProfile :: Maybe TickyProfileData
+        }
+
+data HeapProfileData = HeapProfileData Value (Maybe Value) (Maybe Html)
+
+data TickyProfileData = TickyProfileData Word64 Double Html
diff --git a/src/Eventlog/Vega.hs b/src/Eventlog/Vega.hs
--- a/src/Eventlog/Vega.hs
+++ b/src/Eventlog/Vega.hs
@@ -19,14 +19,15 @@
 data VegaEntry = VegaEntry { x :: Double, y :: Double, k :: Int, c :: Text }
   deriving (Show, ToJSON, Generic)
 
-bandsToVega :: Map Bucket (Int, BucketInfo)
+bandsToVega :: (BucketInfo -> Text)
+            -> Map Bucket (Int, BucketInfo)
             -> (UArray Int Double, UArray (Int, Int) Double)
             -> [VegaEntry]
-bandsToVega ks (ts, vs) =
+bandsToVega bucket_name ks (ts, vs) =
   let (t1, tn) = bounds ts
       go (i, binfo) rs = go_1 ++ rs
         where
-          txt = shortDescription binfo
+          txt = bucket_name binfo
 
           go_1 :: [VegaEntry]
           go_1 = flip map [t1 .. tn] $ \t -> VegaEntry (ts ! t) (vs ! (i, t)) i txt
diff --git a/src/Eventlog/VegaTemplate.hs b/src/Eventlog/VegaTemplate.hs
--- a/src/Eventlog/VegaTemplate.hs
+++ b/src/Eventlog/VegaTemplate.hs
@@ -199,7 +199,7 @@
 encodingHeapSelection c =
   encoding
     . tooltip []
-    . color [MName "c", MmType Nominal, MScale [lineColourProperty c], MLegend [LValues (LStrings ["Heap Size", "Blocks Size", "Live Bytes"])]]
+    . color [MName "c", MmType Nominal, MScale [lineColourProperty c, SDomain (DStrings ["Heap Size", "Blocks Size", "Live Bytes"]) ]]
     . position X [PName "x", PmType Quantitative, PAxis [AxTitle "Time (s)"]]
     . position Y [PName "y", PmType Quantitative, PAxis [], PSort [ByFieldOp "k" Max]]
 
@@ -260,10 +260,7 @@
     . position Y ([PName "y"
                  , PmType Quantitative
                  , PAxis $ case ct of
-                             Stacked -> [AxTitle "Allocation"
-                                        , AxFormat "s"
-                                        , AxTitlePadding 15.0
-                                        , AxMaxExtent 15.0]
+                             Stacked -> [AxTitle "Allocation", AxFormat "s" ]
                              Normalized -> [AxTitle "Allocation (Normalized)", AxFormat "p"]
                              StreamGraph -> [AxTitle "Allocation (Streamgraph)", AxLabels False, AxTicks False, AxTitlePadding 10.0]
                  , PAggregate Sum
